The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package MozRepl::AnyEvent;
use strict;
use AnyEvent;
use AnyEvent::Handle;
use AnyEvent::Strict;
use Encode qw(decode);
use Carp qw(croak);

use vars qw[$VERSION];
$VERSION = '0.40';

=head1 NAME

MozRepl::AnyEvent - AnyEvent-enabled MozRepl client

=head1 SYNOPSIS

  use MozRepl::RemoteObject;
  $ENV{ MOZREPL_CLASS } = 'MozRepl::AnyEvent';
  my $bridge = MozRepl::RemoteObject->install_bridge();

This module provides a compatible API to L<MozRepl> solely
for what L<MozRepl::RemoteObject> uses. It does not
provide plugin support. If you want a fully compatible
AnyEvent-enabled L<MozRepl>, please consider porting L<Net::Telnet>
to L<AnyEvent::Handle>.

Instead of using the process environment, you can also pass
the backend class using the C<repl_class> parameter in the constructor:

  use MozRepl::RemoteObject;
  my $bridge = MozRepl::RemoteObject->install_bridge(
      repl_class => 'MozRepl::AnyEvent',
  );

The module in C<repl_class> will be loaded through C<require>. If the module
does not load, the fatal error will propagate. No fallbacks are tried.

=head1 METHODS

=head2 C<< MozRepl::AnyEvent->new( %options ) >>

Creates a new instance.

Options

=over 4

=item *

C<log> - arrayref of log levels to enable

Currently only C<debug> is implemented, which will dump some information.

  log => [qw[debug],

=item *

C<hdl> - a premade L<AnyEvent::Handle> to talk to Firefox (optional)

=item *

C<prompt> - the regex that defines the repl prompt to look for.

Default is 

  prompt => qr/^(?:\.+>\s)*(repl\d*)>\s+/m

=back

=cut

sub new {
    my ($class, %args) = @_;
    bless {
        hdl => undef,
        prompt => qr/^(?:\.+>\s)*(repl\d*)>\s+/m,
        # The execute stack is an ugly hack to enable synchronous
        # execution within MozRepl::AnyEvent while still having
        # at most one ->recv call outstanding.
        # Ideally, this facility would go into AnyEvent itself.
        execute_stack => [],
        %args
    } => $class;    
};

=head2 C<< $repl->log( $level, @info ) >>

Prints the information to STDERR if logging is enabled
for the level.

=cut

sub log {
    my ($self,$level,@info) = @_;
    if ($self->{log}->{$level}) {
        warn "[$level] $_\n" for @info;
    };
};

=head2 C<< $repl->setup_async( $options ) >>

  my $repl = MozRepl::AnyEvent->setup({
      client => { host => 'localhost',
                  port => 4242,
                },
       log   => ['debug'],
       cv    => $cv,
  });

Sets up the repl connection. See L<MozRepl>::setup for detailed documentation.

The optional CV will get the repl through C<< ->send() >>.

Returns the CV to wait on that signals when setup is done.

=cut

sub setup_async {
    my ($self,$options) = @_;
    my $client = delete $options->{ client } || {};
    $client->{port} ||= 4242;
    $client->{host} ||= 'localhost';
    $options->{log} ||= [];
    my $cb = delete $options->{cv} || AnyEvent->condvar;
    
    $self->{log} = +{ map { $_ => 1 } @{$options->{ log }} };
    
    # Also enable the lower log levels
    my @levels = qw( error warn info debug );
    for (reverse(1..$#levels)) {
        if ($self->{log}->{ $levels[ $_ ]}) {
            $self->{log}->{ $levels[ $_-1 ]} = 1;
        };
    };
    
    my $hdl; $hdl = $self->{hdl} || AnyEvent::Handle->new(
        connect => [ $client->{host}, $client->{port} ],
        #no_delay => 1, # reduce latency, seems to have no effect
        on_error => sub {
            $self->log('error',$_[2]);
            $self->{error} = $_[2];
            $cb->send();
            delete $self->{hdl};
            $_[0]->destroy;
            undef $cb;
            undef $self;
        },
        
        on_connect => sub {
            my ($hdl,$host,$port) = @_;
            $self->log('debug', "Connected to $host:$port");
            $hdl->push_read( regex => $self->{prompt}, sub {
                my ($handle, $data) = @_;
                $data =~ /$self->{prompt}/m
                    or croak "Couldn't find REPL name in '$data'";
                $self->{name} = $1;
                $self->log('debug', "Repl name is '$1'");
                
                # Tell anybody interested that we're connected now
                $self->log('debug', "Connected now");
                $cb->send($self)
            });
        },
    );
   
    # Read the welcome banner
    $self->{hdl} = $hdl;
    
    $cb
};

=head2 C<< $repl->setup(...) >>

Synchronous version of C<< ->setup_async >>, provided
for API compatibility. This one will do a C<< ->recv >> call
inside.

=cut

sub setup {
    my ($self,$options) = @_;
    my $done = $self->setup_async($options);
    my @res = $done->recv;
    if (not @res) {
        # reraise error
        die $self->{error}
    };
    @res
};

=head2 C<< $repl->repl >>

Returns the name of the repl in Firefox.

=cut

sub repl { $_[0]->{name} };


=head2 C<< $repl->hdl >>

Returns the socket handle of the repl.

=cut

sub hdl  { $_[0]->{hdl} };

=head2 C<< $repl->execute_async( $command, $cb ) >>

    my $cv = $repl->execute_async( '1+1' );
    # do stuff
    my $two = $cv->recv;
    print "1+1 is $two\n";

Sends a command to Firefox for execution. Returns
the condvar to wait for the response.

=cut

sub execute_async {
    my ($self, $command, $cb) = @_;
    $self->log( info => "Sending command", $command);
    $cb ||= AnyEvent->condvar;
    $self->hdl->push_write( $command );
    $self->log(debug => "Waiting for prompt", $self->{prompt});

    # Push a log-peek
    #$self->hdl->push_read(sub {
    #    $self->log(debug => "Received data", $_[0]->{rbuf});
    #    return $_[0]->{rbuf} =~ /repl\d*> /;
    #});
    
    $self->hdl->push_read( regex => $self->{prompt}, 
        timeout => 10,
        sub {
            $_[1] =~ s/$self->{prompt}$//;
            #warn "<<$_[1]>>";
            $self->log(info => "Received data", $_[1]);
            # We could decode from UTF-8 here already,
            # but that would mean differnt logic between
            # MozRepl.pm and MozRepl::AnyEvent.pm
            # $cb->(decode('UTF-8' => $_[1]));
            $cb->($_[1]);
    });
    $cb
};

=head2 C<< $repl->execute( ... ) >>

Synchronous version of C<< ->execute_async >>. Internally
calls C<< ->recv >>. Provided for API compatibility.

=cut

sub execute {
    my $self = shift;
    my $cv = $self->execute_async( @_ );
    if (my $cb = pop @{ $self->{execute_stack} }) {
        # pop a callback if we have an internal callback to make
        $cv->cb( $cb );
    } else {
        $cv->recv
    };
};

1;

=head1 SEE ALSO

L<MozRepl> for the module defining the API

L<AnyEvent> for AnyEvent

=head1 REPOSITORY

The public repository of this module is 
L<http://github.com/Corion/mozrepl-remoteobject>.

=head1 AUTHOR

Max Maischein C<corion@cpan.org>

=head1 COPYRIGHT (c)

Copyright 2009-2012 by Max Maischein C<corion@cpan.org>.

=head1 LICENSE

This module is released under the same terms as Perl itself.

=cut