package Games::Object::Manager;

use strict;
use Exporter;

use Carp qw(carp croak confess);
use IO::File;
use Games::Object::Common qw(FetchParams LoadData SaveData ANAME_MANAGER);

use vars qw($VERSION @EXPORT_OK %EXPORT_TAGS @ISA);

$VERSION = "0.10";
@ISA = qw(Exporter);
@EXPORT_OK = qw($CompareFunction REL_NO_CIRCLE);
%EXPORT_TAGS = (
    flags	=> [ qw(REL_NO_CIRCLE) ],
    variables	=> [ qw($CompareFunction) ],
);

use vars qw($CompareFunction);

# Define flags.
use constant REL_NO_CIRCLE	=> 0x00000001; # Don't allow cir. relates

# Define the comparison function to use for processing order.
$CompareFunction = '_CompareDefault';

# Define the default process info.
my @ProcessList = (
    'process_queue',
    'process_pmod',
    'process_tend_to',
);
my $ProcessLimit = 100;

####
## INTERNAL FUNCTIONS

# Default comparison function when determining the order of processing of
# two objects.

sub _CompareDefault { $b->priority() <=> $a->priority() }

# Comparison function when using the creation order option

sub _CompareAddOrder {
    my $cmp = $b->priority() <=> $a->priority();
    $cmp == 0 ? $a->order() <=> $b->order() : $cmp;
}

# Create a relation methods

sub _CreateRelators
{
	my %args = @_;
	my $name = $args{relate_method};
	my $uname = $args{unrelate_method};
	my $rname = $args{related_method};
	my $iname = $args{is_related_method};
	my $lname = $args{related_list_method};

	no strict 'refs';
	*$name = sub {
	    my $man = shift;
	    my $args = ( ref($_[$#_]) eq 'HASH' ? pop @_ : {} );
	    $man->relate(how => $name,
			 self => $_[0],
			 object => $_[1],
			 other => $_[2],
			 args => $args);
	} if (!defined(&$name));
	*$uname = sub {
	    my $man = shift;
	    my $args = ( ref($_[$#_]) eq 'HASH' ? pop @_ : {} );
	    $man->unrelate(how => $name,
			   object => $_[0],
			   other => $_[1],
			   args => $args);
	} if (!defined(&$uname));
	*$rname = sub {
	    my $man = shift;
	    $man->related(how => $name, object => $_[0]);
	} if (!defined(&$rname));
	*$iname = sub {
	    my $man = shift;
	    $man->is_related(how => $name, self => $_[0], object => $_[1]);
	} if (!defined(&$iname));
	*$lname = sub {
	    my $man = shift;
	    $man->related_list(how => $name, self => $_[0]);
	} if (!defined(&$lname));
}

####
## CONSTRUCTOR

# Basic constructor

sub new
{
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my $man = {};
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'opt', 'base_id', 0, 'int' ],
	    [ 'opt', 'process_list', \@ProcessList, 'arrayref' ],
	    [ 'opt', 'process_limit', $ProcessLimit, 'int' ],
	] );
	bless $man, $class;

	# Define storage for created objects. Note that this means that objects
	# will be persistent. They can go out of scope and still exist, since
	# each is identified by a unique ID.
	$man->{index} = {};

	# Define tables that handle object relationships
	$man->{relation_def} = {};
	$man->{relate_to} = {};
	$man->{relate_from} = {};

	# Define a counter for creating objects when the user wants us to
	# assume that every new object is unique. The starting number can be
	# changed with base_id() but only if no objects have been created yet.
	$man->{next} = $args{base_id};

	# Define a counter that will be used to track the order in which objects
	# are created. This is to support a new feature in v0.05
	$man->{order} = 0;

	# And if we are doing this, we want to try and use space efficiently by
	# reclaiming unused IDs. Thus we track the lowest available opening.
	# [ NOT YET IMPLEMENTED ]
	$man->{reclaim} = 1;
	$man->{avail} = 0;

	# Track the highest priority object.
	$man->{highest_pri} = 0;

	# Define a table that shows what order process() is supposed to do
	# things.
	$man->{process_list} = $args{process_list};

	# Define a limit to how many times the same item can be processed in a
	# queue (see process_queue() for details)
	$man->{process_limit} = $args{process_limit};

	# Set the default inherit_from relationship.
	$man->define_relation(
	    name		=> 'inherit',
	    relate_method	=> 'inherit',
	    unrelate_method	=> 'disinherit',
	    related_method	=> 'inheriting_from',
	    related_list_method	=> 'has_inherting',
	    is_related_method	=> 'is_inheriting_from',
	    flags		=> REL_NO_CIRCLE,
	);

	# Done.
	$man;
}

# Constructor for loading entire container from a file.

sub load
{
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my $file = shift;
	my $filename;

	# If we got a filename instead of a file object, open the file.
	if (!ref($file)) {
	    $filename = $file;
	    $file = IO::File->new();
	    $file->open("<$filename") or
		croak "Unable to open manager file '$filename'";
	}

	# Initialize the object.
	my $man;
	if (ref($proto)) {
	    # This is a "load in place", meaning we're reloading to an
	    # existing object, so clear out the old stuff.
	    $man = $proto;
	    foreach my $key (keys %$man) { delete $man->{$key}; }
	} else {
	    # Totally new object originating from the file.
	    $man = {};
	    bless $man, $class;
	}

	# Check the header to make sure this is manager data.
	my $line = <$file>; chomp $line;
	croak "Did not find manager header data in file"
	  if ($line ne 'OBJ:__MANAGER__');
	$line = <$file>; chomp $line;
	croak "Second line of manager data header bad"
	  if ($line !~ /^CL:(.+)$/);
	my $mclass = $1;

	# Load.
	LoadData($file, $man);
	$file->close() if defined($filename);

	# Restore manager attributes to all objects.
	foreach my $obj (values %{$man->{index}}) { $obj->manager($man); }

	# Restore relators.
	foreach my $rel (values %{$man->{relation_def}}) {
	    _CreateRelators(%$rel);
	}

	# Done.
	bless $man, $mclass;
	$man;
}

####
## MANAGER DATA METHODS

# Save the manager and its contents to a file.

sub save
{
	my $man = shift;
	my $file = shift;
	my $filename;

	# If we got a filename instead of a file object, open the file.
	if (!ref($file)) {
	    $filename = $file;
	    $file = IO::File->new();
	    $file->open(">$filename") or
		croak "Unable to open manager file '$filename'";
	}

	# Save header. This indicates that this is indeed manager object
	# data and preserves the class.
	print $file "OBJ:__MANAGER__\n" .
		    "CL:" . ref($man) . "\n";

	# Save data. See the comments on the save() routine in Games::Object
	# for why we copy the ref to an ordinary hash first.
	my %hash = %$man;
	SaveData($file, \%hash);
	$file->close() if (defined($filename));
	1;
}

# "Find" an object (i.e. look up its ID). If given something that is
# already an object, validates that the object is still valid. If the
# assertion flag is passed, an invalid object will result in a fatal error.

sub find
{
	my ($man, $id, $assert) = @_;

	if (!defined($id)) {
	    if ($assert) {
		confess "Assertion failed: ID is undefined";
	    } else {
		return undef;
	    }
	}
	$id = $id->id() if (ref($id) && UNIVERSAL::isa($id, 'Games::Object'));
	if (defined($man->{index}{$id})) {
	    $man->{index}{$id};
	} elsif ($assert) {
	    # Report with confess() so user can see where the assertion was made
	    confess "Assertion failed: '$id' is not a valid/managed object ID";
	} else {
	    undef;
	}
}

# Return the number of objects in the manager.

sub total_objects {
    my $man = shift;
    scalar keys %{$man->{index}};
}

# Returns the ID of an object, with the side effect that it validates that
# this object is really a Games::Object derivative and is being managed by
# this manager. The user specifies either the ID or the object ref. If valid,
# the ID is always returned (thus it can be used to guarantee the return of
# an ID when you're not sure if you were passed an object or the ID).

sub id
{
	my ($man, $obj, $assert) = @_;

	if (ref($obj) && UNIVERSAL::isa($obj, 'Games::Object')) {
	    my $id = $obj->id();
	    defined($man->{index}{$id}) ? $id : undef;
	} elsif (defined($man->{index}{$obj})) {
	    $obj;
	} elsif ($assert) {
	    # Report with confess() so user can see where the assertion was made
	    confess "Assertion failed: '$obj' is not a valid/managed object";
	} else {
	    undef;
	}
}

####
## OBJECT MANAGEMENT METHODS

# Add a new object to the manager. The user may either specify an ID (which
# must not already exist), or allow it to take a predefined ID from the object
# (if defined), or pick one on its own (if previous two undefined)

sub add
{
	my ($man, $obj, $id) = @_;

	# Pick new ID if needed.
	$id = $obj->id() if (!defined($id));
	$id = $man->{next}++ if (!defined($id));

	# Make sure it does not exist.
	croak "Attempt to add duplicate object ID '$id'"
	    if (defined($man->{index}{$id}));

	# Add it. Do this before adding the manager link so we don't get
	# a call back to us.
	$man->{index}{$id} = $obj;
	$obj->id($id);

	# Add the manager attribute
	$obj->manager($man);

	# Done.
	$id;
}

# Similar to add, but allows an object to already exist under this ID, in
# which case the old on is removed. Returns the same values as add(). The
# ID to replace is always taken from the existing object. The ID parameter
# is applied to the new object (thus it must not already exist).

sub replace
{
	my ($man, $obj, $id) = @_;

	# Get rid of the old object. Don't worry if the object does not
	# already exist.
	$man->remove($id);

	# Add new one.
	$man->add($obj, $id);
}

# Remove an object. Returns the object if the object was found and removed,
# undef if not. The on_removed action is invoked on the object (but before
# the object is actually removed so it can still access the manager linkage).
# User may specify additional args to be passed to the action() call.

sub remove
{
	my $man = shift;
	my $self = shift;

	# If the last arg is a hash, this is additional args to any callback
	# that might get invoked.
	my $aargs = ( @_ && ref($_[$#_]) eq 'HASH' ? pop @_ : {} );

	# Any remaining arg is other.
	my $other = ( @_ ? shift : $self );

	# If object does not exist, no need to go any further.
	my $id = $man->id($self);
	return undef if (!defined($man->{index}{$id}));

	# Fetch the object and invoke action.
	$self = $man->find($id);
	$self->action(other => $other,
		      action => "object:remove",
		      args => $aargs);

	# Break relationships TO this object. These are all done with the
	# force option. This means that no tests will be done for each
	# unrelate(), but post-unrelate() actions WILL occur.
	my @hows = ();
	@hows = keys %{$man->{relate_from}{$id}}
	  if (defined($man->{relate_from}{$id}));
	foreach my $how (@hows) {
	    my @fobjs = @{$man->{relate_from}{$id}{$how}};
	    foreach my $fobj (@fobjs) {
		$man->unrelate(
		    how		=> $how,
		    object	=> $fobj,
		    other	=> $other,
		    force	=> 1,
		    args	=> { source => 'remove:to', %$aargs },
		);
	    }
	}

	# Break all relationships FROM this object to others.
	@hows = ();
	@hows = keys %{$man->{relate_to}{$id}}
	  if (defined($man->{relate_to}{$id}));
	foreach my $how (@hows) {
	    my @objs = map { $man->find($_) } @{$man->{relate_from}{$id}{$how}};
	    foreach my $obj (@objs) {
	        $man->unrelate(
		    how		=> $how,
		    object	=> $obj,
		    other	=> $other,
		    force	=> 1,
		    args	=> { source => 'remove:from', %$aargs }
		);
	    }
	}

	# Delete from internal tables, which should remove all references to
	# it save the one we have.
	delete $man->{index}{$id};
	delete $man->{relate_to}{$id};
	delete $man->{relate_from}{$id};

	# Remove the manager attribute.
	$self->manager(undef);

	# Done.
	$self;
}

# Go down the complete list of objects and perform a method call on each. If
# no args are given, 'process' is assumed. This will call them in order of
# priority.
#
# The caller may choose to filter the list by providing a CODE ref as the
# first argument. Only the objects for which the CODE ref returns true are
# considered (new in v0.10).

sub process
{
	my $man = shift;

	# Note that we grab the actual objects and not the ids in the sort.
	# This is more efficient, as each object is simply a reference (a
	# scalar with a fixed size) as opposed to a string (a scalar with
	# a variable size).
	my $method = shift;
	my $code = ( ref($method) eq 'CODE' ? $method : undef );
	$method = shift if ($code);
	my @args = @_;
	$method = 'process' if (!defined($method));

	# Derive the object list.
	my @objs = (
	    $code ?
		grep { &$code($_, @args) }
		grep { UNIVERSAL::can($_, $method) }
		sort $CompareFunction values %{$man->{index}}
	    :
		grep { UNIVERSAL::can($_, $method) }
		sort $CompareFunction values %{$man->{index}}
	);

	# Process.
	unshift @args, $man->{process_list} if ($method eq 'process');
	foreach my $obj (@objs) {
	    $obj->$method(@args) if (UNIVERSAL::can($obj, $method));
	}

	# Return the number of objects processed.
	scalar(@objs);
}

# Set/fetch the process list for the process() function. Note that the user is
# not limited to the methods found here. The methods can be in the subclass
# if desired. Note that we have no way to validate the method names here,
# so we take it on good faith that they exist.

sub process_list {
    my $man = shift;
    if (@_) { @{$man->{process_list}} = @_ } else { @{$man->{process_list}} }
}

####
## OBJECT RELATIONSHIP METHODS

# Check to see if a relationship is valid. If assertion flag present, this
# will bomb the program if the relationship is not present.

sub has_relation
{
	my ($man, $how, $assert) = @_;

	defined($man->{relation_def}{$how}) ? 1 :
	$assert ? croak "'$how' is an invalid relationship type"
		: 0;
}

# Define a new relationship. This allows objects to be related with the
# relate() method, or via a relator method created.

sub define_relation
{
	my $man = shift;
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'req', 'name', undef, 'string' ],
	    [ 'opt', 'relate_method', undef, 'string' ],
	    [ 'opt', 'unrelate_method', undef, 'string' ],
	    [ 'opt', 'related_method', undef, 'string' ],
	    [ 'opt', 'related_list_method', undef, 'string' ],
	    [ 'opt', 'is_related_method', undef, 'string' ],
	    [ 'opt', 'on_remove', undef, 'callback' ],
	    [ 'opt', 'flags', 0, 'int' ],
	], 1 );

	# Add it. Note that we allow redefinition at will.
	my $rname = delete $args{name};
	$args{relate_method} = $rname
	    if (!$args{relate_method});
	$args{unrelate_method} = "un${rname}"
	    if (!$args{unrelate_method});
	$args{related_method} = "${rname}_to"
	    if (!$args{related_method});
	$args{related_list_method} = "${rname}_list"
	    if (!$args{related_list_method});
	$args{is_related_method} = "is_${rname}"
	    if (!$args{is_related_method});
	$man->{relation_def}{$rname} = \%args;

	# Create relator.
	_CreateRelators(%args);

	# Done.
	1;
}

# Relate two objects.

sub relate
{
	my $man = shift;
	my %args = ();

	# Fetch parameters. Self is the thing being related to, object is
	# the thing being related to it.
	FetchParams(\@_, \%args, [
	    [ 'req', 'how', undef, sub { $man->has_relation(shift); } ],
	    [ 'req', 'self', undef, 'any' ],
	    [ 'req', 'object', undef, 'any' ],
	    [ 'opt', 'other', undef, 'any' ],
	    [ 'opt', 'force', 0, 'boolean' ],
	    [ 'opt', 'args', {}, 'hashref' ],
	] );
	my $how = $args{how};
	my $self = $args{self};
	my $object = $args{object};
	my $other = $args{other};
	my $force = $args{force};
	my $aargs = $args{args};

	# If other is undefined, then we set it equal to self, meaning we assume
	# that the receipient of the object itself instigated the action.
	$other = $self if (!defined($other));

	# Do it. First fetch necesary parameters.
	my $rel = $man->{relation_def}{$how};
	my $doaction = "object:on_" . $rel->{relate_method};
	my $tryaction = "object:try_" . $rel->{relate_method};
	my $idself = $man->id($self); $self = $man->find($idself);
	my $idobject = $man->id($object); $object = $man->find($idobject);

	# Perform check to see if relationship is allowed. We do this
	# before anything else, including attempting to unrelate it from
	# whatever it may be currently related to. This way the relate
	# check code can see how it is related now in case that means
	# anything, plus it prevents orphaned objects (which would happen
	# if we first unrelate()d it and then failed the relate() check).
	my $check =
	    $force
	||
	    $self->action(
		action	=> $tryaction,
		object	=> $object,
		other	=> $other,
		args	=> $aargs);
	return 0 if (!$check);

	# Relation is allowed, so check to see if already related.
	if (defined($man->{relate_to}{$idobject}{$how})) {

	    # Already related in this fashion.
	    if ($man->{relate_to}{$idobject}{$how} eq $idself) {
	        # And to the same object, so do nothing (successfully).
	        return 1;
	    } elsif ($man->unrelate(
	      how	=> $how,
	      object	=> $object,
	      force	=> $force,
	      args	=> { source => 'relate', %$aargs } )) {
	        # The unrelate from the previous object succeeded, so
	        # invoke myself to try again.
	        return $man->relate(@_);
	    } else {
	        # The unrelate failed, so no-go.
	        return 0;
	    }

	}

	# Not currently related to anything in this way. The first
	# thing we do is check the REL_NO_CIRCLE flag. If set,
	# then we make a check to see if a circular reference would
	# result from this. If so, then bomb, as this is assumed to
	# be a logic error in the main program.
	if ($rel->{flags} & REL_NO_CIRCLE) {

	    # Check to make sure no circular relationship would result from
	    # this (i.e. self is already related to object in this manner).
	    croak "Relating $idobject to $idself in manner $how would " .
	          "create a circular relationship"
	      if ($man->is_related(
	        object  => $self,
	        self	=> $object,
	        how     => $how,
	        distant => 1));

	}

	# Do it.
	$man->{relate_to}{$idobject}{$how} = $idself;
	$man->{relate_from}{$idself}{$how} = []
	    if (!defined($man->{relate_from}{$idself}{$how}));
	push @{$man->{relate_from}{$idself}{$how}}, $idobject;

	# Invoke post-relate actions.
	$self->action(
	    object	=> $object,
	    other	=> $other,
	    action	=> $doaction,
	    args	=> $aargs,
	);

	# Done.
	1;

}

# Return the object to which this one is related (if any)

sub related
{
	my $man = shift;
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
	    [ 'req', 'object', undef, 'any' ],
	] );
	my $how = $args{how};
	my $object = $args{object};
	my $id = $man->id($object); $object = $man->find($id);

	defined($man->{relate_to}{$id}) &&    # @*!&$ autovivication
	defined($man->{relate_to}{$id}{$how}) ?
	    $man->find($man->{relate_to}{$id}{$how}) : undef;
}

# Return a list of items that are related to a paricular object in a certain
# way.

sub related_list
{
	my $man = shift;
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
	    [ 'req', 'self', undef, 'any' ],
	] );
	my $how = $args{how};
	my $self = $args{self};

	# Return list of objects.
	my $id = $man->id($self);
	my @list = ();
	@list = map { $man->find($_) } @{$man->{relate_from}{$id}{$how}}
		if (defined($man->{relate_from}{$id})
		 && defined($man->{relate_from}{$id}{$how}));
	@list;
}

# Check to see if two objects are related. By default, this checks only if
# two objects are DIRECTLY related. However, specifying the "distant" flag
# will perform a recursive check to see if the relationship exists indirectly.

sub is_related
{
	my $man = shift;
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'req', 'how', undef, sub { $man->has_relation(shift); } ],
	    [ 'req', 'object', undef, 'any' ],
	    [ 'opt', 'self', undef, 'any' ],
	    [ 'opt', 'distant', 0, 'boolean' ],
	] );
	my $how = $args{how};
	my $idobject = $man->id($args{object});
	my $idself = $man->id($args{self});
	my $distant = $args{distant};
	return 0 if (!defined($idobject) || !defined($idself));

	# If idobject is related to nothing then no relation.
	return 0 if (!defined($man->{relate_to}{$idobject})
		  || !defined($man->{relate_to}{$idobject}{$how}));

	# If there is a direct relationships, success.
	return 1 if ($man->{relate_to}{$idobject}{$how} eq $idself);

	# If user did not want a distant relationship, then fail.
	return 0 if (!$distant);

	# Otherwise, check what idobject is related to and see if that is
	# related to idself.
	$man->is_related(
	    object	=> $man->{relate_to}{$idobject}{$how},
	    self	=> $idself,
	    how		=> $how,
	    distant	=> 1);
}

# Unrelate an object.

sub unrelate
{
	my $man = shift;
	my %args = ();

	# Fetch parameters.
	FetchParams(\@_, \%args, [
	    [ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
	    [ 'req', 'object', undef, 'any' ],
	    [ 'opt', 'other', undef, 'any' ],
	    [ 'opt', 'args', {}, 'hashref' ],
	] );
	my $how = $args{how};
	my $object = $args{object};
	my $other = $args{other};
	my $aargs = $args{args};
	my $rel = $man->{relation_def}{$how};
	my $doaction = "object:on_" . $rel->{unrelate_method};
	my $tryaction = "object:try_" . $rel->{unrelate_method};

	# Set the source if not already defined.
	$aargs->{source} = 'direct' if (!defined($aargs->{source}));

	# Get ID and check if related.
	my $idobject = $man->id($object); $object = $man->find($idobject);
	if (defined($man->{relate_to}{$idobject})
	 && defined($man->{relate_to}{$idobject}{$how})) {
	    # Yes it is, so check that object to see if we can unrelate.
	    my $idself = $man->{relate_to}{$idobject}{$how};
	    my $self = $man->find($idself);
	    $other = $self if (!defined($other));
	    my $check =
		$self->action(
		    object	=> $object,
		    other	=> $other,
		    action	=> $tryaction,
		    args	=> { %$aargs },
		);
	    if ($check) {
		# Check succeeded, so unrelate them.
		delete $man->{relate_to}{$idobject}{$how};
		my @nlist = ();
		foreach my $item (@{$man->{relate_from}{$idself}{$how}}) {
		    push @nlist, $item if ($item ne $idobject);
		}
		@{$man->{relate_from}{$idself}{$how}} = @nlist;
		# Invoke post-unrelate actions.
		$self->action(
		    object	=> $object,
		    other	=> $other,
		    action	=> $doaction,
		    args	=> $aargs,
		);
		1;
	    } else {
		0;
	    }
	} else {
	    # Not related to anything in this manner. Since the end result
	    # is the same as the original condition, we consider this to
	    # be success.
	    1;
	}
}

1;
