
package Ball;

# Ball.pm, a class module that allows concurrent simulation (canvas) instances.
#
# This is simply a class module, nothing fancy like a derived widget or
# composite widget.  It has two virtual methods, new() and move_one_ball().
# There are two static methods, get_canvas_hash() and move_all_balls().
#
# Essentially, move_all_balls() is invoked to move all of the balls in a
# simulation's @BALLS list once - from their current to their new postion.
# After moving one ball a call to DoOneEvent() is made to handle pending 
# XEvents.  The *user* of this  module, in this case bounce.pl, has their 
# own main loop which also calls DoOneEvent() and move_all_balls() to keep 
# the simulation active.
#
# Gurusamy Sarathy (gsar@engin.umich.edu)
# Tidied up by SOL.

use English;
use Tk::Canvas;
use Tk::Widget;
use Tk qw/DoOneEvent DONT_WAIT/;
Construct Tk::Canvas 'Ball';
use strict;

# Class Ball global variables.

my %BALLS = ();			# hold @BALLS list on a per canvas basis
my (%DEFAULTS) = (		# Ball constructor option defaults
		  -color    => 'blue',
		  -size     => 20.0,
		  -position => [12.0,12.0],
		  -velocity => [6.0, 9.0],
		  );

sub new {			# Ball object constructor

    # Create a new Ball object, which just happens to be a Canvas item.
    # Fill-in values for defaulted parameters, create the oval item, and
    # store object-specific information in the ball's hash.
    #
    # Finally, update the class global %BALLS hash, indexed by a hashed canvas
    # reference, with the new ball.  Note the special Tk::bind statement that
    # removes a canvas from the %BALLS hash when the canvas is destroyed, thus
    # keeping %BALLS trimmed and preventing a very slow memory leak.

    my($class, $canvas, %args) = @ARG;

    my @missing_args = grep ! defined $args{$ARG}, keys %DEFAULTS;
    @args{@missing_args} = @DEFAULTS{@missing_args};
    my($color, $size, $pos, $vel) = @args{-color, -size, -position, -velocity};

    my $ball = $canvas->create('oval', 
        ($pos->[0] - ($size/2.0)), ($pos->[1] - ($size/2.0)),
        ($pos->[0] + ($size/2.0)), ($pos->[1] + ($size/2.0)),
        -fill => $color,
    );
    $canvas->Tk::bind(
        '<Destroy>' => sub {delete $BALLS{Ball->get_canvas_hash($canvas)}}
    );

    my $ball_obj = {'canvas_ID' => $ball,
		    'canvas'    => $canvas,
		    'color'     => $color, 
		    'size'      => $size,
		    'pos'       => [@$pos],
		    'vel'       => [@$vel],
                   };

    push @{$BALLS{Ball->get_canvas_hash($canvas)}->{'BALLS'}}, $ball_obj;
    return bless $ball_obj, $class;

} # end new, Ball constructor

sub get_canvas_hash {

    # Hash a canvas reference to a key for indexing into the %BALLS hash.
    # For now, just use the string-ified widget reference.  If this trick
    # were ever to fail in the future then only this code needs to be fixed
    # and the Ball class would be up and running in short oder.

    my($class, $canvas) = @ARG;

    return $canvas

} # end get_canvas_hash    

sub move_one_ball {

    # Move one ball, belonging to one simulation, one clock tick.

    my ($ball_obj, $speed_ratio) = @ARG;

    my($ball, $canv, $minx, $miny, $maxx, $maxy);
    my($ballx, $bally, $deltax, $deltay);

    $speed_ratio = 1.0 unless defined $speed_ratio;
    $ball = $ball_obj->{'canvas_ID'};
    $canv = $ball_obj->{'canvas'};
    $ballx = $ball_obj->{'pos'}[0];
    $bally = $ball_obj->{'pos'}[1];

    $minx = $ball_obj->{'size'} / 2.0;
    $maxx = $ball_obj->{'canvas'}->cget(-width) - $minx;

    $miny = $ball_obj->{'size'} / 2.0;
    $maxy = $ball_obj->{'canvas'}->cget(-height) - $miny;

    if ($ballx > $maxx || $ballx < $minx) {
        $ball_obj->{'vel'}[0] = -1.0 * $ball_obj->{'vel'}[0];
    }
    if ($bally > $maxy || $bally < $miny) {
        $ball_obj->{'vel'}[1] = -1.0 * $ball_obj->{'vel'}[1];
    }

    $deltax = $ball_obj->{'vel'}[0] * $speed_ratio;
    $deltay = $ball_obj->{'vel'}[1] * $speed_ratio;

    $canv->move($ball, $deltax, $deltay);
    $ball_obj->{'pos'}[0] = $ballx + $deltax;
    $ball_obj->{'pos'}[1] = $bally + $deltay;

    return $ball_obj;

} # end move_one_ball

sub move_all_balls {

    # Move all the balls belong to one simulation instance one clock tick.

    my($class, $canvas, $speed_ratio) = @ARG;

    foreach (@{$BALLS{Ball->get_canvas_hash($canvas)}->{'BALLS'}}) {
        $ARG->move_one_ball($speed_ratio);
        DoOneEvent(DONT_WAIT);		# be kind and process XEvents if they arise
    }

} # end move_all_balls

1;
