# Copyright (C) 2006-2007, David Muir Sharnoff <muir@idiom.com>

package Plugins;

use strict;
use warnings;
use UNIVERSAL qw(can);
use Carp;
our $VERSION = 0.41;
our $debug = 0;

sub new
{
	my ($pkg, %args) = @_;

	my $context = $args{context} || {};
	my $pkg_override = $context->{pkg_override} || '';

	if ($pkg_override ne __PACKAGE__ 
		and scalar(caller()) ne $pkg_override
		and can($pkg_override, 'new')
		and can($pkg_override, 'new') ne \&new)
	{
		my $new = can($pkg_override, 'new');
		croak "no new in $pkg_override" unless $new;
		@_ = ($pkg_override, %args);
		goto &$new;  # so caller() works
	}

	my $self = bless {
		%args,
		list			=> undef,
		new_list		=> undef,
		plugins			=> {},
		new_config		=> undef,
		config			=> {},
		configfile		=> $args{configfile} || $context->{configfile},
		context			=> $context,
		requestor		=> $args{requestor} || scalar(caller()),
		api			=> $args{api},
	}, $pkg;

	return $self;
}

sub startconfig
{
	my ($self) = @_;

	$self->{new_list} = [];
	$self->{new_config} = {};
}

sub readconfig
{
	my ($self, $configfile, %args) = @_;

	croak "only one call to readconfig() before initialize()" if $self->{new_list};

	$self->startconfig();
	$args{self} ||= scalar(caller());
	$self->parseconfig($configfile, %args);
}

sub parseconfig { croak "Plugins must be subclassed and the subclass must define a parseconfig() method"; };

our %required;

sub pkg_invoke
{
	my ($self, $pkg, $method, @args) = @_;
	unless ($required{$pkg}++) {
		my $p = $pkg;
		$p =~ s!::!/!g;
		eval { require "$p.pm" };
		die "require $p: $@" if $@;
	}
	return undef unless $method;
	my $f = can($pkg, $method);
	return undef unless $f;
	return &$f(@args);
}

my %used;

#
# Plugins that aren't modules and need to be automagically
# turned into a module...
#
sub file_plugin
{
	my ($self, $file, %opts) = @_;

	unless (-f $file) {
		$opts{search_path} ||= [];
		for my $dir (@{$opts{search_path}}) {
			next unless -f "$dir/$file";
			$file = "$dir/$file";
			last;
		}
	}
	my $ref = $opts{referenced} || '';
	croak "Could not open $file $ref"
		unless -f $file;
	my $pkg = $file;
	$pkg =~ s/[^A-Z0-9a-z_]//g;
	$pkg = "Plugins::AutoGenerated::$pkg";
	while($used{$pkg}++) {
		$pkg .= "::R".int(rand(10000));
	}


	require File::Slurp;
	import File::Slurp;

	my $contents = read_file($file);
	my $justbefore = "\n#line 0 $file\n";

	# untaint
	$contents =~ m/^(.*)/s;
	$contents = $1;

	$opts{prefile} ||= '';
	$opts{postfile} ||= '';

	my $isa = $opts{isa} || 'Plugins::Plugin';

	my $eval = sprintf("#line %d %s\n", __LINE__, __FILE__) . <<END;

		package $pkg;

		our \@ISA = qw($isa);
		use strict;

		$opts{prefile}

		$justbefore
		$contents

		$opts{postfile}
		1;
END

	eval $eval;
	die "eval $@" if $@;
	return $pkg;
}

sub genkey
{
	my ($self, $context) = @_;
	my $key = "$context->{pkg}/$context->{configfile}";
	return $key;
}

sub registerplugin
{
	my ($self, %context) = @_;
	my $pkg = $context{pkg};
	{ 
		no strict qw(refs);
		$self->pkg_invoke($pkg)
			unless %{"${pkg}::"};
	}
	my $key = $self->genkey(\%context);
	$context{requestor} = $self->{requestor} unless $context{requestor};
	croak "Duplicate registration of $pkg plugin at $context{file}:$context{lineno} and $self->{new_config}{$key}{file}:$self->{new_config}{$key}{lineno}\n"
		if $self->{new_config}{$key};
	$self->{new_config}{$key} = \%context;
	push(@{$self->{new_list}}, $key);
	return \%context;
}

sub initialize
{
	my ($self, %args) = @_;

	confess "readconfig() not called yet" unless defined $self->{new_list};

	if ($self->{list}) {
		my @shutargs;
		@shutargs = @{$args{shutdown_args}} if $args{shutdown_args};
		for my $old (@{$self->{list}}) {
			$self->{plugins}{$old}->shutdown();
			delete $self->{plugins}{$old};
		}
	}

	$self->{config} = $self->{new_config};
	$self->{new_config} = undef;
	$self->{list} = $self->{new_list};
	$self->{new_list} = undef;

	for my $key (@{$self->{list}}) {
		$self->{plugins}{$key} = $self->initialize_plugin($self->{config}{$key});
	}
}

sub post_initialize { }

sub api
{
	my ($self, $new) = @_;
	my $old = $self->{api};
	$self->{api} = $new if @_ > 1;
	return $old;
}

sub initialize_plugin
{
	my ($self, $context) = @_;
	my $pkg = $context->{pkg};
	$context->{pkg_override} = ref($self)
		unless $context->{pkg_override};
	my $new = can($pkg, 'new')
		or confess "no new() method for $pkg.  \@ISA for $pkg should include Plugins::Plugin";
	my $p = &$new($pkg, { context => $context, api => $self->{api} }, @{$context->{new_args}})
		or confess "$pkg->new() returned false";
	$self->post_initialize($context, $p);
	return $p;
}

sub addplugin
{
	my ($self, %context) = @_;
	my $pkg = $context{pkg};
	{ 
		no strict qw(refs);
		$self->pkg_invoke($pkg)
			unless %{"${pkg}::"};
	}
	my $key = $self->genkey(\%context);
	if ($self->{plugins}{$key}) {
		$self->{plugins}{$key}->shutdown();
	} else {
		push(@{$self->{list}}, $key);
	}
	$context{requestor} = $self->{requestor} unless $context{requestor};
	$self->{config}{$key} = \%context;
	$self->{plugins}{$key} = $self->initialize_plugin(\%context);
}

sub invoke
{
	my ($self, $method, @args) = @_;
	confess "readconfig() not called yet" unless defined $self->{list};
	confess if $method =~ /::/;
	for my $pkg (@{$self->{list}}) {
		my $plugin = $self->{plugins}{$pkg};
		$plugin->invoke($method, @args);
	}
}

sub invoke_until
{
	my ($self, $method, $satisfied, @args) = @_;
	confess "readconfig() not called yet" unless defined $self->{list};
	for my $plugin ($self->plugins) {
		my @r;
		my $m = $plugin->can($method);
		my $pkg = ref($plugin);
		print STDERR "invoke_until $method on $pkg...\n" if $debug;
		next unless $m;
		if (wantarray) {
			@r = eval { &$m($plugin, @args); };
		} else {
			$r[0] = eval { &$m($plugin, @args); };
		}
		print STDERR " results = @r\n" if $debug;
		warn $@ if $@;
		if (&$satisfied(@r)) {
			print STDERR " satisfied!\n" if $debug;
			return @r if wantarray;
			return $r[0];
		}
		print STDERR " NOT satisfied!\n" if $debug;
	}
	return () if wantarray;
	return undef;
}


sub plugins
{
	my ($self) = @_;
	confess "readconfig() not called yet" unless defined $self->{list};
	return map { $self->{plugins}{$_} } @{$self->{list}};
}

sub iterator
{
	my ($self, $method) = @_;
	confess "readconfig() not called yet" unless defined $self->{list};
	my @plugins = @{$self->{list}};
	return sub {
		for (;;) {
			return () unless @plugins;
			my $plugin = shift(@plugins);
			my $f = $self->{plugins}{$plugin}->can($method);
			next unless $f;
			return &$f($self->{plugins}{$plugin}, @_);
		}
	}
}


package Plugins::Plugin;

use strict;
use warnings;
use Carp qw(cluck confess);

our $AUTOLOAD;

sub DESTROY {}
sub shutdown {}

sub invoke
{
	my ($self, $method, @args) = @_;
	if ($Plugins::debug) {
		my $pkg = ref($self);
		print STDERR "Invoking $method on $pkg\n";
	}
	confess if $method =~ /::/;
	my $m = $self->can($method);
	return undef unless $m;
	&$m($self, @args);
}

sub new
{
	my ($pkg, $pconfig,  %args) = @_;
	return bless { context => $pconfig->{context}, api => $pconfig->{api}, config => \%args }, $pkg;
}

sub AUTOLOAD
{
	my $self = shift;

	my $auto = $AUTOLOAD;
	my $ref = ref($self);
	my $p = __PACKAGE__;
	$auto =~ s/^${ref}::// or $auto =~ s/^${p}:://;
	return $self->{myapi}->invoke($auto, @_)
		if $self->{myapi};
	return $self->{api}->invoke($auto, @_)
		if $self->{api};
	cluck "No method '$auto'";
}

1;

