use 5.006;
use strict;
use warnings;

=head1 NAME

EJS::Template::JSAdapter - JavaScript engine adapter for EJS::Template

=cut

package EJS::Template::JSAdapter;

=head1 Variables

=head2 @SUPPORTED_ENGINES

Supported JavaScript engine classes

=over 4

=item * L<JavaScript::V8>

=item * L<JavaScript>

=item * L<JavaScript::SpiderMonkey>

=item * L<JE>

=back

=cut

our @SUPPORTED_ENGINES = qw(
    JavaScript::V8
    JavaScript
    JavaScript::SpiderMonkey
    JE
);

my $default_engine;

=head1 Methods

=head2 create

Instantiates a JavaScript engine adapter object.

    my $engine = EJS::Template::JSAdapter->create();

If no argument is passed, an engine is selected from the available ones.

An explicit engine can also be specified. E.g.

    my $je_engine = EJS::Template::JSAdapter->create('JE');
    my $v8_engine = EJS::Template::JSAdapter->create('JavaScript::V8');

=cut

sub create {
    my ($class, $engine) = @_;
    
    if ($engine) {
        my $engine_class = $class.'::'.$engine;
        eval "require $engine_class";
        
        if ($@) {
            $engine_class = $engine;
            eval "require $engine_class";
            die $@ if $@;
        }
        
        return $engine_class->new();
    } elsif ($default_engine) {
        return $default_engine->new();
    } else {
        for my $candidate (@SUPPORTED_ENGINES) {
            eval "require $candidate";
            next if $@;
            
            my $engine_class = $class.'::'.$candidate;
            eval "require $engine_class";
            next if $@;
            
            $default_engine = $engine_class;
            return $engine_class->new();
        }
        
        die "No JavaScript engine modules are found. ".
            "Consider to install JavaScript::V8";
    }
}

=head2 new

Creates an adapter object.

This method should be overridden, and a property named 'context' is expected to be set up.

    package Some::Extended::JSAdapter;
    use base 'EJS::Template::JSAdapter';
    
    sub new {
        my ($class) = @_;
        my $context = Some::Underlying::JavaScript::Context->new();
        return bless {context => $context}, $class;
    }

=cut

sub new {
    my ($class) = @_;
    return bless {context => undef}, $class;
}

=head2 context

Retrieves the underlying context object.

=cut

sub context {
    my ($self) = @_;
    return $self->{context};
}

=head2 bind

Binds variable mapping to JavaScript objects.

This method should be overridden in a way that it can be invoked like this:

    $engine->bind({
        varname1 => $object1,
        funcname2 => sub {...},
        ...
    });

=cut

sub bind {
    my ($self, $variables) = @_;
    
    if (my $context = $self->context) {
        if ($context->can('bind')) {
            return $context->bind($variables);
        }
    }
}

=head2 eval

Evaluates a JavaScript code.

This method should be overridden in a way that it can be invoked like this:

    $engine->eval('print("ok\n")');

=cut

sub eval {
    my ($self) = @_;
    
    if (my $context = $self->context) {
        if ($context->can('eval')) {
            return $context->eval($_[1]);
        }
    }
}

=head1 SEE ALSO

=over 4

=item * L<EJS::Template>

=back

=cut

1;
