package PXP::Plugin;

=pod

=head1 NAME

  PXP::Plugin - Plugin class definition (used only in the
  internal registry)

=head1 SYNOPSIS

<?xml version="1.0"?>

<plugin
  id="IMC::WebApp::TestPlugin"
  name="Test plugin"
  version="0.1"
  provider-name='IDEALX'>

</plugin>


=head1 DESCRIPTION

A plugin groups together a set of extensions and/or
extension-points.

A C<PXP::Plugin> represents such a container as it is read and its
content loaded into the system. A common interface is provided to
access the configuration descriptors and the actual implementation
that has been loaded into the system.

B<NOTE>: a C<PXP::Plugin> object is B<NOT> a plugin.

A I<real> plugin, is just a set of Perl modules loaded according to
the 'plugin.xml' descriptor file.

Plugin dependencies are supported with the <require> tag.

=head2 Limitations

We support only a subset of the Eclipse plugin model.

=cut

use strict;
use warnings;

use PXP::ExtensionPoint;
use PXP::Extension;

use XML::XPath;
use File::Spec;
use Cwd;

use Log::Log4perl qw(get_logger);
my $logger = get_logger(__PACKAGE__);

sub new {
  my $class = shift;
  my $self = {
	      name => '',
	      id => '',
	      version => 0,
	      provider_name => '',
	      extensions => {},
	      extension_points => {},
	      dependencies => []
	     };
  bless $self, $class;
  return $self->init(@_);
}

sub init {
  my $self = shift;
  my %opts = @_;

  # TODO : warn if no directory
  $self->directory($opts{'directory'});

  my $filename = $opts{'filename'} || '';
  if ($filename) {
    my $config = $self->loadLocalizedResource($filename)
      || die "cannot find config for plugin";

    my $xp = XML::XPath->new(xml => $config);

    $self->loadFromXML($xp);
  }

  return $self;
}

sub loadFromXML {
  my $self = shift;
  my $xp = shift;

  # FIXME: Check all values

  # Set plugin params;
  my @nodes = $xp->findnodes('/plugin');
  unless (@nodes) {
    $logger->error("No plugin defined in xml config file. Loading aborted.");
    return undef;
  }

  my $node = shift @nodes;
  # Define parameters
  $self->id($node->getAttribute('id'));
  $self->class($node->getAttribute('class'));
  $self->name($node->getAttribute('name'));
  $self->version($node->getAttribute('version'));
  $self->providerName($node->getAttribute('provider-name'));

  # Handle dependencies
  my @deps = $xp->findnodes('/plugin/require');
  $self->addDependencies(map ($_->getAttribute('plugin'), @deps));

  # Handle libraries
  my @libs = $xp->findnodes('/plugin/runtime/library');
  $self->addLibraries(map ($_->getAttribute('name'), @libs));

  # Save XML node
  $self->{'xml'} = $xp;

  # Add the plugin directory to Perl search path
  unshift @INC, $self->directory();

  # Extensions and ExtensionPoints are no longer instantiated here.
  # This must be done by _instantiateExtensions(), after dependencies
  # have been loaded.
}

sub instantiateExtensions {
  my $self = shift;

  my $xp = $self->{'xml'};
  my $node;
  # Create extension points
  foreach $node ($xp->findnodes('/plugin/extension-point')) {
    my $id = $node->getAttribute('id') || die "bummer";
    my $class = $node->getAttribute('class') || $id; # FIXME: if nothing was specified, do not force a class load
    my $name = $node->getAttribute('name');
    my $version = $node->getAttribute('version');
    my $obj;

    # Instantiate it
    # FIXME: Add test on values
    my $extpt = PXP::ExtensionPoint->new($self);
    if (_loadClass($class)) {
      $obj = $class->new($self);
      # support for Aurelien's approach (an extension point 'IS-A' PXP::ExtensionPoint)
      if ($obj->isa('PXP::ExtensionPoint')) {
        $logger->warn("$id should not be an PXP::ExtensionPoint !");
	undef $extpt;
	$extpt = $obj;
      }
      $extpt->object($obj);
    }
    $extpt->id($id);
    $extpt->class($class);
    $extpt->name($name);
    $extpt->version($version);
    $self->_storeExtensionPoint($extpt);
  }

  # Add extensions
  foreach $node ($xp->findnodes('/plugin/extension')) {

    # Get informations
    my $id = $node->getAttribute('id') || die "bummer";
    my $point = $node->getAttribute('point') || die "bummer";
    my $class = $node->getAttribute('class') || $id;
    my $name = $node->getAttribute('name');
    my $version = $node->getAttribute('version');
    my $obj;

    my $ext = PXP::Extension->new($self);
    $ext->id($id);
    $ext->class($class);
    $ext->name($name);
    $ext->version($version);
    $ext->point($point);
    $ext->node($node);
    $self->_storeExtension($ext);
  }
}

=pod

=over 4

=item instantiateExtension($class, @args)

Load a perl class ($class) and instantiate a new object of this class, with optional arguments for the object initializer (@args)

Return the new instance or undef if the class could not be loaded

=back

=cut

sub instantiateExtension {
  my $self = shift;
  my $class = shift;
  my @args = shift;
  
  # Instantiate
  if (PXP::Plugin::_loadClass($class)) {
    return $class->new(@args);
  } else {
    die "Could not instanciate class $class";
    return undef;
  }
}

=pod "

=over 4

=item I<name>, I<id>, I<version>, I<providerName>

Basic accessors for plugin properties.

=cut "

sub name {
  my $self = shift;
  if (@_) {
    $self->{name} = shift;
    return $self;
  } else {
    return $self->{name};
  }
}

sub id {
  my $self = shift;
  if (@_) {
    $self->{id} = shift;
    return $self;
  } else {
    return $self->{id};
  }
}

sub class {
  my $self = shift;
  if (@_) {
    $self->{class} = shift;
    return $self;
  } else {
    return $self->{class};
  }
}

sub version {
  my $self = shift;
  if (@_) {
    $self->{version} = shift;
    return $self;
  } else {
    return $self->{version};
  }
}

sub providerName {
  my $self = shift;
  if (@_) {
    $self->{provider_name} = shift;
    return $self;
  } else {
    return $self->{provider_name};
  }
}

=pod

=item I<resourceDir>

Accessor for the 'resourceDir' attribute. 'resourceDir' points to the
directory containing the resources bundled with the plugin, such as
config files, templates, etc.

This method is called by the I<PluginRegistry> as it loads the plugin.

=cut "

sub resourceDir {
  my $self = shift;
  return $self->directory(@_);
}

sub xml {
  my $self = shift;
  return $self->{xml};
}

sub directory {
  my $self = shift;
  if (@_) {
    $self->{directory} = shift;
    return $self;
  } else {
    return $self->{directory};
  }
}

sub fullDirectory {
  my $self = shift;
  return File::Spec->catdir(getcwd(), $self->{directory});
}

sub resource {
  my $self = shift;
  return File::Spec->catfile($self->directory(), @_);
}

sub fullResource {
  my $self = shift;
  return File::Spec->catfile($self->fullDirectory(), @_);
}

sub getDependencies {
  my $self = shift;
  return $self->{'dependencies'};
}

sub addDependencies {
  my $self = shift;
  push @{$self->{'dependencies'}}, @_;
  return $self;
}

sub libraries {
  my $self = shift;
  return $self->{'libraries'};
}

sub addLibraries {
  my $self = shift;
  push @{$self->{'libraries'}}, @_;
  return $self;
}

sub _loadClass {
  my $class = shift;

  eval "require $class";
  if ($@) {
    warn "Could not load $class: " .$@;
    return undef;
  }
  return 1;
}

sub _storeExtensionPoint {
  my $self = shift;
  my $ext = shift;
  $self->{'extension_points'}->{$ext->id()} = $ext;
  return $ext;
}

sub getExtensionPoint {
  my $self = shift;
  my $id = shift;
  return $self->{'extension_points'}->{$id};
}

sub getAllExtensionPoints {
  my $self =  shift;

  if ($self->{'extension_points'}) {
      return (values %{$self->{'extension_points'}});
  }
}

sub _storeExtension {
  my $self = shift;
  my $ext = shift;
  $self->{'extensions'}->{$ext->id()} = $ext;
  # remember extension declaration order in the plugin.xml file
  push @{$self->{'extensions_array'}}, $ext;
  return $ext;
}

sub getExtension {
  my($self) = shift;
  my($id) = shift;
  return($self->{'extensions'}->{$id});
}

# Return an _ordered_ list of extensions for the plugin, so we can control extension registration order
# (this is important for constructing the MainPipeline, and make sure the RequestManager gets called first)

sub getAllExtensions {
  my $self =  shift;

  if ($self->{'extensions_array'}) {
      return @{$self->{'extensions_array'}};
  }
}

use FileHandle;

sub getResourceHandle {
  my $self = shift;
  my $resourceId = shift;

  if (! $self->isa('PXP::Plugin')) {
    $self = PXP::PluginRegistry::getPlugin($self)
      || die "cannot find which plugin I am !";
  }

  my $filename = File::Spec->catfile($self->resourceDir(), $resourceId);
  $logger->debug("resource for " . $self->id() . " should be in " . $filename);

  return new FileHandle $filename;
}

# almost the same code as above, except we try to find a localized property file
# property files are named with the locale and after the original filename,
# but with a .properties extension (instead of the filename original extension, like .xml)
sub getPropertyFileHandle {
  my $self = shift;
  my $resourceId = shift;

  my $dir;
  if (! $self->isa('PXP::Plugin')) {
    $self = PXP::PluginRegistry::getPlugin($self)
      || die "cannot find which plugin I am !";
  }

  $dir = $self->resourceDir();
  use File::Basename;
  my ($filenamePrefix, $dummy, $dummy2) = fileparse($resourceId, qr/\..*/);
  my $filename = File::Spec->catfile($dir, $filenamePrefix);

  $logger->debug('looking for property file ' . $filenamePrefix . '.properties');
  my $pfname = PXP::I18N::getPropertyFile($filename);

  return new FileHandle $pfname;
}

sub loadResource {
  my $self = shift;
  my $resourceId = shift;

  $logger->debug("loading resource $resourceId");
  my $handle = $self->getResourceHandle($resourceId);
  local $/;
  return <$handle>;
}

# same as above, but we look for a property file
# and substitute with the '%property' pattern (same as in Eclipse)
sub loadLocalizedResource {
  my $self = shift;
  my $resourceId = shift;

  $logger->debug("loading localized resource $resourceId");
  my $pfh = $self->getPropertyFileHandle($resourceId);
  unless ($pfh) {
    my $rfh = $self->getResourceHandle($resourceId);
    local $/;
    return <$rfh>;
  }

  my $rfh = $self->getResourceHandle($resourceId);

  return PXP::I18N::loadNLocalize($rfh, $pfh);
}

=pod "

=head1 SEE ALSO

C<PXP::PluginRegistry>, C<PXP::ExtensionPoint>, C<PXP::Extension>

See the article on eclipse.org describing the plugin architecture : 
http://www.eclipse.org/articles/Article-Plug-in-architecture/plugin_architecture.html

=cut

1;