# 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.

package LJ::User;
use strict;
no warnings 'uninitialized';

use Carp;

########################################################################
### 11. Birthdays and Age-Related Functions
###   FIXME: Some of these may be outdated when we remove under-13 accounts.

=head2 Birthdays and Age-Related Functions
=cut

# Users age based off their profile birthdate
sub age {
    my $u = shift;
    croak "Invalid user object" unless LJ::isu($u);

    my $bdate = $u->{bdate};
    return unless length $bdate;

    my ( $year, $mon, $day ) = $bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
    my $age = LJ::calc_age( $year, $mon, $day );
    return $age if defined $age && $age > 0;
    return;
}

# This will format the birthdate based on the user prop
sub bday_string {
    my $u = shift;
    croak "invalid user object passed" unless LJ::isu($u);

    my $bdate = $u->{'bdate'};
    my ( $year, $mon, $day ) = split( /-/, $bdate );
    my $bday_string = '';

    if ( $u->can_show_full_bday && $day > 0 && $mon > 0 && $year > 0 ) {
        $bday_string = $bdate;
    }
    elsif ( $u->can_show_bday && $day > 0 && $mon > 0 ) {
        $bday_string = "$mon-$day";
    }
    elsif ( $u->can_show_bday_year && $year > 0 ) {
        $bday_string = $year;
    }
    $bday_string =~ s/^0000-//;
    return $bday_string;
}

# Returns the best guess age of the user, which is init_age if it exists, otherwise age
sub best_guess_age {
    my $u = shift;
    return 0 unless $u->is_person || $u->is_identity;
    return $u->init_age || $u->age;
}

# returns if this user can join an adult community or not
# adultref will hold the value of the community's adult content flag
sub can_join_adult_comm {
    my ( $u, %opts ) = @_;

    return 1 unless LJ::is_enabled('adult_content');

    my $adultref = $opts{adultref};
    my $comm     = $opts{comm} or croak "No community passed";

    my $adult_content = $comm->adult_content_calculated;
    $$adultref = $adult_content;

    return 0 if $adult_content eq "explicit" && ( $u->is_minor || !$u->best_guess_age );

    return 1;
}

# Birthday logic -- should a notification be sent?
# Currently the same logic as can_show_bday with an exception for
# journals that have memorial or deleted status.
sub can_notify_bday {
    my ( $u, %opts ) = @_;
    croak "invalid user object passed" unless LJ::isu($u);

    return 0 if $u->is_memorial;
    return 0 if $u->is_deleted;

    return $u->can_show_bday(%opts);
}

# Birthday logic -- can any of the birthday info be shown
# This will return true if any birthday info can be shown
sub can_share_bday {
    my ( $u, %opts ) = @_;
    croak "invalid user object passed" unless LJ::isu($u);

    my $with_u = $opts{with} || LJ::get_remote();

    return 0 if $u->opt_sharebday eq 'N';
    return 0 if $u->opt_sharebday eq 'R' && !$with_u;
    return 0 if $u->opt_sharebday eq 'F' && !$u->trusts($with_u);
    return 1;
}

# Birthday logic -- show appropriate string based on opt_showbday
# This will return true if the actual birthday can be shown
sub can_show_bday {
    my ( $u, %opts ) = @_;
    croak "invalid user object passed" unless LJ::isu($u);

    my $to_u = $opts{to} || LJ::get_remote();

    return 0 unless $u->can_share_bday( with => $to_u );
    return 0 unless $u->opt_showbday eq 'D' || $u->opt_showbday eq 'F';
    return 1;
}

# This will return true if the actual birth year can be shown
sub can_show_bday_year {
    my ( $u, %opts ) = @_;
    croak "invalid user object passed" unless LJ::isu($u);

    my $to_u = $opts{to} || LJ::get_remote();

    return 0 unless $u->can_share_bday( with => $to_u );
    return 0 unless $u->opt_showbday eq 'Y' || $u->opt_showbday eq 'F';
    return 1;
}

# This will return true if month, day, and year can be shown
sub can_show_full_bday {
    my ( $u, %opts ) = @_;
    croak "invalid user object passed" unless LJ::isu($u);

    my $to_u = $opts{to} || LJ::get_remote();

    return 0 unless $u->can_share_bday( with => $to_u );
    return 0 unless $u->opt_showbday eq 'F';
    return 1;
}

sub include_in_age_search {
    my $u = shift;

    # if they don't display the year
    return 0 if $u->opt_showbday =~ /^[DN]$/;

    # if it's not visible to registered users
    return 0 if $u->opt_sharebday =~ /^[NF]$/;

    return 1;
}

# This returns the users age based on the init_bdate (users coppa validation birthdate)
sub init_age {
    my $u = shift;
    croak "Invalid user object" unless LJ::isu($u);

    my $init_bdate = $u->prop('init_bdate');
    return unless $init_bdate;

    my ( $year, $mon, $day ) = $init_bdate =~ m/^(\d\d\d\d)-(\d\d)-(\d\d)/;
    my $age = LJ::calc_age( $year, $mon, $day );
    return $age if $age > 0;
    return;
}

# return true if we know user is a minor (< 18)
sub is_minor {
    my $self = shift;
    my $age  = $self->best_guess_age;
    return 0 unless $age;
    return 1 if ( $age < 18 );
    return 0;
}

sub next_birthday {
    my $u = shift;
    return if $u->is_expunged;

    return $u->selectrow_array( "SELECT nextbirthday FROM birthdays " . "WHERE userid = ?",
        undef, $u->id ) + 0;
}

# class method, loads next birthdays for a bunch of users
sub next_birthdays {
    my $class = shift;

    # load the users we need, so we can get their clusters
    my $clusters = LJ::User->split_by_cluster(@_);

    my %bdays = ();
    foreach my $cid ( keys %$clusters ) {
        next unless $cid;

        my @users = @{ $clusters->{$cid} || [] };
        my $dbcr  = LJ::get_cluster_def_reader($cid)
            or die "Unable to load reader for cluster: $cid";

        my $bind = join( ",", map { "?" } @users );
        my $sth  = $dbcr->prepare("SELECT * FROM birthdays WHERE userid IN ($bind)");
        $sth->execute(@users);
        while ( my $row = $sth->fetchrow_hashref ) {
            $bdays{ $row->{userid} } = $row->{nextbirthday};
        }
    }

    return \%bdays;
}

# opt_showbday options
# F - Full Display of Birthday
# D - Only Show Month/Day       DEFAULT
# Y - Only Show Year
# N - Do not display
sub opt_showbday {
    my $u = shift;

    # option not set = "yes", set to N = "no"
    $u->_lazy_migrate_infoshow;

    # migrate above did nothing
    # -- if user was already migrated in the past, we'll
    #    fall through and show their prop value
    # -- if user not migrated yet, we'll synthesize a prop
    #    value from infoshow without writing it
    unless ( LJ::is_enabled('infoshow_migrate') || $u->{allow_infoshow} eq ' ' ) {
        return $u->{allow_infoshow} eq 'Y' ? undef : 'N';
    }
    if ( $u->raw_prop('opt_showbday') =~ /^(D|F|N|Y)$/ ) {
        return $u->raw_prop('opt_showbday');
    }
    else {
        return 'D';
    }
}

# opt_sharebday options
# A - All people
# R - Registered Users
# F - Trusted Only
# N - Nobody
sub opt_sharebday {
    my $u = shift;

    if ( $u->raw_prop('opt_sharebday') =~ /^(A|F|N|R)$/ ) {
        return $u->raw_prop('opt_sharebday');
    }
    else {
        return 'F' if $u->is_minor;
        return 'A';
    }
}

# this sets the unix time of their next birthday for notifications
sub set_next_birthday {
    my $u = shift;
    return if $u->is_expunged;

    my ( $year, $mon, $day ) = split( /-/, $u->{bdate} );
    unless ( $mon > 0 && $day > 0 ) {
        $u->do( "DELETE FROM birthdays WHERE userid = ?", undef, $u->id );
        return;
    }

    my $as_unix = sub {
        return LJ::mysqldate_to_time( sprintf( "%04d-%02d-%02d", @_ ) );
    };

    my $curyear = ( gmtime(time) )[5] + 1900;

    # Calculate the time of their next birthday.

    # Assumption is that birthday-notify jobs won't be backed up.
    # therefore, if a user's birthday is 1 day from now, but
    # we process notifications for 2 days in advance, their next
    # birthday is really a year from tomorrow.

    # We need to do calculate three possible "next birthdays":
    # Current Year + 0: For the case where we it for the first
    #   time, which could happen later this year.
    # Current Year + 1: For the case where we're setting their next
    #   birthday on (approximately) their birthday. Gotta set it for
    #   next year. This works in all cases but...
    # Current Year + 2: For the case where we're processing notifs
    #   for next year already (eg, 2 days in advance, and we do
    #   1/1 birthdays on 12/30). Year + 1 gives us the date two days
    #   from now! So, add another year on top of that.

    # We take whichever one is earliest, yet still later than the
    # window of dates where we're processing notifications.

    my $bday;
    for my $inc ( 0 .. 2 ) {
        $bday = $as_unix->( $curyear + $inc, $mon, $day );
        last if $bday > time() + $LJ::BIRTHDAY_NOTIFS_ADVANCE;
    }

    # up to twelve hours drift so we don't get waves
    $bday += int( rand( 12 * 3600 ) );

    $u->do( "REPLACE INTO birthdays VALUES (?, ?)", undef, $u->id, $bday );
    die $u->errstr if $u->err;

    return $bday;
}

sub should_fire_birthday_notif {
    my $u = shift;

    return 0 unless $u->is_person;
    return 0 unless $u->is_visible;

    # if the month/day can't be shown
    return 0 if $u->opt_showbday =~ /^[YN]$/;

    # if the birthday isn't shown to anyone
    return 0 if $u->opt_sharebday eq "N";

    # note: this isn't intended to capture all cases where birthday
    # info is restricted. we want to pare out as much as possible;
    # individual "can user X see this birthday" is handled in
    # LJ::Event::Birthday->matches_filter

    return 1;
}

# data for generating packed directory records
sub usersearch_age_with_expire {
    my $u = shift;
    croak "Invalid user object" unless LJ::isu($u);

    # don't include their age in directory searches
    # if it's not publicly visible in their profile
    my $age = $u->include_in_age_search ? $u->age : 0;
    $age += 0;

    # no need to expire due to age if we don't have a birthday
    my $expire = $u->next_birthday || undef;

    return ( $age, $expire );
}

########################################################################
### 12. Adult Content Functions

=head2 Adult Content Functions
=cut

# defined by the user
# returns 'none', 'concepts' or 'explicit'
sub adult_content {
    my $u = shift;

    my $prop_value = $u->prop('adult_content');

    return $prop_value ? $prop_value : "none";
}

# uses user-defined prop to figure out the adult content level
sub adult_content_calculated {
    my $u = shift;

    return $u->adult_content;
}

# returns who marked the entry as the 'adult_content_calculated' adult content level
sub adult_content_marker {
    my $u = shift;

    return "journal";
}

# defuned by the user
sub adult_content_reason {
    my $u = shift;

    return $u->prop('adult_content_reason');
}

sub hide_adult_content {
    my $u = shift;

    my $prop_value = $u->prop('hide_adult_content');

    if ( !$u->best_guess_age ) {
        return "concepts";
    }

    if ( $u->is_minor && $prop_value ne "concepts" ) {
        return "explicit";
    }

    return $prop_value ? $prop_value : "none";
}

# returns a number that represents the user's chosen search filtering level
# 0 = no filtering
# 1-10 = moderate filtering
# >10 = strict filtering
sub safe_search {
    my $u = shift;

    my $prop_value = $u->prop('safe_search');

    # current user 18+ default is 0
    # current user <18 default is 10
    # new user default (prop value is "nu_default") is 10
    return 0 if $prop_value eq "none";
    return $prop_value if $prop_value && $prop_value =~ /^\d+$/;
    return 0 if $prop_value ne "nu_default" && $u->best_guess_age && !$u->is_minor;
    return 10;
}

# determine if the user in "for_u" should see $u in a search result
sub should_show_in_search_results {
    my ( $u, %opts ) = @_;

    # check basic user attributes first
    return 0 unless $u->is_visible;
    return 0 if $u->is_person && $u->age && $u->age < 14;

    # now check adult content / safe search
    return 1 unless LJ::is_enabled('adult_content') && LJ::is_enabled('safe_search');

    my $adult_content = $u->adult_content_calculated;
    my $for_u         = $opts{for};

    # only show accounts with no adult content to logged out users
    return $adult_content eq "none" ? 1 : 0
        unless LJ::isu($for_u);

    my $safe_search = $for_u->safe_search;
    return 1 if $safe_search == 0;    # user wants to see everyone

    # calculate the safe_search level for this account
    my $adult_content_flag = $LJ::CONTENT_FLAGS{$adult_content};
    my $adult_content_flag_level =
          $adult_content_flag
        ? $adult_content_flag->{safe_search_level}
        : 0;

    # if the level is set, see if it exceeds the desired safe_search level
    return 1 unless $adult_content_flag_level;
    return ( $safe_search < $adult_content_flag_level ) ? 1 : 0;
}

1;
