package POE::Class;

use vars '$VERSION';
use strict;

$VERSION = '0.01';

use POE qw/Session/;
use Carp;
use Symbol qw/gensym/;

use POE::Class::Attribs
    session_class => 'POE::Session',
    session       => undef,
    parent_object => undef,
    alias         => undef,
    shutdown      => undef;

my %Active_IDS;
my $Next_ID = 1;

# Glue session and objects
my %Session_ID_Object;

# Some autogenerated methods to access object
# sessions
BEGIN {
    no strict 'refs';
    for my $method (qw/
        post
        call
        signal
    /)
    {
        *{"POE::Class::$method"} = sub {
            my $self = shift;
            $poe_kernel->$method($self->get_session => @_);
        };
    }
    *POE::Class::yield = \&post;
    for my $method (qw/
        refcount_increment
        refcount_decrement
    /)
    {
        *{"POE::Class::$method"} = sub {
            my $self = shift;
            $poe_kernel->$method($self->get_session->ID => @_);
        };
    }
}

# Track sessions
sub handler_start {
    my ($self, $kernel, $session) = @_[OBJECT, KERNEL, SESSION];
    $self->set_session($session);
    $Session_ID_Object{$session->ID} = $self;

    if (defined $self->get_alias) {
        $kernel->alias_set($self->get_alias);
    }

    $self->create_states;
}

# cleanup internal data
sub handler_stop {
    my ($self, $session) = @_[OBJECT, SESSION];
    POE::Class->free_object_id($self->ID);
    delete $Session_ID_Object{$session->ID};
}

# put us in a shutdown state
sub handler_shutdown {
    my $self = $_[OBJECT];
    $self->set_shutdown(1);
}

# Children tracking
sub handler_child {
    my ($self, $what, $session) = @_[OBJECT, ARG0, ARG1];

    my $data = _get_internal_data($self);

    my $session_obj = POE::Class->resolve_session($session);
    return unless defined $session_obj;

    if ($what eq 'create' or $what eq 'gain') {
        push @{$data->{child_objects}}, $session_obj;
    }
    elsif ($what eq 'lose') {
        for (0 .. $#{$data->{child_objects}}) {
            if ($data->{child_objects}[$_] == $session_obj) {
                splice @{$data->{child_objects}}, $_, 1;
                last;
            }
        }
    }
}

# Our parent changed, update the ParentObject attribute
sub handler_parent {
    my ($self, $parent_session) = @_[OBJECT, ARG1];
    my $parent_object = POE::Class->resolve_session($parent_session);
    $self->set_parent_object($parent_object);
}

# simple contructor
sub new {
    my $class = shift;
    my $self = bless {}, $class;
    _init_internal_data($self);
    $self->configure(@_) if @_;
    _get_internal_data($self)->{ID} = POE::Class->allocate_object_id;
    return $self;
}

sub DESTROY {
    _destroy_internal_data($_[0]);
}

# Do nothing, we define our states in start
sub create_states { }

# Create session, store session's parent object
# and track my session id
sub start {
    my $self = shift;

    croak "Too many arguments to start()" if @_;

    return undef
        unless $self->can('handler_start');

    # Know who our parent is
    my $parent_session = $poe_kernel->get_active_session;
    my $parent_object = POE::Class->resolve_session($parent_session);
    $self->set_parent_object($parent_object);

    $self->get_session_class->create(
        object_states => [
            $self => {
                _start   => 'handler_start',
                _stop    => 'handler_stop',
                _child   => 'handler_child',
                _parent  => 'handler_parent',
                shutdown => 'handler_shutdown',
            }
        ]
    );
    return $self->get_session;
}

sub configure {
    my $self = shift;

    croak "Arguments to configure() must be even" if @_ & 1;
    my %opts = @_;

    my ($opt, $arg);
    while (($opt, $arg) = each %opts) {
        if ($self->can("set_$opt")) {
            $opt = "set_$opt";
            $self->$opt($arg);
        }
        elsif ($self->can($opt)) {
            $self->$opt($arg);
        }
        else {
            croak "Unknown method ", ref($self), "->$opt";
        }
    }
    undef;
}

# Make it possible to look up objects given a session
# and sessions given an object

sub resolve_session {
    my $class = shift;

    my $session = shift;
    return undef unless defined $session;

    my $id;
    if (
        ref $session and
        UNIVERSAL::can($session, 'ID')
    )
    {
        $id = $session->ID;
    }
    else {
        $id = $session;
    }
    return
        exists $Session_ID_Object{$id}
            ? $Session_ID_Object{$id}
            : undef;
}

sub post_children {
    my ($self, $event, @etc) = @_;

    croak "No event specified to post_children"
        unless defined $event;

    for ($self->get_child_objects) {
        $_->post($event, @etc);
    }
    undef;
}

sub post_parent {
    my ($self, $event, @etc) = @_;

    croak "No event specified to post_parent()"
        unless defined $event;

    return undef unless defined $self->get_parent_object;
    $self->get_parent_object->post($event, @etc);
}

sub ID { _get_internal_data($_[0])->{ID} }

sub get_child_objects {
    my $self = shift;
    my $data = _get_internal_data($self);

    $data->{child_objects} ||= [];
    return @{$data->{child_objects}};
}

sub allocate_object_id {
    my ($class) = @_;
    while (1) {
        last unless exists $Active_IDS{++$Next_ID};
    }
    return $Active_IDS{$Next_ID} = $Next_ID;
}

sub free_object_id {
    my ($class, $id) = @_;
    delete $Active_IDS{$id};
}

1;

__END__

=head1 NAME

POE::Class - a base class for everything OO POE

=head1 SYNOPSIS

    package My::Thingy;
    use POE;

    # POE::Class is a base class
    use base 'POE::Class';

    # None of these method are "required" but your session is not going to live
    # long if you do not atleast define a handler_start (which is the handler
    # for _start).
    sub handler_start {
        my $self = $_[OBJECT];
        # This is required for object/session tracking
        $self->SUPER::handler_start(@_[1 .. $#_]);

        # do something that keeps you alive
    }

    sub handler_stop {
        my $self = $_[OBJECT];
        # This is required for object/session tracking
        # and for internal cleanup
        $self->SUPER::handler_stop(@_[1 .. $#_]);
    }

    sub handler_child {
        my $self = $_[OBJECT];
        # This is required for object/session tracking
        $self->SUPER::handler_child(@_[1 .. $#_]);
    }

    sub handler_parent {
        my $self = $_[OBJECT];
        # This is required for object/session tracking
        $self->SUPER::handler_parent(@_[1 .. $#_]);
    }

    # Called from handler_start, define states we use
    sub create_states {
        my ($self) = @_;
        $poe_kernel->state(state_name => $self, 'handler_state_name');
    }


    # Takes an array of key value pairs and calls
    # the corresponding set method (key) with the value
    my $self = new My::Thingy(
        attrib1 => "stuff",
        attrib2 => "more stuff"
    );

    # Set what session type this will be
    $self->set_session_class("POE::Session::OtherSession");

    # Create the session
    $self->start;

    # Every object has a unique ID for tracking
    print $self->ID, "\n";

    # get your session
    my $session = $self->get_session;

    # Session/Object hierarchy

    # Get the objects of your child sessions
    my @child_objects = $self->get_child_objects;

    # Get your parent session object
    # returns undef if you have no parent
    my $parent = $self->get_parent_object;

    # Resolve a session to the object for that session
    # $session can be a session ID or a session object
    my $object = POE::Class->resolve_object($session);

    # Events

    # post an event to all your children
    $self->post_children(event => @args);

    # post an event to your parent
    $self->post_parent(event => @args);
    # - or -
    # Not as safe, does not check the existence of
    # a parent
    $self->get_parent_object->post(event => @args);

=head1 DESCRIPTION

POE::Class is a base class for making OO POE classes. This design has a one to
one object relationship to a POE session, there is one object for each session.
This is currently in the prototyping stages. The whole API may change tomarrow.

=head1 INSTANCE METHODS

=head2 Accessors

=over

=item configure

This method is called from the default C<new()> constructor. It expects an
array of key value pairs. The keys should be the names of accessor methods. The
value is the argument passed to the accessor method. This methods main purpose
is to make it easier to set instance attributes.  This method has no return
value. If any of the attributes are not object methods, C<configure()> will
fatal.

    $obj->configure(
        attribute1 => "value1",
        attrib2    => "value2"
    );

=item get_session_class

=item set_session_class

This accessor sets the name of the session class to use when we create a
session.  The default value is I<POE::Session>. This session is created when
you call C<start()>.

=item get_session

=item set_session

Stores the session object. Every object in this model corresponds to a session.
This attribute is set inside the default C<handler_start()>.

=item ID

This is the unique ID for this session. It is set in the default C<new()>
constructor to the return of C<allocate_object_id()>.

=item get_parent_object

This contains the parents I<object>. This attribute is set in the default
C<handler_parent()>. undef will be returned if you have no parent.

=item get_child_objects

All children objects are tracked with the default handlers in POE::Class. This
is an access only function. It returns an array of child objects. These objects
are tracked in the default C<handler_child()> which also relies on
C<resolve_session()> which relies on the default C<handler_start()>.

=item get_alias

=item set_alias

This attribute is here because it is a common thing to want to do. If this is set
before the session is created an alias of that name will be created for the session.
This happens in C<handler_start()>.

=item get_shutdown

=item set_shutdown

Another attribute here because of common use. It is simply a flag. This flag is
set in C<handler_shutdown> to 1. POE::Class does not use this flag, it is here
because most, if not all, subclasses do use it.

=back

=head2 Sending Events/Signals

These methods are only to make posting events to object sessions easier.

=over

=item post

=item yield

Post's an event to the object's session. The first argument should be the event
name. All other arguments will be passed to the event. Returns what
POE::Kernel's C<post()> method returns. L<POE::Kernel>.

    $self->post(event_name => "arg1", "arg2");

=item post_children

This method sends an event to all of your children. Takes the same arguments as
C<post()>. This method has no return value.

=item post_parent

This method sends an event to you parent if you have one. It takes the same
arguments as C<post()>. Returns what POE::Kernel's C<post()> method returns.
L<POE::Kernel>.

=item signal

Sends a signal to the session for this object. Arguments ae the same as POE::Kernel's
C<signal()> method minus the session. So:

    $obj->signal("DIE");

L<POE::Kernel> for details on what this is doing.

=back

=head2 External Reference Counts

=over

=item refcount_increment

=item refcount_decrement

These methods change the recount for the object's session. The arguments are the same as POE::Kernel's
methods of the same name without the session ID. So:

    $obj->refcount_increment("thingy");
    $obj->refcount_decrement("thingy");

L<POE::Kernel> for details on what this is doing.

=back

=head2 Startup

=over

=item new

This is the constructor. It does not do much. First it sets the object ID using
C<allocate_object_id>, then it calles C<configure()> we have any arguments. This method
blesses the object as a hash reference. If you do not want that override this in your
subclass. If you do override this method be sure to set the object ID.

=item start

This method actually creates the session and returns it. The session is created with the
following object states.

    _start   => handler_start
    _stop    => handler_stop
    _child   => handler_child
    _parent  => handler_parent
    shutdown => handler_shutdown

All of these handlers exist in POE::Class so do not worry about defining them unless you
want to.

This method also sets the attribute parrent object to the current active sessions object.

=back

=head1 STATES

POE::Class defines four default object states for POE's I<special> states.

=over

=item _start - handler_start

This method sets the attribute session to the current session and sets up
session -> object tracking. If you override this method in your subclass you B<should>
call it in your method or tracking will not work.

=item _stop - handler_stop

This method free all global resources. If you override it in your subclass you must
call it from your subclass (through SUPER::) or your program will leak.

=item _child - handler_child

Trackes children.

=item _parent - handler_parent

Updates parent object attribute to the new parent.

=back

=head1 CLASS METHODS

=head2 Object ID Allocation

These method are not needed unless you override the methods/handlers that use them.

=over

=item allocate_object_id

Allocates a new object ID and returns it.

=item free_object_id

Free's an allocated object ID. Expect the object ID as the only argument.

=head2 External Session Object Tracking

=over

=item resolve_session

Given a session object or a session ID returns the corresponding object or undef
if no object could be found.

=back

=head1 TODO

Write better documentation.

=head1 AUTHOR

Scott Beck E<lt>sbeck@gossamer-threads.comE<gt>

=head1 SEE ALSO

L<POE>

=cut

