package Sim::Dispatcher;

use strict;
use warnings;

our $VERSION = '0.02';

use Carp qw( carp croak );

our $DEBUG = 0;

sub new {
    my $self = ref $_[0] ? ref shift : shift;
    my %opts = @_;
    croak "No clock given" if !$opts{clock};
    bless {
        clock => $opts{clock},
        queue => [],
    }, $self;
}

sub now {
    $_[0]->{clock}->now;
}

sub schedule {
    my $self = shift;
    my %events = @_;
    while (my ($time, $handle) = each %events) {
        if ($time < $self->now) {
            carp "out-dated event [$time => $handle] ignored";
            next;
        }
        $self->_insert_event([$time => $handle]);
    }
}

sub _insert_event {
    my ($self, $event) = @_;
    my $queue = $self->{queue};
    for (my $i = 0; $i < @$queue; $i++) {
        if ($event->[0] < $queue->[$i]->[0]) {
            splice( @$queue, $i, 0, $event );
            return;
        }
    }
    push @$queue, $event;
}

sub fire_next ($) {
    my $self = shift;
    my $queue = $self->{queue};
    my $clock = $self->{clock};
    return undef if @$queue == 0;
    my $event = shift @$queue;
    my ($time, $handle) = @$event;
    my $now = $self->now;
    if ($time >= $now) {
        $clock->push_to($time);
        $handle->();
    } else {
        die "Clock modified outside of the dispatcher: next event is at $time while now is $now";
    }
    return 1;
}

sub run ($@) {
    my $self = shift;
    my %opts = @_;
    my $end_time = $self->now + $opts{duration} if defined $opts{duration};
    my $fires    = $opts{fires} || 100_000_000;
    my $i = 0;
    while (1) {
        #warn "run: next!";
        last if ++$i > $fires;
        my $t = $self->time_of_next;
        last if !defined $t or (defined $end_time and $t > $end_time);
        $self->fire_next;
    }
}

sub time_of_next ($) {
    my $self = shift;
    my $queue = $self->{queue};
    return @$queue ? $queue->[0]->[0] : undef;
}

sub reset ($) {
    my $self = shift;
    $self->{queue} = [];
    $self->{clock}->reset();
}

1;
__END__

=head1 NAME

Sim::Dispatcher - Event dispatcher for Sim

=head1 VERSION

This document describes Sim::Dispatcher 0.02 released
on June 2, 2007.

=head1 SYNOPSIS

    use Sim::Dispatcher;
    use Sim::Clock;

    my $clock = Sim::Clock->new;
    # you can also use your own Clock instance here
    my $engine = Sim::Dispatcher->new(clock => $clock);

    # Example 1: Static scheduling

    $engine->schedule(
       0 => sub { print $engine->now, ": morning!\n" },
       1 => sub { print $engine->now, ": afternoon!\n" },
       5 => sub { print $engine->now, ": night!\n" },
    );
    $engine->run( duration => 50 );
    # or Sim::Dispatcher->run( fires => 5 );

    $engine->reset();

    # Example 2: Dynamic (recursive) scheduling

    my ($count, $handler);

    # event handler:
    $handler = sub {
        $count++;
        my $time_for_next = $engine->now() + 2;
        $engine->schedule(
            $time_for_next => $handler,
        );
    };
    # only schedule the "seed" event
    $engine->schedule(
        0.5 => $handler,
    );
    $engine->run( fires => 5 );
    print "count: $count\n";  # 5
    print "now: ", $engine->now(), "\n";  # 8

=head1 DESCRIPTION

This class implements the most important component in the
whole Sim library, the event dispatcher. Basically, every
activites should be coordinated by this dispatcher.
Every other objects in a simulator either register an
event scheduled to happen at some point in the "future",
or iterate through the dispatching steps.

=head1 METHODS

=over

=item C<< $obj = Sim::Dispatcher->new( clock => $clock) >>

Object constructor accepting one mandatory named
argument C<$clock> which is an instance of classes like
L<Sim::Clock>.

=item C<< $obj->schedule( $time => $handle, ... ) >>

You can use this method to register events scheduled for the future, where
$time is the timestamp and $handle is an anonymous sub which will be invoked
by the dispatcher when the simulation time is at $time.

=item C<< $obj->run( duration => $time, fires => $count ) >>

Runs the dispatcher according to the time duration and event firing count.
both of these named parameters are optional. When none is specified,
C<< fires => 100_000_000 >> will be assumed.

=item C<< $obj->fire_next() >>

This method allows you to iterate through the dispatcher running process yourself.
You should only call C<fire_next> by hand if you've found the limitation criteria
given by the C<run> method can't fit your needs.

=item C<< $obj->now() >>

Reads the value of the simulation time.

=item C<< $obj->time_of_next() >>

Gets the timestamp of the next (or nearest) coming event, which is always a bit
greater or equal to "now".

=item C<< $obj->reset() >>

Clears the internal event queue of the dispatcher and resets the internal simulation
clock too.

=back

=head1 CONCURRENCY ISSUES

If two events have exactly the same timestamp, say, 1.5,
then the one registered earlier will be fired first.

=head1 AUTHOR

Agent Zhang E<lt>agentzh@gmail.comE<gt>

=head1 COPYRIGHT

Copyright 2006, 2007 by Agent Zhang. All rights reserved.

This library is free software; you can modify and/or modify it under the same terms as
Perl itself.

=head1 SEE ALSO

L<Sim::Clock>, L<Sim>.

