The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# -*- perl -*-

# Load and save capabilities

package GOTM;

use strict;
use warnings;
use Exporter;
use vars qw(@ISA @EXPORT);

@ISA = qw(Exporter);
@EXPORT = qw($RESULT);

my %index = ();

sub new
{
	my $class = shift;
	my $obj = {};
	%$obj = @_;
	$index{$obj->{id}} = $obj;
	bless $obj, $class;
	$obj;
}

sub find
{
	shift if @_ > 1;
	my $id = shift;

	$index{id};
}

sub id { shift->{id}; }

sub load
{
	my $class = shift;
	my $file = shift;
	my $obj = {};

	while (my $tag = <$file>) {
	    chomp $tag;
	    last if ($tag eq "ZOT");
	    my $val = <$file>;
	    chomp $val;
	    $obj->{$tag} = $val;
	}
	bless $obj, $class;
}

sub save
{
	my $obj = shift;
	my $file = shift;

	foreach my $tag (keys %$obj) {
	    print $file "$tag\n$obj->{$tag}\n";
	}
	print $file "ZOT\n";
	1;
}

1;

package GOTMSub;

use strict;
use warnings;
use Exporter;
use Games::Object;
use vars qw(@EXPORT_OK @ISA);

@ISA = qw(Games::Object Exporter);
@EXPORT_OK = qw(@RESULTS);

use vars qw(@RESULTS);

sub initialize { @RESULTS = (); }

sub new
{
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my $obj = Games::Object->new(@_);

	bless $obj, $class;
	$obj;
}

# Test method just to make sure it REALLY got re-blessed properly ...

sub answer { 42; }

# Action callbacks, to prove that these can be reloaded.

sub action_changed1
{
	my ($self, $old, $new) = @_;
	push @RESULTS, [ $self->id(), 1, $old, $new ];
	1;
}

sub action_changed2
{
	my ($self, $change) = @_;
	push @RESULTS, [ $self->id(), 2, $change ];
	1;
}

sub action_maxed
{
	my ($self, $excess) = @_;
	push @RESULTS, [ $self->id(), 'max', $excess ];
	1;
}

1;

package main;

use strict;
use warnings;
use Test;
use Games::Object qw(:attrflags);
use Games::Object::Manager;
use IO::File;

BEGIN { $| = 1; plan tests => 32 }

# Create an object from the test module for later use.
my $testobj = GOTM->new(
    id	=> "ackthhbt",
    foo	=> 'blub',
    bar	=> 'blork',
    zog	=> 'yes, no',
);

# Create an object with some attributes.
my $filename = "./testobj.save";
my $obj1 = GOTMSub->new(-id => "SaveObject");
$obj1->new_attr(
    -name	=> "TheAnswer",
    -type	=> "int",
    -value	=> 42,
);
$obj1->new_attr(
    -name	=> "TheQuestion",
    -type	=> "string",
    -value	=> "Unknown, computation did not complete.",
);
$obj1->new_attr(
    -name	=> "HarrysHouse",
    -type	=> 'string',
    -values	=> [qw(Gryffindor Ravenclaw Hufflepuff Slytherin)],
    -value	=> 'Gryffindor',
);
$obj1->new_attr(
    -name	=> "EnterpriseCommander",
    -type	=> 'string',
    -values	=> [qw(Archer Kirk Picard)],
    -map	=> {
	Archer	=> "First starship named Enterprise",
	Kirk	=> "Constitution class vessel",
	Picard	=> "Galaxy class vessel",
    },
    -value	=> 'Kirk',
);
$obj1->new_attr(
    -name	=> "PercentDone",
    -type	=> 'number',
    -value	=> 0,
    -real_value	=> 100,
    -tend_to_rate => 0.5,
);
$obj1->new_attr(
    -name	=> "ComplexData",
    -type	=> 'any',
    -value	=> {
	foo	=> 'bar',
	baz	=> [ 'fud', 'bop' ],
	blork	=> {
	    this	=> 'that',
	    here	=> 'there',
	}
    },
);
$obj1->new_attr(
    -name	=> "ActionData",
    -type	=> 'int',
    -value	=> 50,
    -minimum	=> 0,
    -maximum	=> 100,
    -on_change	=> [
	[ 'O:self', 'action_changed1', 'A:old', 'A:new' ],
	[ 'O:self', 'action_changed2', 'A:change' ],
    ],
    -on_maximum	=> [ 'O:self', 'action_maxed', 'A:excess' ],
);
$obj1->new_attr(
    -name	=> "DisappearingData",
    -flags	=> ATTR_DONTSAVE,
    -type	=> "string",
    -value	=> "How not to be seen",
);
$obj1->new_attr(
    -name	=> "MagicalData",
    -flags	=> ATTR_AUTOCREATE | ATTR_DONTSAVE,
    -type	=> "string",
    -value	=> "Supercalifragilisticexpialadocious",
);

# Add an object reference.
eval('$obj1->new_attr(
    -name	=> "WeirdObject",
    -type	=> "object",
    -value	=> $testobj,
)');
ok( $@ eq '' );
print "# \$@ = $@" if ($@ ne '');

# Trigger the action callbacks on ActionData just to make sure they work.
GOTMSub->initialize();
$obj1->mod_attr(-name => "ActionData", -modify => 10);
ok( @GOTMSub::RESULTS == 2
 && $GOTMSub::RESULTS[0][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[0][1] == 1
 && $GOTMSub::RESULTS[0][2] == 50
 && $GOTMSub::RESULTS[0][3] == 60
 && $GOTMSub::RESULTS[1][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[1][1] == 2
 && $GOTMSub::RESULTS[1][2] == 10 );

# Save it to a file.
my $file1 = IO::File->new();
$file1->open(">$filename") or die "Cannot open file $filename\n";
eval('$obj1->save(-file => $file1)');
ok( $@ eq '' );
print "# \$@ = $@" if ($@ ne '');
$file1->close();
my $size = -s $filename;
#print "# $filename is $size bytes\n";
ok( $size != 0 );

# Now reopen this file and try to create a new object from it.
my $file2 = IO::File->new();
$file2->open("<$filename") or die "Cannot open file $filename\n";
my $obj2;
eval('$obj2 = Games::Object->load(-file => $file2)');
ok( defined($obj2) && $obj2->id() eq 'SaveObject');
print "# \$@ = $@" if (!defined($obj2));
$file2->close();

# Check that the attributes are the same. The pure DONTSAVE attribute should
# NOT be there, while the DONTSAVE + AUTOCREATE should be there but empty.
ok( $obj2->attr('TheAnswer') == 42 );
ok( $obj2->attr('TheQuestion') eq "Unknown, computation did not complete." );
ok( $obj2->attr('HarrysHouse') eq 'Gryffindor' );
ok( $obj2->attr('EnterpriseCommander') eq 'Constitution class vessel' );
ok( $obj2->raw_attr('EnterpriseCommander') eq 'Kirk' );
ok( $obj2->attr('PercentDone') == 0 );
my $data = $obj2->attr('ComplexData');
ok( $data->{foo} eq 'bar'
 && $data->{baz}[1] eq 'bop'
 && $data->{blork}{this} eq 'that' );
ok( $obj2->attr('ActionData') == 60 );
ok( !$obj2->attr_exists('DisappearingData') );
ok( $obj2->attr_exists('MagicalData') && $obj2->attr('MagicalData') eq '' );

# Check that the object reference was loaded and contains the right data.
# We cheat a little here in the interests of testing: we compare stringified
# references (to insure that a new object was indeed created and this is not
# just the old reference) and to check the values of the object's keys.
my $testobj2 = $obj2->attr('WeirdObject');
ok( "$testobj2" ne "$testobj" && ref($testobj2) eq 'GOTM' );
ok( $testobj2->{id} eq "ackthhbt"
 && $testobj2->{foo} eq 'blub'
 && $testobj2->{bar} eq 'blork'
 && $testobj2->{zog} eq 'yes, no' );

# Call process() on the second object. Make sure it updated but the new one
# did not, which should prove that they're distinct objects.
$obj2->process();
ok( $obj1->attr('PercentDone') == 0 );
ok( $obj2->attr('PercentDone') == 0.5 );

# Tweak the action callback as well, make sure it executes
GOTMSub->initialize();
$obj2->mod_attr(-name => "ActionData", -modify => 5);
ok( @GOTMSub::RESULTS == 2
 && $GOTMSub::RESULTS[0][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[0][1] == 1
 && $GOTMSub::RESULTS[0][2] == 60
 && $GOTMSub::RESULTS[0][3] == 65
 && $GOTMSub::RESULTS[1][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[1][1] == 2
 && $GOTMSub::RESULTS[1][2] == 5 );

# Now attempt to load that file by its filename rather than opening the file
# ourselves. We turn on the attribute accessor method feature to make sure
# that.
my $obj3;
eval('$obj3 = Games::Object->load(-filename =>$filename)');
ok( defined($obj3) && $obj3->id() eq 'SaveObject' );
ok( $obj3->attr('TheAnswer') == 42 );
ok( $obj3->attr('TheQuestion') eq "Unknown, computation did not complete." );
ok( $obj3->attr('HarrysHouse') eq 'Gryffindor' );
ok( $obj3->attr('EnterpriseCommander') eq 'Constitution class vessel' );
ok( $obj3->raw_attr('EnterpriseCommander') eq 'Kirk' );
ok( $obj3->attr('PercentDone') == 0 );
ok( $obj3->attr('ActionData') == 60 );
my $testobj3 = $obj3->attr('WeirdObject');
ok( "$testobj3" ne "$testobj" && ref($testobj3) eq 'GOTM' );
ok( $testobj3->{id} eq "ackthhbt"
 && $testobj3->{foo} eq 'blub'
 && $testobj3->{bar} eq 'blork'
 && $testobj3->{zog} eq 'yes, no' );

# Tweak the action callback as well, make sure it executes
GOTMSub->initialize();
$obj3->mod_attr(-name => "ActionData", -modify => 5);
ok( @GOTMSub::RESULTS == 2
 && $GOTMSub::RESULTS[0][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[0][1] == 1
 && $GOTMSub::RESULTS[0][2] == 60
 && $GOTMSub::RESULTS[0][3] == 65
 && $GOTMSub::RESULTS[1][0] eq 'SaveObject'
 && $GOTMSub::RESULTS[1][1] == 2
 && $GOTMSub::RESULTS[1][2] == 5 );

# Finally, we need to test the ability to load multiple objects from the
# same file. Note that we're testing exclusively the individual object load/save
# functionality rather than manager functionality, which is covered in another
# test. First produce a file containing several objects in it.
unlink $filename;
$filename = "./testobjs.save";
my $file3 = IO::File->new();
$file3->open(">$filename") or die "Cannot open file $filename\n";
my $count = 0;
my @pspecs = (
    [ 'Mercury', 'Mercurial Mugwumps', 1.3 ],
    [ 'Venus', 'Venusian Voles', 2.9 ],
    [ 'Earth', 'Hectic Humans', 1.4 ],
    [ 'Mars', 'Martian Mammals', 12.7 ],
    [ 'Jupiter', 'Jovian Jehosephats', 5.9 ],
    [ 'Saturn', 'Saturine Satyrs', 0.6 ],
    [ 'Uranus', 'Uranian Ugnaughts', 0.9 ],
    [ 'Neptune', 'Neptunian Nymphs', 1.5 ],
    [ 'Pluto', 'Plutonian Plutocrats', 0.00005 ],
);
foreach my $spec (@pspecs) {
	$count++;
	my $obj = Games::Object->new(-id => 'Planet' . $count);
	$obj->new_attr(
	    -name	=> 'Name',
	    -type	=> 'string',
	    -value	=> $spec->[0],
	);
	$obj->new_attr(
	    -name	=> "Lifeform",
	    -type	=> 'string',
	    -value	=> $spec->[1],
	);
	$obj->new_attr(
	    -name	=> "GalacticCreditExchangeRate",
	    -type	=> 'number',
	    -value	=> $spec->[2],
	);
	$obj->save(-file => $file3);
}
$file3->close();
$size = -s $filename;
#print "# $filename is $size bytes\n";

# Now reopen the file and attempt to read them back in, validating as we go.
my $file4 = IO::File->new();
$file4->open("<$filename") or die "Cannot open file $filename\n";
while ($count) {
    my $spec = shift @pspecs;
    my $obj;
    my $pnum = 10 - $count;
    eval('$obj = Games::Object->load(-file =>$file4, -id => "NewPlanet" . $pnum)');
    if ($@) {
	print "# Load of $pnum failed\n";
	last;
    }
    if ($obj->attr('Name') ne $spec->[0]) {
	print "# attr Name is bad in $pnum\n";
	last;
    }
    if ($obj->attr('Lifeform') ne $spec->[1]) {
	print "# attr Lifeform is bad in $pnum\n";
	last;
    }
    if ($obj->attr('GalacticCreditExchangeRate') != $spec->[2]) {
	print "# attr GalacticCreditExchangeRate is bad in $pnum\n";
	last;
    }
    $count --;
}
$file4->close();
ok( $count == 0 );
unlink $filename;

exit (0);