#! /usr/bin/env perl
use strict;
use warnings;
use Term::ANSIColor ':constants';
$Term::ANSIColor::AUTORESET = 1;

# ----------------------------------------------------------------------
# This first, to keep PadWalker away from lexical variables below.
sub scoped_eval {
  print MAGENTA @_ if $App::REPL::DEBUG;
  eval shift;
  print BOLD YELLOW $@ if $@;
}

use PadWalker 'peek_my';
use PPI;
use PPI::Find;
use Data::Dumper;
use Symbol;
use Term::ReadLine;
$App::REPL::DEBUG = 0;

{ my $in_package = 'App::REPL';
  sub in_package { @_ ? $in_package = shift : $in_package }
}

# ----------------------------------------------------------------------
# Added RESET as the color somehow bleeds into the prompt
# -- when we use Term::ReadLine
{ my $prompt;
  my $term = Term::ReadLine->new('iperl');
  sub pnew  { $prompt = RESET . in_package . ' _ ' }
  sub pcont { $prompt = RESET . in_package . '. ' }
  sub prompt {
    my $s = $term->readline($prompt);
    $term->addhistory($s) if defined($s) and $s =~ /\S/;
    $s
  }
  pnew
}

sub eek { print STDERR BOLD RED @_, "\n"; goto REPL }


# ----------------------------------------------------------------------
# Magic.  This allows 'my' variables assigned within the eval to carry
# through subsequent evals -- unless the eval'd returns from the eval,
# in which case the next eval will get the same variables.
#--
use constant PRO_IN => <<'EOP';
  use App::REPL;
  use strict;
  no warnings 'void';
EOP
sub PRO {
  my $r = "no strict 'refs';\n"
        . "package @{[in_package]};\n";
  my $h = do { no strict 'refs'; ${in_package . '::REPL::env'} || {}};
  for (keys %$h) {
    /^(.)/;
    $r .= "my $_ = $1" . q,{${", . in_package . q,::REPL::env"}->, . "{'$_'}};\n"
  }
  $r . PRO_IN
}
use constant EPI => <<'EOE';
  ;
  no strict 'refs';
  for (Symbol::qualify('')) { s/::$//; main::in_package($_) }
  ${main::in_package . '::REPL::env'} = PadWalker::peek_my(0)
EOE

# ----------------------------------------------------------------------
# More magic.  This finds the final statement of some Perl, wherever
# that statement may be (even if its result cannot escape the overall
# evaluation), and saves its value in $App::REPL::ret
#--
$App::REPL::ret = '';
{ my $f = PPI::Find->new(sub { shift->isa('PPI::Statement') });
  sub save_ret {
    my $d = shift;
  
    # don't even try if it contains something troublesome.
    return $d->serialize if has_troublesome($d);
  
    my @s = $f->in($d);
    for (reverse @s) {
      next if within_constructor($_, $d);
      print Dumper $d if $App::REPL::DEBUG > 1;
      unshift @{$_->{children}},
        bless({content => '$App::REPL::ret'}, 'PPI::Token::Symbol'),
        bless({content => '='},               'PPI::Token::Operator');
      return $d->serialize
    }
  
    # try and save the whole thing
    return '$App::REPL::ret = ' . $d->serialize if @s;

    # give up
    $d->serialize
  }
}


{ my %troublesome = map { $_, 1 } qw(sub package use require my our local);
  my $f = PPI::Find->new(sub {
    return 0 unless (my $e = shift)->isa('PPI::Token::Word');
    return 1 if exists $troublesome{$e->{content}};
    0
  });
  sub has_troublesome { $f->in(shift) } 
}
  
sub dump_ret {
  return if ref $_[0] eq 'CODE';
  print BOLD CYAN Dumper $App::REPL::ret if $App::REPL::ret;
}

{ my $fc = PPI::Find->new(sub { $_[0]->isa('PPI::Structure::Constructor')
                             or $_[0]->isa('PPI::Structure::Block') });
  sub within_constructor {
    my ($s, $d) = @_;
    my $fs = PPI::Find->new(sub { shift eq $s });
    for ($fc->in($d)) {
      return 1 for $fs->in($_);
    }
    0
  }
}

# ----------------------------------------------------------------------
# The PPI here handles the rest of the magic: it detects unfinished
# blocks and such so that the repl can request more lines until they
# complete.  Note that this does -not- handle e.g. qw(
#--
{ my $f = PPI::Find->new(sub {
    my %h = %{+shift};
    (exists $h{start} and !exists $h{finish}) ? 1 : 0
  });
  sub repl {
    my $s = '';
    REPL: while (defined($_ = prompt)) {
      $s .= "\n" . $_;
      my $d = PPI::Document->new(\$s);
      if ($f->in($d)) {
        pcont
      }
      else {
        scoped_eval PRO . save_ret($d) . EPI;
        dump_ret;
        $App::REPL::ret = '';
        $s = '';
        pnew
      }
    }
  }
}


# ----------------------------------------------------------------------
package App::REPL;
main::repl();



# ----------------------------------------------------------------------
BEGIN {
  # Patch PPI 1.118 into suitability; subsequent versions should work fine.
  # Yes, this is somewhat wrong, and will go away as soon as PPI >1.118
  # comes out -- but in these early versions of App::REPL , it should be
  # OK.
  return unless $PPI::VERSION eq 1.118;
  print "#-- Oh, you have PPI 1.118 -- we need to patch it up a bit.\n";
  no warnings 'redefine';
  package PPI::Find;
  sub _execute {
          my $self   = shift;
          my $wanted = $self->{wanted};
          my @queue  = ( $self->{in} );

          # Pull entries off the queue and hand them off to the wanted function
          while ( my $Element = shift @queue ) {
                  my $rv = &$wanted( $Element, $self->{in} );

                  # Add to the matches if returns true
                  push @{$self->{matches}}, $Element if $rv;
                  
                  # Continue and don't descend if it returned undef
                  # or if it doesn't have children
                  next unless defined $rv;
                  next unless $Element->isa('PPI::Node');

                  # Add the children to the head of the queue
                  if ( $Element->isa('PPI::Structure') ) {
                          unshift @queue, $Element->finish if $Element->finish;
                          unshift @queue, $Element->children;
                          unshift @queue, $Element->start if $Element->start;
                  } else {
                          unshift @queue, $Element->children;
                  }
          }

          1;
  }
}
