package Games::Cards::Undo;

# part of Games::Cards by Amir Karger (See Cards.pm for details)

=pod

=head1 NAME

Games::Cards::Undo -- undoing/redoing moves in Games::Cards games

=head1 SYNOPSIS

    use Games::Cards::Undo;
    $Undo = new Games::Cards::Undo(100); # Make undo engine to save 100 moves
    $Undo->undo; # undo last move
    $Undo->redo; # redo last undone move
    $Undo->end_move; # tell undo engine we're done with a move

=head1 DESCRIPTION

This is the package for methods to undo & redo moves. The GC::Undo object has
no publicly accessible fields.  But it stores an array of the
preceding moves. Note that a "move" is made up of several "atoms" (objects of
the private class GC::Undo::Atom and its subclassess).  For example, moving a
card from one column to another in solitaire involves one or more Splice atoms
(removing or adding card(s) to a CardSet) and possibly a Face atom (turning a
card over).

Many of the GC::Undo methods (and all of the GC::Undo::Atom methods) will be
called by other Games::Cards methods, but not by the actual games. Here are
the publicly accesssible methods:

=over 4

=cut

# TODO write Undo::Sort?


# sub-packages
{
package Games::Cards::Undo;
package Games::Cards::Undo::Atom;
package Games::Cards::Undo::Splice;
package Games::Cards::Undo::Face;
package Games::Cards::Undo::End_Move;
}

# How does Games::Cards handle undo?
#
# Undo_List is just an array of (objects from derived classes of) Undo::Atoms.
# E.g. in solitaire one "move" might include moving cards from one column to
# another (two Undo::Splice objects) and turning a card over (a Undo::Face
# object) The undo list will store those Atoms as well as an End_Move object,
# which is just a placeholder saying that move is over.

# Global private variables
# Can't keep this info in an object, because private GC subroutines
# (like CardSet::splice) need access to the Undo list, and I shouldn't have
# to pass the undo object around to every sub.
# GC::Undo::Undo_List holds all previous moves in GC::Undo::Atom objects
# GC::Undo::Current_Atom is the index of the current Atom in @Undo_List
# GC::Undo::Max_Size is the maximum size (moves, not Atoms!) of the undo list
# GC::Undo::In_Undo says that we're currently doing (or undoing) an Undo, so we
# shouldn't store undo information when we move cards around
my (@Undo_List, $Current_Atom, $Max_Size, $In_Undo);

=item new(MOVES)

Initialize the Undo engine. MOVES is the number of atoms to save.
0 (or no argument) allows infinite undo.

This method must be called before any undo-able moves are made (i.e., it can be
called after the hands are dealt).  This method will also re-initialize the
engine for a new game.

=cut

    sub new {
	my $class = shift;
	# (re)set global private variables
	$Max_Size = shift || 0;
        $Current_Atom = -1;
	@Undo_List = ();
	$In_Undo = 0;

	# Make the (dummy) object to give a "handle" for methods
	my $thing = {};
	bless $thing, $class;
	return $thing;
    }

=item end_move

End the current move. Everything between the last call to end_move and now
is considered one move. This tells undo how much to undo.

=cut

    sub end_move {
	# Don't store anything if no atoms have been stored since the
	# last End_Move atom. This could happen e.g. if someone does
	# an illegal move & then wants to undo it.
	if (! defined $Current_Atom || 
	   $Current_Atom == -1 ||
	   ref($Undo_List[$Current_Atom]) eq "Games::Cards::Undo::End_Move") {
	       return;
	}

	# calling with just "store(foo)" there aren't enough args!
	my $atom = new Games::Cards::Undo::End_Move;
	$atom->store;
    } # end sub Games::Cards::Undo::end_move

    sub store {
    # Stores a move in the undo list, which can later be undone or redone. The
    # first argument is the type of move to store, other args give details about
    # the move depending on the move type.
    #
    # arg1 is a subclass of Undo::Atom
	# Don't store moves if the undo engine hasn't been initialized
	return unless defined $Current_Atom;

	# don't store undo moves when we're currently implementing an undo/redo
	return if $In_Undo; 

        shift; # ignore class
	my $atom = shift; # the Undo::Atom to store

	# If we undid some moves & then do a new move instead of redoing,
	# then erase the moves we undid
	$#Undo_List = $Current_Atom;

	# Now add the move to the undo list
	push @Undo_List, $atom;

	# If the list is too big, remove a whole move (not just an Atom)
	# from the beginning of the list (oldest undos)
	my $end_class = "Games::Cards::Undo::End_Move";
	if ($Max_Size && grep {ref eq $end_class} @Undo_List > $Max_Size) {
	    $atom = shift @Undo_List until ref($atom) eq $end_class;
	}

	$Current_Atom = $#Undo_List;

        return 1;
    } # end sub Games::Cards::Undo::store

=item undo

Undo a move.

=cut

    sub undo {
    # undoing a move means undoing all the Atoms since the last
    # End_Move Atom
    # Note that this sub can (?) also undo from the middle of a move
	# If called w/ class instead of object, and we never called new(),
	# then return. This shouldn't happen.
	return unless defined $Current_Atom; # never called new
	return if $Current_Atom == -1;
	$In_Undo = 1; # Don't store info when moving cards around

	# Loop until the next End_Move Atom or until we exhaust the undo list
	my $end_class= "Games::Cards::Undo::End_Move";
	$Current_Atom-- if ref($Undo_List[$Current_Atom]) eq $end_class;
	for (;$Current_Atom > -1; $Current_Atom--) {
	   my $atom = $Undo_List[$Current_Atom];
	   last if ref($atom) eq $end_class;
	   $atom->undo;
	}
	# now $Current_Atom is on the End_Move at the end of the last move

	$In_Undo = 0; # done undoing. Allowed to store again.
	return 1;
    } # end sub Games::Cards::Undo::undo


=item redo

Redo a move that had been undone with undo.

=cut

    sub redo {
    # redoing a move means redoing every Atom from the current atom
    # (which should be an End_Move) until the next End_Move atom
	# If called w/ class instead of object, and we never called new(),
	# then return. This shouldn't happen.
	return unless defined $Current_Atom; 
	return if $Current_Atom == $#Undo_List;
	$In_Undo = 1; # Don't store info when moving cards around

	# Loop until the next End_Move Atom or until we exhaust the undo list
	my $atom;
	my $end_class = "Games::Cards::Undo::End_Move";
	$Current_Atom++ if ref($Undo_List[$Current_Atom]) eq $end_class;
	for (;$Current_Atom <= $#Undo_List; $Current_Atom++) {
	   my $atom = $Undo_List[$Current_Atom];
	   last if ref($atom) eq $end_class;
	   $atom->redo;
	}
	# now $Current_Atom is on the End_Move at the end of this move

	$In_Undo = 0; # done redoing. Allowed to store again.
	return 1;
    } # end sub Games::Cards::Undo::redo

=back

=cut

{
package Games::Cards::Undo::Atom;
# A CG::Undo::Atom object stores the smallest indivisible amount of undo
# information. The subclasses of this class implement different kinds of atoms,
# as well as the way to undo and redo them.

    sub new {
    # This new will be used by subclasses
    # arg0 is the class. arg1 is a hashref containing various fields. Just
    # store 'em.
        my $class = shift;
	my $atom = shift || {};

	# turn it into an undo move
	bless $atom, $class;
    } # end sub Games::Cards::Undo::Atom::new

    sub store {
    # Store this Atom in the Undo List
        Games::Cards::Undo->store(shift);
    } # end sub Games::Cards::Undo::Atom::store

} # end package Games::Cards::Undo::Atom

{
package Games::Cards::Undo::End_Move;
# An Undo::End_Move is just a marker. Everything in the Undo_List from just
# after the last End_Move until this one is one "move". 

    @Games::Cards::Undo::End_Move::ISA = qw(Games::Cards::Undo::Atom);

    # inherit SUPER::new
    # No other methods necessary!

} # end package Games::Cards::Undo::End_Move

{
package Games::Cards::Undo::Face;
# This object stores the act of turning a card over

    @Games::Cards::Undo::Face::ISA = qw(Games::Cards::Undo::Atom);

    # inherit SUPER::new

    sub undo {
        my $face = shift;
	my ($card, $direction) = ($face->{"card"}, $face->{"direction"});
	if ($direction eq "up") {
	    $card->face_down;
	} elsif ($direction eq "down") {
	    $card->face_up;
	} else {
	    my $func = (caller(0))[3];
	    die ("$func called with unknown direction $direction\n");
	}
    } # end sub Games::Cards::Undo::Face::undo

    sub redo {
        my $face = shift;
	my ($card, $direction) = ($face->{"card"}, $face->{"direction"});
	if ($direction eq "up") {
	    $card->face_up;
	} elsif ($direction eq "down") {
	    $card->face_down;
	} else {
	    my $func = (caller(0))[3];
	    die ("$func called with unknown direction $direction\n");
	}
    } # end sub Games::Cards::Undo::Face::redo

} # end package Games::Cards::Undo::Face

{
package Games::Cards::Undo::Splice;
# This object stores the act of adding or removing cards from a CardSet, i.e.
# one of these objects gets created each time GC::CardSet::splice is called.
# This stores most of the actions in a card game.

    @Games::Cards::Undo::Splice::ISA = qw(Games::Cards::Undo::Atom);

    # inherit SUPER::new

    sub undo {
    # If we changed ARRAY by doing:
    # RESULT = splice(ARRAY, OFFSET, LENGTH, LIST);
    # then we can return ARRAY to its original form by
    # splice(ARRAY, OFFSET, scalar(LIST), RESULT);
    #
    # (sub splice also made sure that for calls to splice without 
    # all the arguments, the missing arguments were added, and that OFFSET
    # would be >= 0)

	my $splice = shift;
	# Could do this quicket with no strict refs :)
	my ($set, $offset, $in_cards, $out_cards) = 
	    map {$splice->{$_}} qw(set offset in_cards out_cards);

	# Do the anti-splice and return its return value
	# (Return will actually be in_cards!)
	$set->splice ($offset, scalar(@$in_cards), $out_cards);
    } # end sub Cards::Games::Undo::Splice::undo

    sub redo {
    # we changed ARRAY by doing:
    # RESULT = splice(ARRAY, OFFSET, LENGTH, LIST);
    # Just redo the splice.
    # (sub splice also made sure that for calls to splice without 
    # all the arguments, the missing arguments were added, and that OFFSET
    # would be >= 0)

	my $splice = shift;
	# Could do this quicket with no strict refs :)
	my ($set, $offset, $in_cards, $length) = 
	    map {$splice->{$_}} qw(set offset in_cards length);

	# Do the splice and return its return value
	# (Return will actually be out_cards!)
	$set->splice ($offset, $length, $in_cards);
    } # end sub Cards::Games::Undo::Splice::redo

} # end package Games::Cards::Undo::Splice

1; # end package Games::Cards::Undo