#!/usr/bin/env raku

# Disable output buffering
# See https://stackoverflow.com/q/63486337/807650
.out-buffer = 0 for $*OUT, $*ERR;

# For example
#
# lorea --regex api.yml ./tools/build-doc

constant IGNORE = (
    rx{ [ ^ || '/' ] '.' \w }, # Hidden files or directories
    rx{ '.sw' <[px]> $ },      # Entries ending in .swp or .swx
    rx{ '~' $ },               # Entries ending in ~
);

need Getopt::Long;

my %OPTS;
sub ARGS-TO-CAPTURE (|c) {
    given Getopt::Long::ARGS-TO-CAPTURE(|c) {
        %OPTS = .hash;
        .return;
    }
}

multi sub MAIN (
    Bool :$version, #= Print the version of the command and compiler
) {
    use App::Lorea::Command;
    say "v{ App::Lorea::Command.^ver } on Raku {
        $*VM.config< versionmajor versionminor >.join: '.'
    }";
}

multi sub MAIN (
             *@,                  #= The command to execute and its arguments
             :r(:@regex),         #= A regular expression to match filenames
             :R(:@ignore),        #= A regular expression to ignore filenames
    Bool     :$start-service,     #= Run command as a service
    IO::Path :c(:$config),        #= The path to a configuration file
    Bool     :v(:$verbose),       #= Print tracing information
    Bool     :$help,              #= Display usage information
    Bool     :$all,               #= Include all normally ignored files
    Str      :$substitute = '{}', #= Set the string to be used as placeholder
) {
    use App::Lorea::Watcher;

    die '--config is only compatible with --verbose'
        if $config && %OPTS{ %OPTS.keys.grep: { ! /config|verbose/ } };

    $*USAGE.say and return if $help || '-?' (&) @*ARGS || !( $config || @*ARGS );

    my @runs = $config ?? $config.&parse-config
        !! \( @*ARGS, :@regex, :@ignore, :$start-service, :$all, :$substitute );

    my @exclude;
    @exclude = IGNORE unless any @runs.map: { .Hash<all>.so };

    if $verbose {
        say "Globally ignoring on : { .raku }" for @exclude;
    }

    my $supply = $*CWD.&watch-recursive( debounce => 0.5, :@exclude );

    $supply.&run-command(|$_) for @runs;

    $supply.wait;
}

# Takes an IO::Path representing the config file to parse, and returns a list
# of captures resulting from parsing it. Each line in the config file should
# be a list of options and arguments just like those that would be passed
# during normal execution (with the exception of the `--help` and
# `--config` flags, which are not allowed in config files).
#
# Lines starting with "#" are ignored. Commands can span multiple lines if
# they end with a backslash (`\`) or if they are part of a multiline string
# (as per C<bash> quoting rules).
#
# This means that each capture returned by this function represents, rather
# than a "line", a "statement".
#
my sub parse-config (
    IO::Path $config,
) {
    use Text::ShellWords;

    my @statements = gather {
        my $buffer;
        for $config.lines -> $line is copy {
            $buffer ~= $line;

            my @args = $buffer.&shell-words;
            unless @args.grep: Failure {
                $buffer = '';

                @args .= grep: *.Str.chars > 0;
                take @args;
            }

            LEAVE $buffer ~= "\n";

            LAST take @args if @args.grep: Failure;
        }
    }

    gather for @statements -> @args is copy {
        my %hash;
        Getopt::Long.new-from-sub(&MAIN)
            .get-options(@args, :%hash, :write-args(@args));

        die "--$_ is not allowed in config" if %hash{$_}
            for < config help >;

        next unless @args;
        next if @args.head.starts-with: '#';

        take \( @args, |%hash );
    }
}

my sub run-command (
          $supply is copy, #= A supply to tap into
          @args,           #= The command to execute and its arguments
         :@regex,          #= A regular expression to match filenames
         :@ignore,         #= A regular expression to ignore filenames
    Bool :$all,            #= No-op, here to ignore this argument
    Bool :$start-service,  #= Run command as a service
    Str  :$substitute      #= The string to use as placeholder
) {
    use App::Lorea::Command;
    use App::Lorea::Watcher;

    die "Must have a command to execute" unless @args;

    $supply.tap: *.note if %OPTS<verbose>;

    if %OPTS<verbose> {
        say '-' x 10;
        say "Command: { @args.join: ' ' }";

        for @regex {
            FIRST say 'Matching on:';
            say "* { .raku }";
        }

        for @ignore {
            FIRST say 'Ignoring on:';
            say "* { .raku }";
        }

        say '-' x 10;
    }

    @regex .= map: { rx{ <$_> } };

    $supply .= grep: {
        .path.&App::Lorea::Watcher::normalise ~~ any @regex
    } if @regex;

    .run with App::Lorea::Command.new:
        @args, :$supply, :$substitute, is-service => $start-service;

    return;
}
