#!/usr/bin/env rakupp
# webraku — build and run a Webraku application.
#
#   webraku build app.raku          # write app.client.js beside it
#   webraku run   app.raku [--port=8080] [--dev]
#   webraku bundle app.raku         # print the assembled Raku, without transpiling
#
# `build` is the step that makes one file into two programs. It concatenates the
# framework's browser half with the application, replaces every `is server` body
# with a call to the RPC stub, appends the mount call, and hands the result to
# `rakupp --target=js --standalone`.
use lib $?FILE.IO.parent.parent.add('lib').Str;
use Webraku::Bundle;

# `webraku run app.raku --port=8080` is how the README writes it; without this,
# both engines insist on the options coming before the command.
my %*SUB-MAIN-OPTS = :named-anywhere;

# In a checkout the application is run with `-I` this lib; installed, Webraku
# is in the store and there is nothing to add.
my $LIB = $?FILE.IO.parent.parent.add('lib');

# The transpiler is Raku++'s, so both halves go to it even when zef installed
# this script with a Rakudo wrapper. RAKUPP names a particular binary.
my $ENGINE = $*RAKU.compiler.name eq 'Raku++' ?? $*EXECUTABLE.Str !! (%*ENV<RAKUPP> // 'rakupp');

sub part(Str $rel --> Str) { strip-uses(framework-source($rel)) }

#| The whole browser program, as one file of Raku.
sub assemble(IO::Path $app --> Str) {
    my $src = $app.slurp;
    my @out;
    @out.push: '# Generated by `webraku build` — do not edit.';
    @out.push: "# The browser half of {$app.basename}: the framework, then the program with";
    @out.push: '# every `is server` body replaced by the call that fetches it.';
    @out.push: 'use js;';
    @out.push: '';
    @out.push: part('Webraku/HTML.rakumod');
    @out.push: part('Webraku/Route.rakumod');
    @out.push: part('Webraku/JSON.rakumod');
    @out.push: part('client/Client.rakumod');
    @out.push: part('client/Rpc.rakumod');
    @out.push: '';
    @out.push: '# ---- the application ----';
    # `serve` and `stylesheet` belong to the server; the browser never runs them.
    # `page` is kept, and the mount below picks the one matching the URL.
    # Stub first: whether the browser needs a module beside the application
    # depends on what is left to call it once the `is server` bodies are gone.
    my $stubbed = stub-server-subs($src);
    my $client = strip-uses($stubbed, beside => $app.parent, client => $stubbed);
    $client = drop-server-calls($client);
    @out.push: $client;
    @out.push: '';
    @out.push: '# ---- mount ----';
    @out.push: 'webraku-hydrate();';
    @out.join("\n");
}

#| Remove the statements that only make sense on the server: `serve …;` and
#| `stylesheet …;`. A dropped line may OPEN A HEREDOC — `stylesheet q:to/CSS/;`
#| is how a page's styles are written — and the body then has to go with it, or
#| the CSS is left behind as loose Raku and the file stops parsing at the end.
sub drop-server-calls(Str $src --> Str) {
    my @out;
    my @lines = $src.lines;
    my $i = 0;
    while $i < @lines {
        my $l = @lines[$i];
        my $server-only = $l ~~ / ^ \s* 'serve' <[\s;(:]> /
                       || $l ~~ / ^ \s* 'stylesheet' <[\s;(]> /;
        unless $server-only {
            @out.push: $l;
            $i++;
            next;
        }
        @out.push: '# (a server-only statement was dropped here)';
        # `q:to/MARK/`, `qq:to/MARK/`, `Q:to[MARK]` — take the body and its
        # terminator too.
        if $l ~~ / <[qQ]> <[qQ]>? ':to' <[\/\[\{\|]> \s* (<[\w\-]>+) / {
            my $mark = ~$0;
            $i++;
            while $i < @lines && @lines[$i].trim ne $mark { $i++ }
        }
        $i++;
    }
    @out.join("\n");
}

sub client-js(IO::Path $app, Bool :$keep-raku = False --> Str) {
    my $bundle = assemble($app);
    my $tmp = $app.parent.add($app.basename.subst(/ '.raku' $ /, '') ~ '.client.raku');
    $tmp.spurt: $bundle;
    my $out = $app.parent.add($app.basename.subst(/ '.raku' $ /, '') ~ '.client.js');
    # `-I` the application's directory: a module beside it that the browser's
    # code `use`s is compiled into the bundle from there.
    my $p = run $ENGINE, '--target=js', '--standalone', '-I', $app.parent.Str, $tmp.Str, '-o', $out.Str, :out, :err;
    my $err = $p.err.slurp(:close);
    $p.out.slurp(:close);
    unless $p.exitcode == 0 {
        note $err;
        note "webraku: the browser half did not transpile. The assembled Raku is at $tmp";
        exit 1;
    }
    $tmp.unlink unless $keep-raku;
    $out.slurp;
}

multi sub MAIN('build', Str $app, Bool :$keep-raku = False) {
    my $f = $app.IO;
    die "webraku: no such file $app" unless $f.e;
    my $js = client-js($f, :$keep-raku);
    say "webraku: {$f.basename.subst(/'.raku'$/, '')}.client.js — {$js.chars} characters";
}

multi sub MAIN('bundle', Str $app) {
    print assemble($app.IO);
}

multi sub MAIN('run', Str $app, Int :$port = 8080, Bool :$dev = False, Bool :$keep-raku = False) {
    my $f = $app.IO;
    die "webraku: no such file $app" unless $f.e;
    my $js = client-js($f, :$keep-raku);
    # The application declares its routes when it is run, and its own `serve`
    # call starts the server. It is handed the bundle through the environment
    # so that the program itself stays free of build concerns.
    %*ENV<WEBRAKU_CLIENT_JS_FILE> = $f.parent.add($f.basename.subst(/'.raku'$/, '') ~ '.client.js').Str;
    %*ENV<WEBRAKU_PORT> = $port.Str;
    %*ENV<WEBRAKU_DEV>  = $dev ?? '1' !! '';
    my $p = run $ENGINE, |('-I', $LIB.Str if $LIB.add('Webraku.rakumod').e), $f.Str;
    exit $p.exitcode;
}

multi sub MAIN() {
    say q:to/USAGE/;
    webraku — build and run a Webraku application

      webraku build  app.raku              write app.client.js beside it
      webraku run    app.raku [--port=N] [--dev]
      webraku bundle app.raku              print the assembled browser Raku

    --dev       reload the page when the program changes
    --keep-raku keep the assembled .client.raku, to read or to transpile by hand
    USAGE
}
