package IM::Engine::HasPlugins;
use Moose::Role;
use MooseX::AttributeHelpers;

use IM::Engine::Plugin;

use Data::OptList 'mkopt';

requires 'engine';

has plugins_args => (
    is       => 'ro',
    isa      => 'ArrayRef',
    init_arg => 'plugins',
    default  => sub { [] },
);

has _plugins => (
    metaclass => 'Collection::List',
    isa       => 'ArrayRef[IM::Engine::Plugin]',
    builder   => '_build_plugins',
    init_arg  => undef,
    lazy      => 1,
    provides  => {
        elements => 'plugins',
        grep     => 'find_plugins',
    },
);

sub BUILD { } # provide an empty default in case the class has none
after BUILD => sub {
    my $self = shift;

    # Initialize plugin list so the plugins can perform further initialization
    $self->plugins;

    # Let them validate that other plugins exist, etc.
    $_->post_initialization for $self->plugins;
};

sub _build_plugins {
    my $self = shift;

    my $args = mkopt(
        $self->plugins_args,
        'plugins',
        0, # can have more than one instance of the same plugin
        [qw(HASH)],
    );

    my @plugins;
    for (@$args) {
        my ($class, $params) = @$_;
        $class = "IM::Engine::Plugin::$class"
            unless $class =~ s/^\+//;

        Class::MOP::load_class($class);

        push @plugins, $class->new(
            %{ $params || {} },
            engine => $self->engine,
        );
    }
    return \@plugins;
}

sub plugins_with {
    my $self = shift;
    my $role = shift;

    $role = "IM::Engine::Plugin::$role"
        unless $role =~ s/^\+//;

    return $self->find_plugins(sub { $_->does($role) });
}

sub each_plugin {
    my $self = shift;
    my %args = @_;

    my $role     = $args{role};
    my $callback = $args{callback};

    for my $plugin ($self->plugins_with($role)) {
        $callback->($plugin);
    }

    return;
}

sub plugin_relay {
    my $self = shift;
    my %args = @_;

    my $method = $args{method};
    my $baton  = $args{baton};

    $self->each_plugin(
        %args,
        callback => sub { $baton = shift->$method($baton, \%args) },
    );

    return $baton;
}

sub plugin_default {
    my $self = shift;
    my %args = @_;

    my $method = $args{method};
    my $default;

    # I think I want to use Continuation::Escape here :)
    $self->each_plugin(
        %args,
        callback => sub {
            return if $default;

            my $plugin = shift;
            $default = $plugin->$method(\%args);
        },
    );

    return $default;
}

sub plugin_collect {
    my $self = shift;
    my %args = @_;

    my $method = $args{method};
    my @items  = @{ $args{items} || [] };

    $self->each_plugin(
        %args,
        callback => sub { push @items, shift->$method(\%args) },
    );

    return @items;
}

no Moose::Role;

1;

__END__

=head1 NAME

IM::Engine::HasPlugins - role for objects that have plugins

=head1 DESCRIPTION

This should probably only be applied to L<IM::Engine> objects. Beware!

=head1 ATTRIBUTES

=head2 plugins

=head1 METHODS

=head2 find_plugins

Return the L<IM::Engine::Plugin> objects that return true for the passed
coderef.

=head2 plugins_with

Return the L<IM::Engine::Plugin> objects that do the particular role. For
convenience, the role specifier has C<IM::Engine::Plugin::> prepended to it,
unless it is prefixed with C<+>.

=head2 each_plugin

For each plugin that does the C<role> argument, invoke the C<callback>
argument. Returns nothing.

=head2 plugin_relay

For each plugin that does the C<role> argument, call the C<method> on it,
passing the C<baton> argument to it. The return value of C<method> is used as
the baton for the next plugin. The return value of this method is the final
state of the baton.

This is useful for letting each plugin get a chance at modifying some value or
object.

=head2 plugin_default

For each plugin that does the C<role> argument, call the C<method> on it. The
first return value of C<method> that is defined will be returned from this
method.

This is useful for (among other things) letting each plugin get a chance at
short-circuiting some other calculation.

=head2 plugin_collect

For each plugin that does the C<role> argument, call the C<method> on it. The
return values of all C<method> calls are collected into a list to be returned
by this method.

This is useful for (among other things) letting each plugin contribute to
constructor arguments.

=cut