#!/usr/bin/env rakudo
use nqp;

use CoreHackers::NfaChainsaw::Utils::UserInterface;
use CoreHackers::NfaChainsaw::NfaRunner;
use CoreHackers::NfaChainsaw::Optimizer;
use CoreHackers::NfaChainsaw::Extractor;
use CoreHackers::NfaChainsaw::NFA;

use JSON::Fast;

my %cclass_names := %CoreHackers::NfaChainsaw::NFA::cclass_names;
my @known_variants := @CoreHackers::NfaChainsaw::Optimizer::known_variants;

my $*DEBUG_VERBOSE = 0;

my &prompt = &CoreHackers::NfaChainsaw::Utils::UserInterface::prompt;

sub offer-nfa-choice-from-grammar(Mu $grammar, $filter, :$full_filter = Str) {
    my @methods = $grammar.^methods.sort(*.name);

    with $filter {
        my $count-before = +@methods;
        @methods .= grep(*.name.contains($filter));
        if not @methods {
            say "filter $filter.raku() given on commandline matched no methods! There were $count-before methods available.";
        }
    }

    # if we re-run offer-nfa-choice, for example because we changed opt
    # variants, we empty @*all-nfas first
    @*all-nfas = Empty;

    with $filter {
        say "all NFAs matching $filter.raku():";
    }
    else {
        say "all NFAs:";
    }

    for @methods {
        output-nfas-for-code($_.name, $_, :$full_filter);
    }

    my @entries = do 
        (
            "  ",
            .key.fmt("% 3d"),
            " ",
            (.value.states.elems - 1).fmt("% 7d"),
            " states: ",
            .value.nfa-name
        ).join("") => .value for @*all-nfas.pairs;

    my $desired-nfa = do if @*all-nfas > 1 {
        my $choice;
        loop {
            .key.say for @entries;
            say "";
            say "s, j: Save NFAs to files (States list or Json)";
            say "q: Abort";

            $choice = prompt "Choice: ";

            if $choice eq "s" | "j" {
                my @choices = multi-bool-choice-menu(@entries);
                my $prefix = ("A".."Z").roll(8).join("");
                my $orig-out = $*OUT;

                for @choices {
                    my $ending = $choice eq "s" ?? "txt" !! "json";
                    my $fname = ("nfa_" ~ $prefix ~ "-" ~ $_.value.nfa-name.subst(" ", "_", :g) ~ "-statelist." ~ $ending).IO;
                    if $choice eq "s" {
                        my $*OUT = $fname.open(:w);
                        mydump(.value.states);
                        $*OUT.close;
                    }
                    else {
                        $fname.spurt(stateslist-to-json(.value.states));
                    }
                    $orig-out.say: "saved $_.key() to $fname.absolute(): $fname.s() bytes";
                }
                say "";
            }
            elsif $choice eq any(@entries.keys) {
                last;
            }
            elsif $choice eq "q" {
                return;
            }
            else {
                say "  !!! Unrecognized input $choice";
            }
        }

        $choice;
    }
    elsif @*all-nfas == 1 {
        0;
    }
    else {
        say "got nothing :(";
    }

    my $simstate;

    if $desired-nfa eq any(@*all-nfas.keys) {
        my $the-nfa = @*all-nfas[$desired-nfa];
        $simstate = NFASimState.start($the-nfa.states, text => '');
        $simstate.nfa-name = $the-nfa.nfa-name;
        $simstate.nfa-basename = $the-nfa.basename;
    }

    return $simstate
}

my %found-states;

sub generate-futures($state, @spk) {
    my %futures;

    for chr(ord(@spk[0]) - 1), |@spk {
        # say "forking with $_.raku() (", ($state.text ~ $_).raku, ")";
        my $forked = $state.fork($_).step(:quiet);
        my $states-key = $forked.states-key;
        %futures{$states-key}.push([$_, $forked]);
        if $forked.text.chars > 1 {
            %found-states{$states-key}.push($forked.text);
        }
    }

    return %futures;
}

sub do-random-exploration(@start-states, :%seen --> NFASimState) {
    say " ==> Will automatically explore the NFA's state space.";
    my NFASimState @active = @start-states;

    my $*TRACK-PRED-EDGES = False;

    my $total-added = 0;
    my $longest = 0;

    my $simstate;

    while @active {
        my NFASimState $item = @active.shift;

        with %seen{$item.states-key} -> $old {
            # say "we already had states for key $($item.states-key): ", $old.map(*.text.raku).join(", ");
            if $old.elems > 4 {
                next;
            }
            elsif rand < 0.8e0 {
                next;
            }
        }

        my @possible-edges = $item.all-active-edges();
        my %splitpoints;
        @possible-edges.map({ split-apart %splitpoints, $_.value });
        my @spk = %splitpoints.keys.sort;

        if @spk {
            my %futures = generate-futures($item, @spk);

            # randomize order of things picked
            my @suggestions;
            for %futures.pairs.pick(*) -> $f {
                for $f.value.list.pick(*) {
                    @suggestions.push(.[1]);
                }
            }
            @suggestions = @suggestions.unique(as => *.states-key);
            @active.push($_) for @suggestions.pick(*);
        }

        $total-added++;
        %seen{$item.states-key}.push($item);
        $longest max= $item.text.chars;

        if $total-added %% 10 {
            say "  ... already seen $total-added.fmt("% 4d") examples for %seen.elems().fmt("% 5d") different states. @active.elems().fmt("% 7d") items in the queue. Longest string $longest";
            @active .= pick(*).sort(-*.text.chars);

            if $total-added >= 500 {
                say " ... aborting search so we don't explode our memory!";
                @active = @active[0];
                last;
            }
        }
    }

    say "Here's all the combinations of states I could find:";
    for %seen.pairs.sort {
        say "States $_.key()";
        say "  ", .value.list.map(*.text.raku()).join(",  ");
        say "";
    }
    say "";

    loop {
        say "s: save to a .json file";
        say "r: pick a random state as the new current state";
        say "c: continue exploring some more";
        say "any other input: do nothing and return to menu";
        say "";
        my $choice = prompt "What do you want to do with it? ";
        if $choice eq "s" {
            my %seen_to_serialize = %seen.pairs.map({ .key => .value.list.map(*.text) });
            my $fn = prompt "filename, please (or press enter for random): ";
            if $fn eq "" {
                my $nfn = ("nfa_exploration_" ~ ("A".."Z").pick(8).join("") ~ ".json").IO;
                say "Saving to $nfn.absolute()";
                $nfn.spurt(to-json(%seen_to_serialize, :pretty));
            }
            elsif $fn.IO.e {
                say "OK to overwrite $fn.IO.absolute() ($fn.IO.s() bytes big)?";
                say "y: yes";
                say "n: no, abort";
                say "r: random new name";
                my $ch2 = prompt "> ";
                if $ch2 eq "y" {
                    $fn.IO.spurt(to-json(%seen_to_serialize, :pretty))
                }
                elsif $ch2 eq "n" {
                    # nothing here
                    next;
                }
                elsif $ch2 eq "r" {
                    my $nfn = ($fn.IO.basename ~ ("A".."Z").pick(8).join("") ~ ".json").IO;
                    say "Saving to $nfn.absolute()";
                    $nfn.spurt(to-json(%seen_to_serialize, :pretty));
                }
            }
            elsif $fn.so {
                say "Saving to $fn.IO.absolute()";
                $fn.IO.spurt(to-json(%seen_to_serialize, :pretty))
            }
            else {
                say " ==> Returning to menu!";
                last;
            }
        }
        elsif $choice eq "r" {
            $simstate = %seen.pairs.pick.value.pick;
            say " ==> Randomly chose this state:";
            say "    $simstate.gist()";
        }
        elsif $choice eq "c" {
            return do-random-exploration(%seen.pairs.map(*.value.Slip).list.grep(*.active > 0), :%seen);
        }
        else {
            say " ==> Returning to menu!";
            last;
        }
    }

    return $simstate;
}


my &*GET_OPT_OUTPUT;
my @*OPT_VARIANTS = <original>;

sub main-menu($simstate is copy, Mu $chosen-grammar) {
    while $simstate {
        my @possible-edges = $simstate.all-active-edges();
        # say "possible edges: ", @possible-edges;
        my %splitpoints;
        @possible-edges.map({ split-apart %splitpoints, $_.value });
        my @spk = %splitpoints.keys.sort;

        my @cclasses = @possible-edges
            .map(*.value)
            .map(&generate-possible-matching-characters)
            .grep(CClass)
            .map(*.cclass_id)
            .unique
            .map({ %cclass_names{$_} });

        say "current state: ", $simstate.gist;

        say "";
        say "Current text:";
        say "  ", $simstate.text.raku;
        say "";

        my $should-step = True;

        if $simstate.text.chars <= $simstate.offset || $simstate.active == 0 {
            my @inputs;
            if !@spk || $simstate.active == 0 {
                say "";
                say "The NFA has finished running.";
            }
            elsif @spk {
                say "";
                say "Possible theoretical inputs:";
                say "";

                my %futures = generate-futures($simstate, @spk);

                for %futures.pairs.sort -> $f {
                    my @examples;
                    with %found-states{$f.key} -> $_ {
                        @examples = .grep({ !.starts-with($simstate.text) && .chars != $simstate.text.chars + 1 }).pick(5).map(*.raku);
                    }
                    if @examples {
                        say +@inputs, ": ", $f.key, "    (ex: ", (@examples || Empty).join(", "), ")";
                    }
                    else {
                        say +@inputs, ": ", $f.key;
                    }
                    say "    " ~ $f.value.map(*.[0].raku).join(" ");
                    say "";
                    @inputs.push($f.value[0][0]);
                }

                say " ... also valid: stuff from cclass $_" for @cclasses;
                say "" if @cclasses;

                say "c: Enter your own";
            }

            say "b: go back one";
            say "s: go back to the start";
            say "a: automatically explore";
            say "e: show edges of currently active states";
            say "o: re-do optimization with variants turned on or off";
            say "q: stop";

            my $choice = prompt "Make your choice: ";
            last without $choice;
            if $choice eq "c" {
                my $char = prompt "Which character(s)? ";
                next without $char;
                $simstate .= fork($char);
                say " ==> advancing text: $simstate.text().raku()";
            }
            elsif $choice == any(@inputs.keys) {
                $simstate .= fork(@inputs[$choice]);
            }
            elsif $choice eq "b" {
                $simstate = $simstate.parent-state.parent-state;
                $should-step = False;
                say " ==> Went back one step, text is now $simstate.text().raku()";
            }
            elsif $choice eq "s" {
                $simstate = NFASimState.start($simstate.states);
                $should-step = False;
                say " ==> Returned to start";
            }
            elsif $choice eq "q" {
                say " ==> Quitting ...";
                say "";
                say "Text so far: $simstate.text.raku()";
                last;
            }
            elsif $choice eq "a" {
                with do-random-exploration([$simstate]) -> $newstate {
                    $simstate = $newstate;
                }

                $should-step = False;
            }
            elsif $choice eq "e" {
                mydump($simstate.states, only_states => $simstate.active.map(*.state-idx));
                $should-step = False;
            }
            elsif $choice eq "o" {
                temp $*DEBUG_VERBOSE;
                my $save = False;
                temp &*GET_OPT_OUTPUT;
                loop {
                    say "Available variants:";
                    for @known_variants.pairs {
                        say $_.key, ": ", ($_.value eq any(@*OPT_VARIANTS) ?? "[X]" !! "[ ]"), " ", $_.value;
                    }
                    say "v, vv, vvv: run optimizer with verbosity", ($*DEBUG_VERBOSE > 0 ?? " [$*DEBUG_VERBOSE]" !! "");
                    say "q: run optimizer without output", ($*DEBUG_VERBOSE == 0 ?? " [X]" !! "");
                    say "s: [$($save ?? "X" !! " ")] save optimizer output to files";
                    say "";
                    say "k: accept new variants and re-run optimizer";
                    say "anything else: abort";
                    my $choice = prompt "Choose action: ";
                    if $choice eq any(@known_variants.keys) {
                        if @known_variants[$choice] eq any(@*OPT_VARIANTS) {
                            @*OPT_VARIANTS .= grep(none(@known_variants[$choice]));
                        }
                        else {
                            @*OPT_VARIANTS.push(@known_variants[$choice]);
                        }
                    }
                    elsif $choice eq "k" {
                        my $orig-out = $*OUT;
                        my Pair @output_files;
                        my $prefix = ("A".."Z").roll(8).join("");
                        if $save {
                            &*GET_OPT_OUTPUT = -> Mu $nfa, $variant {
                                with @output_files.tail {
                                    .value.close;
                                    $orig-out.say: "  debugger output in $_.key.absolute(): $_.key.s() bytes";
                                }
                                my $fname = ("nfa_opt_" ~ $prefix ~ "-" ~ $simstate.nfa-name.subst(" ", "_", :g) ~ "-opt-" ~ $variant ~ ".optimize.txt").IO;
                                @output_files.push($fname => my $res = $fname.open(:w));
                                $res;
                            }
                        }
                        my $new = offer-nfa-choice-from-grammar($chosen-grammar, $simstate.nfa-basename, full_filter => $simstate.nfa-name);
                        @output_files.tail.value.close if @output_files;
                        if $save {
                            say "Saved optimizer outputs:";
                            for @output_files {
                                say "  debugger output in $_.key.absolute(): $_.key.s() bytes";
                            }
                        }
                        with $new {
                            $simstate = $new;
                        }
                        else {
                            say " ... No simstate gotten from your previous choice :(";
                        }
                        $should-step = False;
                        last;
                    }
                    elsif $choice eq any(<v vv vvv vvvv>) {
                        $*DEBUG_VERBOSE = $choice.chars;
                        say " ==> Verbosity is now $*DEBUG_VERBOSE";
                    }
                    elsif $choice eq "q" {
                        $*DEBUG_VERBOSE = 0;
                        say " ==> Verbosity is now $*DEBUG_VERBOSE";
                    }
                    elsif $choice eq "s" {
                        $save = not $save;
                    }
                    else {
                        say " ==> Aborting";
                        last;
                    }
                }
            }
            else {
                say "Did not recognize input $choice.raku()";
            }
        }

        if $should-step {
            say "... running step ...";
            $simstate .= step;
            say "";
        }
    }
}


multi sub MAIN(Str $filter = "", Str $grammar = "Perl6::Grammar") {
    use Perl6::Grammar:from<NQP>;

    my Mu $chosen-grammar := Perl6::Grammar;

    my @*all-nfas;

    my $simstate = offer-nfa-choice-from-grammar($chosen-grammar, $filter);
    main-menu($simstate, $chosen-grammar);
}