The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# -*- cperl -*-

package Parse::Vipar::Shell;

use Tk::FBox;
use Parse::Vipar::Console;
use Parse::YALALR::Run;
use Parse::Vipar::Common;
use Parse::YALALR::Common;
use Parse::Vipar::ViparText qw(makestart makeend find_parent foreach_tag);

BEGIN { *{__PACKAGE__."::new"} = \&Parse::Vipar::subnew; }

use strict;

sub layout {
    my $self = shift;
    my ($info, $win) = @_;

    $self->{win} = $win;
    $self->{info} = $info;

    ########## STATUS ###########
    $info->{status_l} = $info->{shell_f}->Label(-text => "Enter command and press <Enter>")
        ->pack(-fill => 'x', -side => 'bottom');

    ######## CONSOLE ##########
    $info->{f1} = $info->{shell_f}->Frame()->pack(-side => 'bottom',
						  -fill => 'both',
						  -expand => 1);
    $info->{shell_t} =
        $info->{f1}->Scrolled('Console',
			      -scrollbars => 'e',
			      -width => PANEWIDTH * 2.5,
			      -command => sub { $self->execute(shift()) })
	  ->pack(-side => 'left', -fill => 'both', -expand => 1);

    my $t = $self->{_t} = $info->{shell_t};

    my $vipar = $self->{parent};
    $t->tagInvisibleLink('sym', sub {
			     foreach ($t->curTagNames()) {
				 next unless /sym_(\d+)/;
				 $vipar->select_symbols($1);
			     }
			 });

    $t->tagInvisibleLink('rule', sub {
			     foreach ($t->curTagNames()) {
				 next unless /rule_(\d+)/;
				 $vipar->select_rule($1);
			     }
			 });

    $t->tagInvisibleLink('state', sub {
			     foreach ($t->curTagNames()) {
				 next unless /state_(\d+)/;
				 $vipar->select_state($1);
			     }
			 });

    ######### BUTTON MENU ############
    my $buttons = $info->{actions_f} = $info->{f1}->Frame()
        ->pack(-fill => 'y');

    my %listen;

    my @button_info =
        ( [ 'Step' =>
            'step', have_file => 'normal', no_file => 'disabled' ],
          [ 'Next Action' =>
            'na', have_file => 'normal', no_file => 'disabled' ],
          [ 'Next Reduce' =>
            'nr', have_file => 'normal', no_file => 'disabled' ],
          [ 'Next Input' =>
            'ni', have_file => 'normal', no_file => 'disabled' ],
          [ 'Restart' =>
            'restart' ],
          [ 'Stop' =>
            'stop', have_file => 'normal', no_file => 'disabled' ],
          [ 'End Run' =>
            'end', have_file => 'normal', no_file => 'disabled' ],
        );

    foreach (@button_info) {
        my ($name, $action, %transitions) = @$_;
        my $b = $buttons->Button(-text => $name,
                                 -command => sub { $t->userinput($action) })
            ->pack(-side => 'top', -fill => 'x');
        while (my ($event, $state) = each %transitions) {
            push @{$listen{$event}}, sub { $b->configure(-state => $state) };
        }
    }

    $self->{listen} = \%listen;

    ######### MENU ############
    my $menubar = $info->{menu_m} = $info->{shell_f}->Menu(-type => 'menubar');
    $win->configure(-menu => $menubar);

    # Stolen from example code menus.pl
    my $modifier = 'Meta';	# Unix
    if ($^O eq 'MSWin32') {
	$modifier = 'Control';
    } elsif ($^O eq 'MacOS') {  # one of these days
	$modifier = 'Command';
    }

    my $m_file = $menubar->cascade(-label => '~File', -tearoff => 0);
    $m_file->command(-label => 'Load ~Parser...',
                     -command => sub {
                         $self->fileDialog("load parser %s",
                                           ["YACC files", '*.y'])
                     });
    $m_file->command(-label => 'Load ~Input...',
                     -command => sub { $self->fileDialog("load %s") });
    $m_file->command(-label => 'Exit', -command => \&Tk::exit);

    # Setup lookahead explanations

    $self->setup_why() if $vipar->{builder}->{why};
    $self->setup();
}

sub setup_why {
    my ($self) = @_;
    my $t = $self->{_t};
    my $vipar = $self->{parent};

    $t->map->{pre}->{lookahead} = sub {
	my ($xmltag) = @_;
	if (exists $xmltag->{state}) {
	    my $tag = $t->makeTag('lookahead',
				  state => $xmltag->{state},
				  item => $xmltag->{item},
				  token => $xmltag->{token});
	    $xmltag->{body} = [ makestart('lookahead'),
				@{$xmltag->{body}}, ' ',
				makestart($tag),
				'(why)',
				makeend('lookahead', $tag) ];
	    $t->tagLink($tag);
	} else {
	    $xmltag->{body} = [	makestart('lookahead'),
				@{$xmltag->{body}},
				makeend('lookahead') ];
	}
    };

    $t->tagBind('lookahead', '<1>', sub {
		    my %info = $t->getNumericalAttrs('lookahead');
		    $vipar->why_lookahead(@info{'state','item','token'});
		});

    my $parser = $self->{parent}->{parser};
    $t->map->{pre}->{FIRST} = sub {
	my ($xmltag) = @_;
	my ($token, $symbol) = @$xmltag{'token','symbol'};
	my @tags = ("FIRST",
		    "FIRST_symbol_${symbol}_token${token}");
	$xmltag->{body} = [ @{$xmltag->{body}}, ' ',
			    makestart(@tags),
			    '(why)',
			    makeend(@tags) ];
	$t->tagLink($tags[1], sub { $t->userinput("why is token ".$parser->dump_sym($token)." in FIRST(".$parser->dump_sym($symbol).")"); });
    };

    $t->map->{pre}->{nullable} = sub {
	my ($xmltag) = @_;
	my $symbol = $xmltag->{symbol};
	my @tags = ("nullable",
		    "nullable_$symbol");
	$xmltag->{body} = [ @{$xmltag->{body}}, ' ',
			    makestart(@tags),
			    '(why)',
			    makeend(@tags) ];
	$t->tagLink($tags[1], sub { $t->userinput("why is symbol ".$parser->dump_sym($symbol)." nullable"); });
    };
}

sub announce {
    my ($self, $event) = @_;
    foreach my $cb (@{ $self->{listen}->{$event} }) {
        if (!ref $cb) {
            $self->$cb($event, @_[2..$#_]);
        } elsif (ref $cb eq 'ARRAY') {
            $cb->[0]->(@$cb[1..$#$cb]);
        } else {
            &$cb;
        }
    }
}

sub restart {
    my ($self) = @_;
    $self->report_cmd("RESTART");
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $ptree = $vipar->{parsetree};

    $ptree->{_t}->delete('all');
    if (!defined $self->{current_file}) {
        $self->log("INTERNAL ERROR: No file currently open!");
        return;
    }
    $self->load_file($self->{current_file});
}

sub stop {
    my ($self) = @_;
    $self->report_cmd("STOP");
    $self->{stopped} = 1;
}

sub end {
    my ($self) = @_;
    $self->report_cmd("END");
    $self->{stopped} = 1;
    $self->announce('no_file');
}

sub log {
    my ($self, $xml, $whycmd) = @_;
    chomp($xml);
    my $t = $self->{_t};
    $t->output($xml);

    if ($whycmd) {
        my $tag = "why-".++$self->{whys};
        $t->output(" ");
        $t->output("(why)", $tag);
        $t->tagLink($tag, sub { $whycmd->($self) });
    }

    $t->output("\n");
}

sub why_read {
    my ($self, $tokenname) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    $self->report_cmd("WHY READ $tokenname");
    $self->log("seemed like a good thing to do at the time");
}

sub log_read {
    my ($self, $token) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $tokenname = $parser->dump_sym($token);
    $self->set_lookahead($token);
    $self->log("read input <sym id=$token>token $E{$tokenname}</sym>");
}

sub why_nullable {
    my ($self, $symname) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $builder = $vipar->{builder};
    my $sym = $parser->{symmap}->get_index($symname)
        or do { $self->log("Unknown symbol $E{$symname}"); return; };
    $self->report_cmd("WHY IS $symname NULLABLE");
    my $xmlsym = $parser->dump_sym($sym, 'xml');
    my ($binary, $xml) =
        $builder->explain_nullable($sym, 'xml');
    $self->log("$xmlsym is nullable because\n$xml");
}

sub why_FIRST {
    my ($self, $tokenname, $symname) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $builder = $vipar->{builder};
    my $sym = $parser->{symmap}->get_index($symname)
        or do { $self->log("Unknown symbol $E{$symname}"); return; };
    my $token = $parser->{symmap}->get_index($tokenname)
        or do { $self->log("Unknown symbol $E{$tokenname}"); return; };
    $self->report_cmd("WHY IS $tokenname IN FIRST($symname)");
    my $xmlsym = $parser->dump_sym($sym, 'xml');
    my $xmltok = $parser->dump_sym($token, 'xml');
    my ($binary, $xml) =
        $builder->explain_FIRST($token, $sym, 'xml');
    $self->log("$xmltok is in FIRST($xmlsym) because\n$xml");
}

sub why_shift {
    my ($self, $symname, $state0) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $builder = $vipar->{builder};
    my $sym = $parser->{symmap}->get_index($symname)
        or do { $self->log("Unknown symbol $E{$symname}"); return; };
    $self->report_cmd("WHY SHIFT $symname IN STATE $state0");
    my $kernel0 = $parser->{states}->[$state0];
    my $actions = $kernel0->{actions};
    my $state1 = $actions->[$sym];
    my $xmlsym = $parser->dump_sym($sym, 'xml');
    my ($binary, $xml) =
        $builder->explain_shift($kernel0, $sym, $state1, $actions, 'xml');
    $xml = "shift $xmlsym because ".$parser->dump_kernel($kernel0, 'briefxml').$xml;
    $self->log($xml);
}

sub log_shift {
    my ($self, $sym, $kernel0, $kernel1) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $symname = $parser->dump_sym($sym);
    my ($state0, $state1) = ($kernel0->{id}, $kernel1->{id});
    $self->set_lookahead(undef);
    my $whycmd;
    $whycmd = sub { shift()->{_t}->userinput("ws\@$state0 $symname") }
      if $vipar->{builder}->{why};
    $self->log("<state id=$state0>state $state0</state>: "
	       ."shift <sym id=$sym>token $E{$symname}</sym>, "
	       ."goto <state id=$state1>state $state1</state>",
	       $whycmd);
    $self->set_state($state1);
}

sub why_reduce {
    my ($self, $tokenname, $state0) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $builder = $vipar->{builder};
    my $token = $parser->{symmap}->get_index($tokenname);
    my $kernel0 = $parser->{states}->[$state0];
    my $actions = $kernel0->{actions};
    my $action = $actions->[$token];
    my ($rule, $lhs, $sz_rhs) = @$action;

    print "why_reduce($tokenname, $state0)\n";

    $self->report_cmd("WHY REDUCE ON $tokenname IN STATE $kernel0->{id}");
    my ($binary, $xml) =
      $builder->explain_reduce($kernel0, $token, $action, $actions, 'xml');
    my ($state, $index, $symbol, $ultimate_kitem) = @$binary;
    my $tag = $self->{_t}->makeTag('lookahead',
				   state => $state0,
				   item => $index,
				   token => $symbol);
    $vipar->data->{$tag} = $binary;
    print "UKI=$ultimate_kitem\n";
    my $full = "reduced $E{$parser->dump_rule($rule, '<-', 'brief')} ";
    $full .= "because $E{$parser->dump_kernel($kernel0)}\n";
    $full .= $xml;
    $self->log($full);
}

sub log_reduce {
    my ($self, $token, $kernel0, $state1, $action) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my ($rule, $lhs, $sz_rhs) = @$action;
    my $lhsname = $parser->dump_sym($lhs);
    my $tokenname = $parser->dump_sym($token);
    my $state0 = $kernel0->{id};
    my $whycmd;
    $whycmd = sub { shift()->{_t}->userinput("wr\@$state0 on $tokenname") }
      if $vipar->{builder}->{why};
    $self->log("reduce <rule id=$rule>$E{$parser->dump_rule($rule, '<-', 'brief')}</rule>, goto <state id=$state1>state $state1</state>", $whycmd);
    $self->set_state($state1);
}

sub why_lookahead {
    my ($self, $tokenname, $state, $item) = @_;
    my $vipar = $self->{parent};
    my $builder = $vipar->{builder};
    my $parser = $vipar->{parser};
    my $token = $parser->{symmap}->get_index($tokenname);
    my $kernel = $parser->{states}->[$state];

    $self->report_cmd("WHY LOOKAHEAD TOKEN $tokenname IN STATE $state ITEM $item");

    my $tag = "lookahead_state${state}_item${item}_token$token";
    print "LOOKING UP tag $tag\n";
    my $binary = $vipar->data->{$tag};
    my $ultimate_kitem;

    if (defined $binary) {
	print "BINARY=$binary\n";
	$ultimate_kitem = $binary->[3];
	print "READ UKI=$ultimate_kitem\n";
    }

    my (undef, $xml) =
      $builder->explain_lookahead($kernel, $item, $token, $ultimate_kitem, 'xml');
    $self->log($xml);
}

sub execute {
    my ($self, $command) = @_;
    print "Executing $command\n";

    if (lc $command eq 's' || lc $command eq 'step') {
        $self->step();
    } elsif ($command =~ /^eval /i) {
	local $::vipar = $self->{parent};
	my $value = eval substr($command, 5);
	$value = "(undef)" if !defined $value;
	$self->log($value);
    } elsif (lc $command eq 'na' || lc $command eq 'next action') {
        $self->next_action();
    } elsif ($command =~ /^(?:next reduce(?:to\s+)?|nr)\s*(.+)?/i ) {
        my $symbol = $1;
        $self->next_reduce($symbol);
    } elsif ($command =~ /^(?:next input|ni)\s*(.+)?/i ) {
        my $token = $1;
        $self->next_read($token);
    } elsif ($command =~ /^(?:why shift|ws)\s*(.+)?/i ) {
        my $rest = $1;
        if ($rest =~ /\@(\d+) (\S+)/) {
            $self->why_shift($2, $1);
        } elsif ($rest =~ /(\S+) in (state\s+)?(\d+)/) {
            $self->why_shift($1, $3);
        } else {
	    $self->why_shift($rest, $self->{currentState});
        }
    } elsif ($command =~ /^(?:why reduce|wr)\s*(.+)?/i ) {
        my $rest = $1;
        if ($rest =~ /\@(\d+) on (\S+)/) {
	    print "rest=$rest, grabbed \$1=$1 \$2=$2\n";
            $self->why_reduce($2, $1);
        } else {
            my ($state) = $rest =~ /state (\d+)/i;
            my ($lookahead) = $rest =~ /on (\S+)/i;
	    $state = $self->{currentState} if !defined $state;
            if (!defined $state || !defined $lookahead) {
                $self->log("Error: bad params to WHY REDUCE command");
            } else {
                $self->why_reduce($lookahead, $state);
            }
        }
    } elsif ($command =~ /^(?:why input|wi)\s*(\S+)/i ) {
        $self->why_read($1);
    } elsif ($command =~ /^(?:open)\s+\"(.*)\"/i ) {
        $self->load_file($1);
    } elsif ($command =~ /^restart/i ) {
        $self->restart();
    } elsif ($command =~ /^stop/i ) {
        $self->stop();
    } elsif ($command =~ /^stop/i ) {
        $self->end();
    } elsif ($command =~ /^load parser (\"?)(.*)\1$/i ) {
        $self->load_parser($2);
    } elsif ($command =~ /^load (\"?)(.*)\1$/i ) {
        $self->load_file($2);
    } elsif ($command =~ /^why lookahead (.*)/) {
	my $rest = $1;
	my ($state, $token, $item);
	($state) = $rest =~ /state\s+(\d+)/;
	$state = $self->{currentState} if (!defined $state);
	($token) = $rest =~ /token\s+(\S+)/;
	($item) = $rest =~ /item (\d+)/;
	$self->why_lookahead($token, $state, $item);
    } elsif ($command =~ /^why (is )?(symbol )?(\S+) nullable/) {
	my $symbol = $3;
	$self->why_nullable($symbol);
    } elsif ($command =~ /^why (is )?(token )?(\S+) in FIRST\((.*?)\)/) {
	my ($token, $symbol) = ($3, $4);
	$self->why_FIRST($token, $symbol);
    } elsif (lc $command eq 'exit') {
	Tk::exit(0);
    } else {
        $self->log("Error: unknown command");
    }
}

sub status {
    my ($self, $msg) = @_;
    my $status = $self->{info}->{status_l};
    $status->configure(-text => $msg);
}

sub report_cmd {
    my ($self, $command) = @_;
    $self->{_t}->rewrite($command);
}

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

    my $shift_cb = $self->{callbacks}->{shift};
    local $self->{callbacks}->{shift} = sub { $shift_cb->(@_); 0; };
    my $reduce_cb = $self->{callbacks}->{reduce};
    local $self->{callbacks}->{reduce} = sub { $reduce_cb->(@_); 0; };
    my $read_cb = $self->{callbacks}->{read};
    local $self->{callbacks}->{read} = sub { $read_cb->(@_); 0; };

    $self->report_cmd("STEP");
    $self->go();
}

sub next_read {
    my ($self, $token) = @_;

    my $toknum;

    if (defined $token) {
        $toknum = $self->{parent}->{parser}->{symmap}->get_index($token, 1);
        if (!defined $toknum) {
            $self->log("Token $E{$token} unknown");
            return;
        }
    }

    # Continue if TOKEN given and the token read in is not TOKEN
    my $read_cb = $self->{callbacks}->{read};
    local $self->{callbacks}->{read} =
        sub { $read_cb->(@_); defined $toknum && $toknum != $_[1]; };

    $self->report_cmd("NEXT INPUT".(defined $token ? " $token" : ""));
    $self->go();
}

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

    my $shift_cb = $self->{callbacks}->{shift};
    local $self->{callbacks}->{shift} = sub { $shift_cb->(@_); 0; };
    my $reduce_cb = $self->{callbacks}->{reduce};
    local $self->{callbacks}->{reduce} = sub { $reduce_cb->(@_); 0; };

    $self->report_cmd("NEXT ACTION");
    $self->go();
}

sub next_reduce {
    my ($self, $symname) = @_;

    my $symnum;
    $symnum = $self->{parent}->{parser}->{symmap}->get_index($symname)
        if defined $symname;

    my $reduce_cb = $self->{callbacks}->{reduce};
    local $self->{callbacks}->{reduce} =
        sub {
            $reduce_cb->(@_);
            defined $symnum && $symnum != $_[3]->[1];
        };

    $self->report_cmd("NEXT REDUCE".(defined $symname ? " TO $symname" : ""));
    $self->go();
}

sub fileDialog {
    my ($self, $action, @types) = @_;
    push @types, ["All files", '*'];
    my $filename = $self->{win}->getOpenFile(-filetypes => \@types);
    return if !defined $filename || $filename eq '';
    $self->{_t}->userinput(sprintf($action, '"'.$filename.'"'));
}

sub load_parser {
    my ($self, $filename) = @_;
    $self->report_cmd("LOAD PARSER \"$filename\"");
    $self->log("Unimplemented");
}

sub load_file {
    my ($self, $filename) = @_;
    if (!open(FILE, $filename)) {
        $self->log("Error opening $E{$filename}: $!");
        return;
    }

    $self->report_cmd("LOAD \"$filename\"");
    $self->{current_file} = $filename;

    my @data = grep(!/^$/, <FILE>);
    close FILE;
    chomp(@data);

    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};

    my @tokens = eval {
        map { my ($t, $v) = split(/\s+/, $_, 2);
              [ $parser->{symmap}->get_index($t), $v ] } @data;
    };

    if ($@) {
        $self->log("Unknown token found in $E{$filename}: $@");
        $self->announce(no_file => $filename);
    } else {
        $self->log("$E{$filename} loaded");
        $self->{runner} =
          Parse::YALALR::Run->new($parser, \@tokens, $self->{callbacks});
        $self->announce(have_file => $filename);
	$self->set_state(0);
    }
}

sub _set_prompt {
    my $self = shift;
    my ($state, $la) = ($self->{currentState}, $self->{currentLA});
    my $prompt = '';
    $prompt .= "<state id=$state>$E{\"State $state\"}</state>"
      if defined $state;
    if (defined $la) {
	$prompt .= " " if $prompt;
	$prompt .= "<sym id=$la>".$self->{parent}->{parser}->dump_sym($la, 'xml')."</sym>";
    }
    $prompt .= $E{'> '};

    $self->{_t}->setprompt($prompt);
}

sub set_state {
    my ($self, $state) = @_;
    $self->{currentState} = $state;
    $self->_set_prompt();
}

sub set_lookahead {
    my ($self, $token) = @_;
    $self->{currentLA} = $token;
    $self->_set_prompt();
}

sub setup {
    my ($self) = @_;
    my $vipar = $self->{parent};
    my $parser = $vipar->{parser};
    my $ptree = $vipar->{parsetree};

    my %callbacks =
        ( 'read' => sub {
            my ($runner, $token) = @_;
            $self->log_read($token);
	    $self->{_t}->update();
	    !$self->{stopped};
          },
          discard => sub {
              $self->log("Discarded $E{$parser->dump_sym($_[1])}"); 1;
          },
          recover => sub {
              $self->log("Recovered on $E{$parser->dump_sym($_[1])}"); 1;
          },
          error => sub {
              $self->log("Error on $E{$parser->dump_sym($_[1])}"); 1;
          },
          popstate => sub {
              $self->log("Popping symbol $E{$parser->dump_sym($_[2])}"); 1;
          },
          fatal => sub {
              $self->log("FATAL: $E{$_[1]}"); 1;
          },
          'exec' => sub {
	      my $val = $_[2];
	      $val = '<undef>' if !defined $val;
              $self->log("Executed, val=$E{$val}"); 1;
          },
          done => sub {
              $self->end();
          },
          reduce => sub {
              my ($runner, $token, $oldstate, $action, $value, $newstate) = @_;
              my $lhs = $action->[1];
              $ptree->reduce($action->[2], $parser->dump_sym($lhs), $value);
              $vipar->select_state($newstate);
              $self->log_reduce($token, $oldstate, $newstate, $action);
	      $self->{_t}->update();
              !$self->{stopped};
          },
          'shift' => sub {
              my ($runner, $state, $sym, $tostate) = @_;
              $ptree->push($parser->dump_sym($sym));
              $vipar->select_state($tostate->{id});
              $self->log_shift($sym, $state, $tostate);
              1;
          },
        );

    $self->{callbacks} = \%callbacks;
    $self->announce('no_file');

    print "Filling vipar: $vipar\n";
    print "Filling file: $vipar->{datafile}\n";
    $self->{_t}->userinput("load \"$vipar->{datafile}\"")
      if defined $vipar->{datafile};
}

sub go {
    my $self = shift;
    $self->{runner}->parse();
    $self->{stopped} = 0;
}

sub run {
    my ($self, $cmd) = @_;
    $self->{_t}->userinput($cmd);
}

1;