#!/usr/bin/perl
#

use strict;
use lib "$ENV{'LJHOME'}/cgi-bin";
require 'ljlib.pl';

package LJ::SixDegrees;

sub find_path
{
    my ($fu, $tu, $timeout) = @_;
    return () unless $fu && $tu;
    return () unless $fu->{journaltype} eq "P" && $tu->{journaltype} eq "P";

    $LJ::SixDegrees::MEMC_EXPIRE ||= 86400;

    my $cache = {};  # keys for links in/out -> listrefs, userids -> $u's, "notes" -> why pass/fail
    $cache->{$fu->{userid}} = $fu;
    $cache->{$tu->{userid}} = $tu;

    my $memkey = [ $fu->{'userid'}, "6dpath:$fu->{userid}:$tu->{userid}" ];
    my $exp = 3600;
    my $path = LJ::MemCache::get($memkey);
    unless ($path) {
	$path = _find_path_helper($fu, $tu, $timeout, $cache);
	LJ::MemCache::set($memkey, $path, $exp) if $path;
    }

    return () unless $path;
    return map { $cache->{$_} || LJ::load_userid($_) } @$path;
}

# returns arrayref of userids in path on success (even if empty), or undef on timeout
sub _find_path_helper
{
    my ($fu, $tu, $timeout, $cache) = @_;

    my $time_start = time();

    # user is themselves (one element in path)
    return [$fu->{userid}] if $fu->{'userid'} == $tu->{'userid'};

    # from user befriends to user (two elements in path
    my $fu_friends = links_out($fu, $cache);
    if (intersect($fu_friends, [ $tu->{'userid'} ])) {
	$cache->{'note'} = "2 way path";
	return [$fu->{userid}, $tu->{userid}];
    }

    # try to find a three-way path (fu has a friend who lists tu as a friend)
    my $tu_friendofs = links_in($tu, $cache);
    if (my $via = intersect($fu_friends, $tu_friendofs)) {
	$cache->{'note'} = "3 way path";
	return [$fu->{userid}, $via, $tu->{userid}];
    }

    # try to find four-way path by expanding fu's friends' friends,
    # one at a time, looking for intersections.  along the way,
    # keep track of all friendsfriends, then we can walk along
    # tu's friendofs-friendofs looking for intersections there later
    # if necessary.
    my %friendsfriends = ();  # uid -> 1
    my %friends = ();         # uid -> 1
    my $tried = 0;
    foreach my $fid (@$fu_friends) {
	$friends{$fid} = 1;
	next if ++$tried > 100;
	if (time() > $time_start + $timeout) {
	    $cache->{'note'} = "timeout";
	    return undef;
	}

	# a group of one friend's ($fid's) friends
	my $ffset = links_out($fid, $cache);

	# see if $fid's friends intersect $tu's friendofs
	if (intersect($ffset, [ $tu->{userid} ])) {
	    $cache->{'note'} = "returning via fid's friends to tu";
	    return [$fu->{userid}, $fid, $tu->{userid}];
	}

	# see if $fid's friends intersect $tu's friendofs
	if (my $via = intersect($ffset, $tu_friendofs)) {
	    $cache->{'note'} = "returning via fid's friends to tu's friendofs";
	    return [$fu->{userid}, $fid, $via, $tu->{userid}];
	}
	
	# otherwise, track who's a friends-of-friend, and the friend we're on
	# so we don't try doing the same search later
	foreach (@$ffset) {
	    $friendsfriends{$_} ||= $fid;
	}
    }

    # try to find a path by looking at tu's friendof-friendofs
    $tried = 0;
    foreach my $foid (@$tu_friendofs) {
	last if ++$tried > 100;
	if (time() > $time_start + $timeout) {
	    $cache->{'note'} = "timeout";
	    return undef;
	}

	if (my $fid = $friendsfriends{$foid}) {
	    $cache->{'note'} = "returning via friend-of-friend is friend of target";
	    return [$fu->{userid}, $fid, $foid, $tu->{userid}];
	}

	my $foset = links_in($foid, $cache);
	
 	# see if we can go from $tu to $foid's friends.  (now, this shouldn't normally
	# happen, but we limit the links_in/out to 1000, so there's a possibility
	# we stopped during the friend-of-friend search above)
	if (intersect([ $fu->{userid} ], $foset)) {
	    $cache->{'note'} = "returning via friend-of-friend but discovered backwards";
	    return [$fu->{userid}, $foid, $tu->{userid}];
	}

	# otherwise, see if any of this group of friendof-friendofs are a friend-friend
	foreach my $uid (@$foset) {
	    if (my $fid = $friends{$uid}) {
		$cache->{'note'} = "returning via friend intersection with friendof-friendof";
		return [$fu->{userid}, $fid, $foid, $tu->{userid}];
	    }
	    if (my $fid = $friendsfriends{$uid}) {
		$cache->{'note'} = "returning via friend-of-friend intersection with friendof-friendof";
		return [$fu->{userid}, $fid, $uid, $foid, $tu->{userid}];
	    }
	}
    }

    return [];  # no path, but not a timeout (as opposed to undef above)
}

sub intersect
{
    my ($list_a, $list_b) = @_;
    return 0 unless ref $list_a && ref $list_b;
    my %temp;
    $temp{$_} = 1 foreach @$list_a;
    foreach (@$list_b) {
	return $_ if $temp{$_};
    }
    return 0;
}

sub link_fetch
{
    my ($uid, $key, $sql, $cache) = @_;

    # first try from the pre-load/already-done per-process cache
    return $cache->{$key} if defined $cache->{$key};
    
    # then try memcache
    my $memkey = [$uid, $key];
    my $listref = LJ::MemCache::get($memkey);
    if (ref $listref eq "ARRAY") {
	$cache->{$key} = $listref;
	return $listref;
    }

    # finally fall back to the database.
    my $dbr = LJ::get_db_reader();
    $listref = $dbr->selectcol_arrayref($sql, undef, $uid) || [];

    # get the $u's for everybody (bleh, since we need to know if they're a community
    # or not)
    my @need_load;   # userids necessary to load
    foreach my $uid (@$listref) {
	push @need_load, $uid unless $cache->{$uid};
    }
    if (@need_load) {
	LJ::load_userids_multiple([ map { $_, \$cache->{$_} } @need_load ]);
    }

    # filter out communities/deleted/suspended/etc
    my @clean_list;  # visible users, not communities
    foreach my $uid (@$listref) {
	my $u = $cache->{$uid};
	next unless $u && $u->{'statusvis'} eq "V" && $u->{'journaltype'} eq "P";
	push @clean_list, $uid;
    }

    $listref = \@clean_list;
    LJ::MemCache::set($memkey, $listref, $LJ::SixDegrees::MEMC_EXPIRE);
    $cache->{$key} = $listref;
    return $listref;
}

sub links_out
{
    my $uid = LJ::want_userid($_[0]);
    return link_fetch($uid, "6dlo:$uid",
		      "SELECT friendid FROM friends WHERE userid=? LIMIT 1000",
		      $_[1]);
}

sub links_in
{
    my $uid = LJ::want_userid($_[0]);
    return link_fetch($uid, "6dli:$uid",
		      "SELECT userid FROM friends WHERE friendid=? LIMIT 1000",
		      $_[1]);
}

1;
