#| Interaction wtih Idris 2's IDE Mode
unit class IUtils::IDEMode;

#| The underlying idris process
has $!process;
#| The port number we are connected via
has $!port;
#| The socket we are connected via
has $!socket;
#| The next request id
has $!request-id = 0;

#| Grammar for S-Expressions
grammar SExp {
    rule TOP { <sexp> }

    proto rule sexp {*}
    rule sexp:sym<list>   { '(' <sexp>* ')' }
    rule sexp:sym<nil>    { 'nil' }
    rule sexp:sym<num>    { \d+ }
    rule sexp:sym<symbol> { ':' <[\w\-]>+ }
    rule sexp:sym<string> { '"' <str-content>* '"' }

    token str-content {
        | <-[\"\\]>+         # Any char except " or \
        | '\\"'              # Escaped quote
        | '\\'               # Escaped backslash
    }
}

#| Convert a parsed S-Expression into a list of lists
class SExp::Actions {
    method TOP($/) { make $<sexp>.made }

    method sexp:sym<list>($/) {
        make $<sexp>».made.List
    }
    method sexp:sym<nil>($/)    { make List }
    method sexp:sym<num>($/)    { make $/.Int }
    method sexp:sym<symbol>($/) { make $/.Str.trim }
    method sexp:sym<string>($/) {
        make $<str-content>».made.join
    }

    method str-content($/) {
        make $/.Str.subst(/\\(.)/, {$0}, :g)
    }
}

submethod TWEAK {
    # Start idris2 in IDE mode
    my $ret = Promise.new;
    $!process = Proc::Async.new('idris2', '--ide-mode-socket');
    start {
        react {
            whenever $!process.stdout.lines {
                $!port = $_.Int;
                $!socket = IO::Socket::INET.new(:host<localhost>, :port($!port));
                $ret.keep;
            }
            whenever $!process.start {
                say 'Idris 2 exited, exitcode=', .exitcode, ' signal=', .signal;
                done;
            }
            whenever $!process.ready {
                say 'Idris 2 online, PID=', $_;
            }
        }
    }
    await $ret;
    my ($major, $minor) = self.process-protocol-version;
    my @ret = self.version;
    my @version = @ret[0][1][1][0];
    my $commit = @ret[0][1][1][1][0];
    say "Idris 2 Version: ", @version[0], ".", @version[1], ".", @version[2],
        " (", $commit, ")";
    say "IDE Protocol Version: $major.$minor";
}

#| Capture and parse the initial protocol version message
method process-protocol-version() {
    my @resp = self.read-sexp();
    if @resp[0] eq ':protocol-version' {
        return (@resp[1], @resp[2]);
    }
    die "Expected protocol version, got: ", @resp;
}

#| Read one sexp from the IDE server
method read-sexp() {
    my $len = $!socket.read(6).decode('utf8');
    my $msg = $!socket.read(:bin, $len.parse-base(16)).decode('utf8');
    SExp.parse($msg, actions => SExp::Actions).made
}

#| Send a command to the IDE server, and collect all the responses to that
#| command
#|
#| For convinence, automatically symbolizes the first argument, and will wrap
#| the command in parens if there is more than one argument
method send-command(*@cmd) {
    my $id = ++$!request-id;
    my $cmd-str;
    if @cmd.elems > 1 {
        $cmd-str = "((" ~ (":" ~ @cmd[0]) ~ " " ~ @cmd[1..*].join(" ") ~ ") $id)";
    } else {
        $cmd-str = "(" ~ (":" ~ @cmd[0]) ~ " $id)";
    }
    my $len = sprintf("%06x", $cmd-str.chars);
    $!socket.print($len ~ $cmd-str);

    my @responses;
    loop {
        my $resp = self.read-sexp();
        @responses.push($resp);

        if $resp[1][0] eq ':error' && $resp[2] == $id {
            die "Idris error: ", $resp[1][1];
        }

        if $resp[0] eq ':return' && $resp[2] == $id {
            return @responses;
        }
    }
}

#| Wrapper for :load-file command
method load-file($filename, $line-number?){
    if $line-number {
        self.send-command('load-file', "\"$filename\"", $line-number.Str)
    } else {
        self.send-command('load-file', "\"$filename\"")
    }
}

#| Wrapper for :cd command
method cd($filepath) {
    self.send-command('cd', "\"$filepath\"")
}

#| Wrapper for :interpret command
method interpret($cmd) {
    self.send-command('interpret', "\"$cmd\"")
}

#| Wrapper for :type-of command
method type-of($item) {
    self.send-command('type-of', "\"$item\"")
}

#| Wrapper for :docs-for command
method docs-for($item) {
    self.send-command('docs-for', "\"$item\"")
}

#| Wrapper for :browse-namespace command
#|
#| Will import $namespace before browsing it to ensure we get results
method browse-namespace($namespace) {
    self.interpret: ":import $namespace";
    self.send-command('browse-namespace', "\"$namespace\"")
}

#| Wrapper for :version command
method version() {
    self.send-command('version')
}