# This code was forked from the LiveJournal project owned and operated
# by Live Journal, Inc. The code has been modified and expanded by
# Dreamwidth Studios, LLC. These files were originally licensed under
# the terms of the license supplied by Live Journal, Inc, which can
# currently be found at:
#
# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
#
# In accordance with the original license, this code and all its
# modifications are provided under the GNU General Public License.
# A copy of that license can be found in the LICENSE file included as
# part of this distribution.

# This package is for managing a queue of notifications
# for a user.
# Mischa Spiegelmock, 4/28/06

package LJ::NotificationInbox;

use strict;
use Carp qw(croak);
use LJ::NotificationItem;
use LJ::Event;
use LJ::NotificationArchive;

# constructor takes a $u
sub new {
    my ( $class, $u ) = @_;

    croak "Invalid args to construct LJ::NotificationInbox" unless $class && $u;
    croak "Invalid user" unless LJ::isu($u);

    # return singleton from $u if it already exists
    return $u->{_notification_inbox} if $u->{_notification_inbox};

    my $self = {
        uid       => $u->userid,
        count     => undef,        # defined once ->count is loaded/cached
        items     => undef,        # defined to arrayref once items loaded
        bookmarks => undef,        # defined to arrayref
    };

    return $u->{_notification_inbox} = bless $self, $class;
}

# returns the user object associated with this queue
*owner = \&u;

sub u {
    my $self = shift;
    return LJ::load_userid( $self->{uid} );
}

# Returns a list of LJ::NotificationItems in this queue.
sub items {
    my $self = shift;

    croak "items is an object method"
        unless ( ref $self ) eq __PACKAGE__;

    return @{ $self->{items} } if defined $self->{items};

    my @qids = $self->_load;

    my @items = ();
    foreach my $qid (@qids) {
        push @items, LJ::NotificationItem->new( $self->owner, $qid );
    }

    $self->{items} = \@items;

    # optimization:
    #   now items are defined ... if any are comment
    #   objects we'll instantiate those ahead of time
    #   so that if one has its data loaded they will
    #   all benefit from a coalesced load
    $self->instantiate_comment_singletons;

    $self->instantiate_message_singletons;

    return @items;
}

# returns a list of all notification items except for sent user messages
sub all_items {
    my $self = shift;

    return grep { $_->event && $_->event->class ne "LJ::Event::UserMessageSent" } $self->items;
}

# returns a list of friend-related notificationitems
sub friend_items {
    my $self = shift;

    my @friend_events = friend_event_list();

    my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
    return grep { $friend_events{ $_->event->class } } $self->items;
}

# returns a list of friend-related notificationitems
sub circle_items {
    my $self = shift;

    my @friend_events = circle_event_list();

    my %friend_events = map { "LJ::Event::" . $_ => 1 } @friend_events;
    return grep { $friend_events{ $_->event->class } } $self->items;
}

# returns a list of non user-messaging notificationitems
sub non_usermsg_items {
    my $self = shift;

    my @usermsg_events = qw(
        UserMessageRecvd
        UserMessageSent
    );

    @usermsg_events =
        ( @usermsg_events, ( LJ::Hooks::run_hook('usermsg_notification_types') || () ) );

    my %usermsg_events = map { "LJ::Event::" . $_ => 1 } @usermsg_events;
    return grep { !$usermsg_events{ $_->event->class } } $self->items;
}

# returns a list of non user-message recvd notificationitems
sub usermsg_recvd_items {
    my $self = shift;

    my @events = ('UserMessageRecvd');

    return $self->subset_items(@events);
}

# returns a list of non user-message recvd notificationitems
sub usermsg_sent_items {
    my $self = shift;

    my @events = ('UserMessageSent');

    return $self->subset_items(@events);
}

sub usermsg_sent_last_items {
    my $self = shift;

    my @events = ('UserMessageSent');

    return $self->subset_items(@events);
}

sub birthday_items {
    my $self = shift;

    my @events = ('Birthday');

    return $self->subset_items(@events);
}

sub encircled_items {
    my $self = shift;

    my @events = ('AddedToCircle');

    return $self->subset_items(@events);
}

sub entrycomment_items {
    my $self = shift;

    my @events = entrycomment_event_list();

    return $self->subset_items(@events);
}

sub pollvote_items {
    return $_[0]->subset_items('PollVote');
}

sub communitymembership_items {
    my $self = shift;

    my @community_events = communitymembership_event_list();

    my %community_events = map { "LJ::Event::" . $_ => 1 } @community_events;
    return grep { $community_events{ $_->event->class } } $self->items;
}

sub sitenotices_items {
    my $self = shift;

    my @site_events = sitenotices_event_list();

    my %site_events = map { "LJ::Event::" . $_ => 1 } @site_events;
    return grep { $site_events{ $_->event->class } } $self->items;
}

# return a subset of notificationitems
sub subset_items {
    my ( $self, @subset ) = @_;

    my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
    return grep { $subset_events{ $_->event->class } } $self->items;
}

sub singleentry_items {
    my ( $self, $itemid ) = @_;
    my %related_events = map { $_ => 1 } LJ::Event::JournalNewComment->related_events;
    return grep {
               $related_events{ $_->event->etypeid }
            && $_->event->comment
            && $_->event->comment
            ->entry    # may have been deleted, which breaks all filter to entry comments
            && $_->event->comment->entry->ditemid == $itemid
    } $self->items;
}

# return flagged notifications
sub bookmark_items {
    my $self = shift;

    return grep { $self->is_bookmark( $_->qid ) } $self->items;
}

# return archived notifications
sub archived_items {
    my $self = shift;

    my $u       = $self->u;
    my $archive = $u->notification_archive;
    return $archive->items;
}

# return unread notifications
sub unread_items {
    my $self = shift;

    return grep { $_->unread } $self->items;
}

sub count {
    my $self = shift;

    return $self->{count} if defined $self->{count};

    if ( defined $self->{items} ) {
        return $self->{count} = scalar @{ $self->{items} };
    }

    my $u = $self->owner;
    return $self->{count} =
        $u->selectrow_array( "SELECT COUNT(*) FROM notifyqueue WHERE userid=?", undef, $u->id );
}

# returns number of unread items in inbox
# returns a maximum of 1000, if you get 1000 it's safe to
# assume "more than 1000"
sub unread_count {
    my $self = shift;

    # cached unread count
    my $unread = LJ::MemCache::get( $self->_unread_memkey );

    return $unread if defined $unread;

    # not cached, load from DB
    my $u = $self->u or die "No user";

    my $sth =
        $u->prepare("SELECT COUNT(*) FROM notifyqueue WHERE userid=? AND state='N' LIMIT 1000");
    $sth->execute( $u->id );
    die $sth->errstr if $sth->err;
    ($unread) = $sth->fetchrow_array;

    # cache it
    LJ::MemCache::set( $self->_unread_memkey, $unread, 30 * 60 );

    return $unread;
}

# load the items in this queue
# returns internal items hashref
sub _load {
    my $self = shift;

    my $u = $self->u
        or die "No user object";

    # is it memcached?
    my $qids;
    $qids = LJ::MemCache::get( $self->_memkey ) and return @$qids;

    # not cached, load
    my $sth = $u->prepare( "SELECT userid, qid, journalid, etypeid, arg1, arg2, state, createtime "
            . "FROM notifyqueue WHERE userid=?" );
    $sth->execute( $u->{userid} );
    die $sth->errstr if $sth->err;

    my @items = ();
    while ( my $row = $sth->fetchrow_hashref ) {
        my $qid = $row->{qid};

        # load this item into process cache so it's ready to go
        my $qitem = LJ::NotificationItem->new( $u, $qid );
        $qitem->absorb_row($row);

        push @items, $qitem;
    }

    # sort based on create time
    @items = sort { $a->when_unixtime <=> $b->when_unixtime } @items;

    # get sorted list of ids
    my @item_ids = map { $_->qid } @items;

    # cache
    LJ::MemCache::set( $self->_memkey, \@item_ids, 86400 );

    return @item_ids;
}

sub instantiate_comment_singletons {
    my $self = shift;

    # instantiate all the comment singletons so that they will all be
    # loaded efficiently later as soon as preload_rows is called on
    # the first comment object
    my @comment_items = grep {
        $_->event
            && ( $_->event->class eq 'LJ::Event::JournalNewComment'
            || $_->event->class eq 'LJ::Event::JournalNewComment::TopLevel'
            || $_->event->class eq 'LJ::Event::JournalNewComment::Edited' )
    } $self->items;
    my @comment_events = map { $_->event } @comment_items;

    # instantiate singletons
    LJ::Comment->new( $_->event_journal, jtalkid => $_->jtalkid ) foreach @comment_events;

    return 1;
}

sub instantiate_message_singletons {
    my $self = shift;

    # instantiate all the message singletons so that they will all be
    # loaded efficiently later as soon as preload_rows is called on
    # the first message object
    my @message_items =
        grep { $_->event && $_->event->class eq 'LJ::Event::UserMessageRecvd' } $self->items;
    my @message_events = map { $_->event } @message_items;

    # instantiate singletons
    LJ::Message->load( { msgid => $_->arg1, journalid => $_->u->{userid} } )
        foreach @message_events;

    return 1;
}

sub _memkey {
    my $self   = shift;
    my $userid = $self->u->id;
    return [ $userid, "inbox:$userid" ];
}

sub _unread_memkey {
    my $self   = shift;
    my $userid = $self->u->id;
    return [ $userid, "inbox:newct:${userid}" ];
}

sub _bookmark_memkey {
    my $self   = shift;
    my $userid = $self->u->id;
    return [ $userid, "inbox:bookmarks:${userid}" ];
}

# deletes an Event that is queued for this user
# args: Queue ID to remove from queue
sub delete_from_queue {
    my ( $self, $qitem ) = @_;

    croak "delete_from_queue is an object method"
        unless ( ref $self ) eq __PACKAGE__;

    my $qid = $qitem->qid;

    croak "no queueid for queue item passed to delete_from_queue" unless int($qid);

    my $u = $self->u
        or die "No user object";

    $u->do( "DELETE FROM notifyqueue WHERE userid=? AND qid=?", undef, $u->id, $qid );
    die $u->errstr if $u->err;

    # invalidate caches
    $self->expire_cache;

    return 1;
}

sub expire_cache {
    my $self = shift;

    $self->{count} = undef;
    $self->{items} = undef;

    LJ::MemCache::delete( $self->_memkey );
    LJ::MemCache::delete( $self->_unread_memkey );
}

# FIXME: make this faster
sub oldest_item {
    my $self  = shift;
    my @items = $self->items;

    my $oldest;
    foreach my $item (@items) {
        $oldest = $item if !$oldest || $item->when_unixtime < $oldest->when_unixtime;
    }

    return $oldest;
}

# This will enqueue an event object
# Returns the enqueued item
sub enqueue {
    my ( $self, %opts ) = @_;

    my $evt     = delete $opts{event};
    my $archive = delete $opts{archive} || LJ::is_enabled('esn_archive');
    croak "No event" unless $evt;
    croak "Extra args passed to enqueue" if %opts;

    my $u = $self->u or die "No user";

    # if over the max, delete the oldest notification
    my $max  = $u->count_inbox_max;
    my $skip = $max - 1;              # number to skip to get to max
    if ( $max && $self->count >= $max ) {

        # Get list of bookmarks and ignore them when checking inbox limits
        my $bmarks = join ',', map { $self->is_bookmark( $_->qid ) ? $_->qid : () } $self->items;
        my $bookmark_sql = '';
        $bookmark_sql = "AND qid NOT IN ($bmarks) " if ($bmarks);

        my $too_old_qid = $u->selectrow_array(
            "SELECT qid FROM notifyqueue "
                . "WHERE userid=? $bookmark_sql"
                . "ORDER BY qid DESC LIMIT $skip,1",
            undef, $u->id
        );

        if ($too_old_qid) {
            $u->do( "DELETE FROM notifyqueue WHERE userid=? AND qid <= ? $bookmark_sql",
                undef, $u->id, $too_old_qid );
            $self->expire_cache;
        }
    }

    # get a qid
    my $qid = LJ::alloc_user_counter( $u, 'Q' )
        or die "Could not alloc new queue ID";

    my %item = (
        qid        => $qid,
        userid     => $u->{userid},
        journalid  => $evt->u->{userid},
        etypeid    => $evt->etypeid,
        arg1       => $evt->arg1,
        arg2       => $evt->arg2,
        state      => $evt->mark_read ? 'R' : 'N',
        createtime => $evt->eventtime_unix || time()
    );

    # insert this event into the notifyqueue table
    $u->do(
        "INSERT INTO notifyqueue ("
            . join( ",", keys %item )
            . ") VALUES ("
            . join( ",", map { '?' } values %item ) . ")",
        undef,
        values %item
    ) or die $u->errstr;

    if ($archive) {

        # insert into the notifyarchive table with State defaulted to space
        $item{state} = ' ';
        $u->do(
            "INSERT INTO notifyarchive ("
                . join( ",", keys %item )
                . ") VALUES ("
                . join( ",", map { '?' } values %item ) . ")",
            undef,
            values %item
        ) or die $u->errstr;
    }

    # invalidate memcache
    $self->expire_cache;

    return LJ::NotificationItem->new( $u, $qid );
}

# return true if item is bookmarked
sub is_bookmark {
    my ( $self, $qid ) = @_;

    # load bookmarks if they don't already exist
    $self->load_bookmarks unless defined $self->{bookmarks};

    return $self->{bookmarks}{$qid} ? 1 : 0;
}

# populate the bookmark hash
sub load_bookmarks {
    my ($self) = @_;

    my $u   = $self->u;
    my $uid = $self->u->id;
    my $row = LJ::MemCache::get( $self->_bookmark_memkey );

    $self->{bookmarks} = ();
    if ($row) {
        my @qids = unpack( "N*", $row );
        foreach my $qid (@qids) {
            $self->{bookmarks}{$qid} = 1;
        }
        return;
    }

    my $sql  = "SELECT qid FROM notifybookmarks WHERE userid=?";
    my $qids = $u->selectcol_arrayref( $sql, undef, $uid );
    die "Failed to load bookmarks: " . $u->errstr . "\n" if $u->err;

    foreach my $qid (@$qids) {
        $self->{bookmarks}{$qid} = 1;
    }

    $row = pack( "N*", @$qids );
    LJ::MemCache::set( $self->_bookmark_memkey, $row, 3600 );

    return;
}

# add a bookmark
sub add_bookmark {
    my ( $self, $qid ) = @_;

    my $u   = $self->u;
    my $uid = $self->u->id;

    return 0 unless $self->can_add_bookmark;

    my $sql = "INSERT IGNORE INTO notifybookmarks (userid, qid) VALUES (?, ?)";
    $u->do( $sql, undef, $uid, $qid );
    die "Failed to add bookmark: " . $u->errstr . "\n" if $u->err;

    # Make sure notice is in inbox
    $self->ensure_queued($qid);

    $self->{bookmarks}{$qid} = 1 if defined $self->{bookmarks};
    LJ::MemCache::delete( $self->_bookmark_memkey );

    return 1;
}

# remove bookmark
sub remove_bookmark {
    my ( $self, $qid ) = @_;

    my $u   = $self->u;
    my $uid = $self->u->id;

    my $sql = "DELETE FROM notifybookmarks WHERE userid=? AND qid=?";
    $u->do( $sql, undef, $uid, $qid );
    die "Failed to remove bookmark: " . $u->errstr . "\n" if $u->err;

    delete $self->{bookmarks}->{$qid} if defined $self->{bookmarks};
    LJ::MemCache::delete( $self->_bookmark_memkey );

    return 1;
}

# add or remove bookmark based on whether it is already bookmarked
sub toggle_bookmark {
    my ( $self, $qid ) = @_;

    my $ret =
          $self->is_bookmark($qid)
        ? $self->remove_bookmark($qid)
        : $self->add_bookmark($qid);

    return $ret;
}

# return true if can add a bookmark
sub can_add_bookmark {
    my ( $self, $count ) = @_;

    my $max = $self->u->count_bookmark_max;
    $count = $count || 1;
    my $bookmark_count = scalar $self->bookmark_items;

    return 0 if ( ( $bookmark_count + $count ) > $max );
    return 1;
}

# return count of unread bookmarks
sub bookmark_count {
    my ( $self, $count ) = @_;
    my $unread_bookmark_count = scalar grep { $_->unread } $self->bookmark_items;
    return $unread_bookmark_count;
}

sub delete_all {
    my ( $self, $view, %args ) = @_;
    my @items;

    # Unless in folder 'Bookmarks', don't fetch any bookmarks
    if ( $view eq 'all' ) {
        @items = $self->all_items;
        push @items, $self->usermsg_sent_items;
    }
    elsif ( $view eq 'usermsg_recvd' ) {
        @items = $self->usermsg_recvd_items;
    }
    elsif ( $view eq 'circle' ) {
        @items = $self->circle_items;
        push @items, $self->birthday_items;
        push @items, $self->encircled_items;
    }
    elsif ( $view eq 'birthday' ) {
        @items = $self->birthday_items;
    }
    elsif ( $view eq 'encircled' ) {
        @items = $self->encircled_items;
    }
    elsif ( $view eq 'entrycomment' ) {
        @items = $self->entrycomment_items;
    }
    elsif ( $view eq 'bookmark' ) {
        @items = $self->bookmark_items;
    }
    elsif ( $view eq 'usermsg_sent' ) {
        @items = $self->usermsg_sent_items;
    }
    elsif ( $view eq 'usermsg_sent_last' ) {
        @items = $self->usermsg_sent_last_items;
    }
    elsif ( $view eq 'singleentry' && $args{itemid} ) {
        my $itemid = $args{itemid} + 0;
        return unless $itemid;
        @items = $self->singleentry_items($itemid);
    }
    elsif ( $view eq 'pollvote' ) {
        @items = $self->pollvote_items;
    }
    elsif ( $view eq 'communitymembership' ) {
        @items = $self->communitymembership_items;
    }
    elsif ( $view eq 'sitenotices' ) {
        @items = $self->sitenotices_items;
    }
    elsif ( $view eq 'unread' ) {
        @items = $self->unread_items;
    }

    @items = grep { !$self->is_bookmark( $_->qid ) } @items
        unless $view eq 'bookmark';

    my @ret;
    foreach my $item (@items) {
        push @ret, { qid => $item->qid };
    }

    # Delete items
    foreach my $item (@items) {
        $item->delete;
    }

    return @ret;
}

sub mark_all_read {
    my ( $self, $view, %args ) = @_;
    my @items;

    # Only get items in currently viewed folder and subfolders
    if ( $view eq 'all' ) {
        @items = $self->all_items;
        push @items, $self->usermsg_sent_items;
    }
    elsif ( $view eq 'usermsg_recvd' ) {
        @items = $self->usermsg_recvd_items;
    }
    elsif ( $view eq 'circle' ) {
        @items = $self->circle_items;
        push @items, $self->birthday_items;
        push @items, $self->encircled_items;
    }
    elsif ( $view eq 'birthday' ) {
        @items = $self->birthday_items;
    }
    elsif ( $view eq 'encircled' ) {
        @items = $self->encircled_items;
    }
    elsif ( $view eq 'entrycomment' ) {
        @items = $self->entrycomment_items;
    }
    elsif ( $view eq 'bookmark' ) {
        @items = $self->bookmark_items;
    }
    elsif ( $view eq 'usermsg_sent' ) {
        @items = $self->usermsg_sent_items;
    }
    elsif ( $view eq 'usermsg_sent_last' ) {
        @items = $self->usermsg_sent_last_items;
    }
    elsif ( $view eq 'singleentry' && $args{itemid} ) {
        my $itemid = $args{itemid} + 0;
        return unless $itemid;
        @items = $self->singleentry_items($itemid);
    }
    elsif ( $view eq 'pollvote' ) {
        @items = $self->pollvote_items;
    }
    elsif ( $view eq 'communitymembership' ) {
        @items = $self->communitymembership_items;
    }
    elsif ( $view eq 'sitenotices' ) {
        @items = $self->sitenotices_items;
    }
    elsif ( $view eq 'unread' ) {
        @items = $self->unread_items;
    }

    # Mark read
    $_->mark_read foreach @items;
    return @items;
}

# Copy archive notice to inbox
# Needed when bookmarking a notice that only lives in archive
sub ensure_queued {
    my ( $self, $qid ) = @_;

    my $u = $self->u
        or die "No user object";

    my $sth = $u->prepare( "SELECT userid, qid, journalid, etypeid, arg1, arg2, state, createtime "
            . "FROM notifyarchive WHERE userid=? AND qid=?" );
    $sth->execute( $u->{userid}, $qid );
    die $sth->errstr if $sth->err;

    my $row = $sth->fetchrow_hashref;
    if ($row) {
        my %item = (
            qid        => $row->{qid},
            userid     => $row->{userid},
            journalid  => $row->{journalid},
            etypeid    => $row->{etypeid},
            arg1       => $row->{arg1},
            arg2       => $row->{arg2},
            state      => 'R',
            createtime => $row->{createtime}
        );

        # insert this event into the notifyqueue table
        $u->do(
            "INSERT IGNORE INTO notifyqueue ("
                . join( ",", keys %item )
                . ") VALUES ("
                . join( ",", map { '?' } values %item ) . ")",
            undef,
            values %item
        ) or die $u->errstr;

        # invalidate memcache
        $self->expire_cache;
    }

    return;
}

# return a count of a subset of notificationitems
sub subset_unread_count {
    my ( $self, @subset ) = @_;

    my %subset_events = map { "LJ::Event::" . $_ => 1 } @subset;
    my @events =
        grep { $_->event && $subset_events{ $_->event->class } && $_->unread } $self->items;
    return scalar @events;
}

sub all_event_count {
    my $self = shift;

    my @events =
        grep { $_->event && $_->event->class ne 'LJ::Event::UserMessageSent' && $_->unread }
        $self->items;
    return scalar @events;
}

sub friend_event_count {
    my $self = shift;
    return $self->subset_unread_count( friend_event_list() );
}

sub circle_event_count {
    my $self = shift;
    return $self->subset_unread_count( circle_event_list() );
}

sub entrycomment_event_count {
    my $self = shift;
    return $self->subset_unread_count( entrycomment_event_list() );
}

sub pollvote_event_count {
    my $self = shift;
    return $self->subset_unread_count('PollVote');
}

sub usermsg_recvd_event_count {
    my $self   = shift;
    my @events = ('UserMessageRecvd');
    return $self->subset_unread_count(@events);
}

sub usermsg_sent_event_count {
    my $self   = shift;
    my @events = ('UserMessageSent');
    return $self->subset_unread_count(@events);
}

sub communitymembership_event_count {
    my $self = shift;
    return $self->subset_unread_count( communitymembership_event_list() );
}

sub sitenotices_event_count {
    my $self = shift;
    return $self->subset_unread_count( sitenotices_event_list() );
}

# Methods that return Arrays of Event categories
sub friend_event_list {
    my @events = qw(
        AddedToCircle
        RemovedFromCircle
        InvitedFriendJoins
        CommunityInvite
        NewUserpic
    );
    @events = ( @events, ( LJ::Hooks::run_hook('friend_notification_types') || () ) );
    return @events;
}

sub circle_event_list {
    my @events = qw(
        AddedToCircle
        RemovedFromCircle
        InvitedFriendJoins
        CommunityInvite
        NewUserpic
        Birthday
    );
    @events = ( @events, ( LJ::Hooks::run_hook('friend_notification_types') || () ) );
    return @events;
}

sub entrycomment_event_list {
    my @events = (
        'JournalNewEntry',             'JournalNewComment',
        'JournalNewComment::TopLevel', 'JournalNewComment::Edited'
    );
    return @events;
}

sub communitymembership_event_list {
    my @events = ( 'CommunityJoinApprove', 'CommunityJoinReject', 'CommunityJoinRequest' );
    return @events;
}

sub sitenotices_event_list {
    my @events = (
        'ImportStatus',             'OfficialPost',
        'SecurityAttributeChanged', 'UserExpunged',
        'VgiftApproved',            'VgiftDelivered',
        'XPostFailure',             'XPostSuccess'
    );
    return @events;
}

1;
