### ----------------------------------------------------
### -- App::Rak
### -- Licenses: Artistic-2.0
### -- Authors: Elizabeth Mattijsen
### -- File: lib/App/Rak.rakumod
### ----------------------------------------------------
### -- Chunk 3 of 4
oarVMBytecode('mbc-strings');
    set-action('mbc-strings', $value);
}

my sub option-meta-modified($value --> Nil) {
    set-filesystem-Instant('meta-modified', $value);
}

my sub option-mode($value --> Nil) {
    set-filesystem-Int('mode', $value);
}

my sub option-modifications($value --> Nil) {
    if Bool.ACCEPTS($value) {
        $modifications := $value;
        if $action-for -> $_ {
            meh "Not allowed to make modify files" when 'modify-files';
            meh "Not allowed to rename files"      when 'rename-files';
        }
    }
    else {
        meh "--modifications must be specified as a flag";
    }
}

my sub option-modified($value --> Nil) {
    set-filesystem-Instant('modified', $value);
}

my sub option-modify-files($value --> Nil) {
    meh "Not allowed to make modifications" unless $modifications;
    set-action('modify-files', $value);
}

my sub option-module($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--module' expects the name of a module to load"
      !! @modules.push($value)
}

my sub option-not($value --> Nil) {
    set-additional-pattern('not', $value, '!');
    $seen-initial-pattern := True;
}

my sub option-only-first($value --> Nil) {
    set-listing-flag-or-Int(
      'only-first',
      $value eq '∞' | '*' | 'Inf' ?? 0 !! $value
    );
}

my sub option-or($value --> Nil) {
    set-additional-pattern('or', $value, '');
}

my sub option-ornot($value --> Nil) {
    set-additional-pattern('ornot', $value, '!');
}

my sub option-output-dir($value --> Nil) {
    meh "'--output-dir' expects a directory specification"
      if Bool.ACCEPTS($value);

    my $io := $value.IO;
    meh "Directory '$io' with --output-dir must not exist" if $io.e;

    mkdir $io;
    $io.d
      ?? ($output-dir := $io)
      !! meh "Could not create directory '$output-dir' for --output-dir";
}

my sub option-output-file($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--output-file' expects a file specification"
      !! ($output-file := $value);
}

my sub option-pager($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--pager' expects a program specification"
      !! ($pager = $value);
}

my sub option-paragraph-context($value --> Nil) {
    set-result-flag('paragraph-context', $value);
}

my sub option-passthru($value --> Nil) {
    set-result-flag('passthru', $value);
}

my sub option-passthru-context($value --> Nil) {
    set-result-flag('passthru-context', $value);
}

my sub option-paths($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--paths' must be a path specification, not a flag"
      !! set-source('paths', $value.split(","));
}

my sub option-paths-from($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--paths-from' must be a file specification, not a flag"
      !! set-source('paths-from', $value);
}

my sub option-pattern($value --> Nil) {
    set-additional-pattern('pattern', $value, '');
    $seen-initial-pattern := True;
}

my sub option-patterns-from($value --> Nil) {

    # helper sub for getting pattern(s) from file
    sub read-patterns($handle) {
        if $handle.lines -> @lines {
            add-pattern($_) for @lines;
            $seen-initial-pattern := True;
        }
    }

     if Bool.ACCEPTS($value) {
         meh "'--patterns-from' must be a file specification, not a flag"
     }
     elsif $value eq '-' {
         note "Reading from STDIN, please enter patterns and ^D when done:"
           unless $reading-from-stdin;
        read-patterns($*IN);
    }
    elsif $value.IO.r {
        read-patterns($value.IO);
    }
    else {
        meh "Could not read from '$value' to obtain patterns";
    }
}

my sub option-pdf-info($value --> Nil) {
    check-PDFExtract('pdf-per-file');
    set-action('pdf-info', $value);
}

my sub option-pdf-per-file($value --> Nil) {
    check-PDFExtract('pdf-per-file');
    set-action('pdf-per-file', $value);
}

my sub option-pdf-per-line($value --> Nil) {
    check-PDFExtract('pdf-per-line');
    set-action('pdf-per-line', $value);
}

my sub option-per-file($value --> Nil) {
    if Bool.ACCEPTS($value) {
        set-producer 'per-file', $value;
    }
    orwith convert-to-simple-Callable($value, 'per-file') {
        set-producer 'per-file', $_;
    }
    else {
        meh "Problem compiling code for '--per-file': $value";
    }
}

my sub option-per-line($value --> Nil) {
    if Bool.ACCEPTS($value) {
        set-producer 'per-line', $value, 'produce-many';
    }
    orwith convert-to-simple-Callable($value, 'per-file') {
        set-producer 'per-line', $_, 'produce-many';
    }
    else {
        meh "Problem compiling code for '--per-line': $value";
    }
}

my sub option-per-paragraph($value --> Nil) {
    if Bool.ACCEPTS($value) {
        if $value {
            set-producer(
              'per-line',
              -> $io { paragraphs($io.lines, 1, :Pair(PairContext)) },
              'produce-many-pairs'
            );
        }
    }
    else {
        meh "Can only specify '--per-paragraph' as a flag";
    }
}

my sub option-progress($value --> Nil) {
    my $ERR := PROCESS::<$ERR>;

    multi sub progress() { $ERR.print: (" " x 80) ~ "\r" }
    multi sub progress($p) {
        my $progress := "$p.nr-sources() / $p.nr-items() / $p.nr-matches()";
        $ERR.print: (" " x (80 - $progress.chars)) ~ $progress ~ "\r";
    }

    Bool.ACCEPTS($value)
      ?? $value
        ?? $writing-to-stderr
          ?? (%rak<progress> := &progress)
          !! Nil
        !! Nil
      !! meh "'--progress' must be a flag"
}

my sub option-proximate($value --> Nil) {
    set-listing-flag-or-Int('proximate', $value);
}

my sub option-rakudo-all($value --> Nil) {
    set-rakudo-xxx($value, "all");
}

my sub option-rakudo-c($value --> Nil) {
    set-rakudo-xxx($value, "c");
}

my sub option-rakudo-doc($value --> Nil) {
    set-rakudo-xxx($value, "doc");
}

my sub option-rakudo-java($value --> Nil) {
    set-rakudo-xxx($value, "java");
}

my sub option-rakudo-js($value --> Nil) {
    set-rakudo-xxx($value, "js");
}

my sub option-rakudo-nqp($value --> Nil) {
    set-rakudo-xxx($value, "nqp");
}

my sub option-rakudo-perl($value --> Nil) {
    set-rakudo-xxx($value, "perl");
}

my sub option-rakudo-raku($value --> Nil) {
    set-rakudo-xxx($value, "raku");
}

my sub option-rakudo-shell($value --> Nil) {
    set-rakudo-xxx($value, "shell");
}

my sub option-rakudo-test($value --> Nil) {
    set-rakudo-xxx($value, "test");
}

my sub option-rakudo-yaml($value --> Nil) {
    set-rakudo-xxx($value, "java");
}

my sub option-rename-files($value --> Nil) {
    meh "Not allowed to rename files" unless $modifications;
    set-action('rename-files', $value);
}

my sub option-quietly($value --> Nil) {
    set-rak-flag('quietly', $value);
}

my sub option-quote($value --> Nil) {
    set-csv-value('quote', $value, 'a quote');
}

my sub option-rak($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($debug-rak := $value)
      !! meh "'--rak' must be specified as a flag";
}

my sub option-recurse-symlinked-dir($value --> Nil) {
    set-filesystem-flag('recurse-symlinked-dir', $value);
}

my sub option-recurse-unmatched-dir($value --> Nil) {
    set-filesystem-flag('recurse-unmatched-dir', $value);
}

my sub option-repository($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? meh "'--repository' expects the specification of a repository"
      !! @repos.push($value)
}

my sub option-save($value --> Nil) {
    Bool.ACCEPTS($value) || !$value
      ?? meh "'--save' must be contain name to save for"
      !! (%global<save> := $value);
}

my sub option-sayer($value --> Nil) {
    meh "--sayer option NYI";
}

my sub option-sep($value --> Nil) {
    set-csv-value('sep', $value, 'a seperator');
}

my sub option-shell($value --> Nil) {
    external-execution('shell', $value);
}

my sub option-show-blame($value --> Nil) {
    check-GitBlameFile('show-blame');
    set-result-flag('show-blame', $value);
}

my sub option-show-filename($value --> Nil) {
    set-listing-flag('show-filename', $value);
}

my sub option-show-item-number($value --> Nil) {
    set-result-flag('show-item-number', $value);
}

my sub option-silently($value --> Nil) {
    set-rak-flag('silently', $value);
}

my sub option-smartcase($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($smartcase := $value)
      !! meh "'--smartcase' must be specified as a flag";
}

my sub option-smartmark($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($smartmark := $value)
      !! meh "'--smartmark' must be specified as a flag";
}

my sub option-sourcery($value --> Nil) {
    check-sourcery('sourcery');
    set-result-flag('sourcery', $value);
}

my sub option-stats($value --> Nil) {
    set-rak-flag('stats', $value);
}

my sub option-stats-only($value --> Nil) {
    set-result-flag('stats-only', $value);
}

my sub option-strict($value --> Nil) {
    set-csv-flag('strict', $value);
}

my sub option-summary-if-larger-than($value --> Nil) {
    set-listing-Int('summary-if-larger-than', $value);
}

my sub option-trim($value --> Nil) {
    set-listing-flag('trim', $value);
}

my sub option-type($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($type := Any)
      !! StrType.ACCEPTS($value)
        ?? ($type := $value)
        !! meh "'$value' is not an expected --type";
}

my sub option-uid($value --> Nil) {
    set-filesystem-id('uid', $value);
}

my sub option-under-version-control($value --> Nil) {
    set-filesystem-flag('under-version-control', $value);
}

my sub option-unicode($value --> Nil) {
    set-action('unicode', $value);
}

my sub option-unique($value --> Nil) {
    set-result-flag('unique', $value);
    set-rak-flag('sort', True);
}

my sub option-user($value --> Nil) {
    set-filesystem-name('user', $value, 'getpwnam', 'getpwuid');
}

my sub option-verbose($value --> Nil) {
    Bool.ACCEPTS($value)
      ?? ($verbose := $value)
      !! meh "'--verbose' must be specified as a flag";
}

my sub option-version($value --> Nil) {
    set-action('version', $value);
}

my sub option-vimgrep($value --> Nil) {
    set-action('vimgrep', $value);
}

my sub option-with-line-endings($value --> Nil) {
    set-rak-flag('with-line-endings', $value);
}

#-------------------------------------------------------------------------------
# Subroutines checking applicability of groups of options specified

my sub meh-output-file($name --> Nil) is hidden-from-backtrace {
    meh "Specifying --output-file is incompatible with --$name" if $output-file;
}

my sub meh-pager($name --> Nil) is hidden-from-backtrace {
    meh "Using a pager is incompatible with --$name" if $pager;
}

my sub meh-what($name, %hash, $description --> Nil) is hidden-from-backtrace {
    if %hash.map({ .key if .value }) -> @keys {
        meh qq:to/MEH/.chomp;
These $description options are incompatible with --$name:
@keys.sort.map({"--$_"})
MEH
    }
}

my sub meh-csv($name --> Nil) is hidden-from-backtrace {
    meh-what($name, %csv, 'CSV')
}

my sub meh-listing($name --> Nil) is hidden-from-backtrace {
    meh-what($name, %listing, 'listing')
}

my sub meh-filesystem($name --> Nil) is hidden-from-backtrace {
    meh-what($name, %filesystem, 'filesystem');
}

my sub meh-modify($name --> Nil) is hidden-from-backtrace {
    meh-what($name, %modify, 'modify')
}

my sub meh-result($name --> Nil) is hidden-from-backtrace {
    meh-what($name, %result, 'result');
}

my sub meh-only($name --> Nil) is hidden-from-backtrace {
    meh "'--$name' must be the only option"
      if %filesystem
      || %listing
      || %global
      || %csv;
}

my sub meh-for($option, *@mehs) is hidden-from-backtrace {
    ::("&meh-$_")($option) for @mehs;
}

my sub maybe-meh-together(*@keys) is hidden-from-backtrace {
    if @keys > 1 {
        meh "Cannot specify &mm(@keys.skip) with --@keys.head()";
    }
}

my sub move-filesystem-options-to-rak(--> Nil) {
    if $reading-from-stdin {
        meh "Cannot use &mm(%filesystem.keys.sort) when reading from STDIN"
          if %filesystem;
        %listing<show-filename> := False
          if $source-for && !$source-for.ends-with("-from");
    }
    elsif %filesystem {
        if %filesystem<under-version-control> {
            maybe-meh-together 'under-version-control', %filesystem<
              dir file recurse-symlinked-dir recurse-unmatched-dir
              auto-decompress
            >:k;
        }
        else {
            if %filesystem<file>:exists {
                maybe-meh-together 'file', %filesystem<
                  extensions
                >:k;
                my $file := %rak<file> := %filesystem<file>:delete;
                meh "--/file only makes sense when used together with --find"
                  if !$file && !%result<find>;
            }
            elsif %filesystem<extensions>:delete -> $seen {
                %rak<file> := $seen;
            }

            if %filesystem<auto-decompress>:delete {
                %rak<ioify> := &IOAD;

                my &old-filter :=
                  %rak<file> // codify-extensions @known-extensions;
                %rak<file> := {
                    my str $extension = extension($_);
                    $extension eq 'gz'
                      ?? old-filter(.chop(3))
                      !! $extension eq 'bz2'
                        ?? old-filter(.chop(4))
                        !! old-filter($_)
                }
            }
        }

        if %filesystem<user>:delete -> $uid {
            maybe-meh-together %filesystem<user uid>:k;
            maybe-meh-together 'user', %filesystem<uid>:k;
            %rak<uid> := $uid;
        }

        if %filesystem<group>:delete -> $gid {
            maybe-meh-together 'group', %filesystem<gid>:k;
            %rak<gid> := $gid;
        }

        %rak ,= %filesystem;
        %filesystem = ();
    }

    %rak<is-text> := True
      unless $reading-from-stdin || (%rak<
        file is-text under-version-control is-pdf is-moarvm
      >:k) || %result<find>;
}

my sub move-result-options-to-rak(--> Nil) {

    if %result {
        if %result<
          context after-context before-context
          paragraph-context passthru-context
        >:k -> @contexts {
            maybe-meh-together @contexts
              unless @contexts (==) <after-context before-context>;
            %listing<trim> := False if %listing<trim>:!exists;
        }

        maybe-meh-together %result<unique frequencies>:k;
        maybe-meh-together %result<count-only stats-only>:k;

        # Seem to only want file information
        if %result<files-with-matches files-without-matches>:k -> @keys {
            maybe-meh-together @keys;
            maybe-meh-together @keys, %result<
              max-matches-per-file frequencies unique
            >:k;
            maybe-meh-together @keys, %result<invert-match>:k;

            my $with := do if %result<files-with-matches>:delete {
                %rak<sources-only> := True;
                'with'
            }
            elsif %result<files-without-matches>:delete {
                %rak<sources-without-only> := True;
                'without'
            }

            # Only interested in number of files
            if %result<count-only>:delete {
                my int $seen;
                %rak<eager>  := True;
                %rak<mapper> := -> $_ --> Empty {
                    LAST sayer $seen == 0
                      ?? "No files $with matches"
                      !! $seen == 1
                        ?? "One file $with matches"
                        !! "$seen files $with matches";
                    ++$seen;
                }
            }

            # Need to separate files with a null-byte
            elsif %result<file-separator-null>:delete {
                # Set way to stringify paths
                my &stringify :=
                  IO::Path.^find_method(%listing<absolute>:delete
                    ?? "absolute"
                    !! "relative"
                  );

                my str @files;
                %rak<eager>  := True;
                %rak<mapper> := -> $_ --> Empty {
                    LAST sayer @files.join("\0");
                    @files.push: stringify($_);
                }
            }
        }

        # Want all information
        else {
            if %result<max-matches-per-file>:delete -> $max {
                %rak<max-matches-per-source> := $max;
            }

            if %result<show-item-number>:exists {
                %rak<omit-item-number> := True
                  unless %result<show-item-number>:delete;
            }

            if %result<find>:delete {
                %rak<find>             := True;
                %rak<omit-item-number> := True
                  unless %result<frequencies classify categorize>:k;

                # Only interested in number of files
                if %result<count-only>:delete {
                    my int $seen;
                    %rak<eager>  := True;
                    %rak<mapper> := -> $, @files --> Empty {
                        LAST sayer $seen == 0
                          ?? "No files"
                          !! $seen == 1
                            ?? "One file"
                            !! "$seen files";
                        $seen = @files.elems;
                    }
                }
            }

            # Only interested in number of matches / files
            elsif %result<count-only>:delete {
                %rak<eager>  := True;

                if %result<unique>:delete {
                    %rak<sort>:delete;
                    %rak<omit-item-number> := True;

                    my int $nr-files;
                    my @uniques;

                    %rak<mapper> := -> $, @matches --> Empty {
                        LAST {
                            my int $nr-uniques = @uniques.unique.elems;
                            sayer $verbose
                              ?? "$nr-uniques unique occurrenc&es($nr-uniques) in $nr-files file&s($nr-files)"
                              !! $nr-uniques;
                        }

                        ++$nr-files;
                        @uniques.append: @matches.unique;
                    }
                }
                else {
                    # Set way to stringify paths
                    my &stringify :=
                      IO::Path.^find_method(%listing<absolute>:delete
                        ?? "absolute"
                        !! "relative"
                      );

                    my @files;
                    %rak<mapper> := -> $io, @matches --> Empty {
                        LAST {
                            if $verbose {
                                sayer "$_.key() has $_.value() match{"es" if .value > 1}"
                                  for @files;
                            }
                            my int $nr-matches = @files.map(*.value).sum;
                            my int $nr-files   = @files.elems;
                            sayer $nr-files
                              ?? "$nr-matches match&es($nr-matches) in $nr-files file&s($nr-files)"
                              !! "No files with matches";
                        }
                        @files.push: Pair.new:
                          $reading-from-stdin ?? '<STDIN>' !! stringify($io),
                          @matches.elems;
                    }
                }
            }

            # Wanna blame
            elsif %result<show-blame>:delete {
                %rak<mapper> := -> $io, @matches {
                    my @line-numbers = @matches.map: *.key;
                    with $GitBlameFile.new($io, :@line-numbers) -> $blamer {
                        $io => $blamer.lines.Slip
                    }
                    else {
                        $io => @matches.map({ .key ~ ':' ~ .value }).Slip
                    }
                }
            }
        }

        %rak ,= %result;
        %result = ();
    }
}

my sub activate-output-options() {
    $pager = %*ENV<RAK_PAGER> unless $pager.defined;
    if $pager {
        meh "Cannot specify a pager and an output-file"
          if $output-file && $output-file ne '-';
        $pager = "$pager -r" if $pager eq "less" | "more";  # ensure raw
        $*OUT = (run $pager.words, :in).in(:bin)
    }
    elsif $output-file {
        $*OUT = open($output-file, :w) if $output-file ne '-';
    }
}

#-------------------------------------------------------------------------------
# Actions.  These are subroutines that start with "action-" and as such handle
# the associated action arguments.  These all have access to the variables set
# up during option processing.

my sub action-blame-per-file(--> Nil) {
    meh-for 'blame-per-file', <csv modify>;

    prepare-needle;
    %filesystem<under-version-control> := True;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;

    %listing<group-matches> := False if %listing<group-matches>:!exists;
    %listing<has-break>     := False if %listing<has-break>:!exists;

    %rak<batch> := 1;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-blame-per-line(--> Nil) {
    meh-for 'blame-per-line', <csv modify>;

    prepare-needle;
    %filesystem<under-version-control> := True;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;

    %rak<batch> := 1;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-checkout(--> Nil) {
    meh-for 'checkout', <output-file pager filesystem modify csv>;

    prepare-needle;

    %rak<sources>          := 'checkout';
    %rak<omit-item-number> := True;
    %rak<map-all>          := True;

    %rak<mapper> := -> $, @matches --> Empty {
        my $pattern := @patterns.head;

        if @matches {
            if @matches == 1 {
                run 'git', 'checkout', @matches.head;
            }

            # one of the branches is an exact match
            elsif $pattern (elem) @matches {
                run 'git', 'checkout', $pattern;
            }

            else {
                sayer "Found @matches.elems() branches matching '"
                  ~ BON ~ $pattern ~ BOFF ~ "':";
                sayer "  $_" for @matches;
            }
        }

        # Special casing of master / main confusion
        elsif $pattern eq 'master' {
            run <git checkout main>;
        }
        elsif $pattern eq 'main' {
            run <git checkout master>;
        }
        else {
            sayer "No branch found with '" ~ BON ~ $pattern ~ BOFF ~ "'.";
            my @branches = branches();
            if $verbose {
                sayer "@branches.elems() branches known:";
                sayer "  $_" for @branches;
            }
            else {
                sayer "@branches.elems() branches known, add --verbose to see them";
            }
        }
    }

    run-rak(:eagerly);
    rak-stats;
}

my sub action-csv-per-line(--> Nil) {
    meh-for 'csv-per-line', <modify>;

    prepare-needle;
    %filesystem<file> //= codify-extensions %exts<#csv>
      unless $reading-from-stdin;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;

    %csv<auto-diag> := True unless %csv<auto-diag>:exists;

    # Set up args for producer
    $headers := %csv<headers>:delete // True;
    %CSV      = %csv{}:p:delete;

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-eco-meta(--> Nil) {
    meh-for 'eco-meta', <csv modify filesystem>;

    my $dir := ($*HOME // $*TMPDIR).add('.zef').add('store');
    %rak<sources> := %result<eco-meta>:delete.map: {
        $dir.add($_).add("$_.json")
    }
    %listing<show-filename> := False
      if %listing<show-filename>:!exists;

    move-result-options-to-rak;
    %rak<omit-item-number> := True
      unless %rak<unique frequencies omit-item-number>:k;

    $enc := %rak<encoding>:delete // 'utf8-c8';

    prepare-needle;
    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-edit(--> Nil) {
    %rak<progress>:delete;  # no progress indicator if editing

    my $pattern := @patterns.head.value;

    my sub go-edit-error($error --> Nil) {
        if backtrace-files($error).map: -> (:key($file), :value(@line)) {
            @line.map({ Pair.new: $file, $_}).Slip
        } -> @result {
            edit-files @result.List;
        }
#        note $error.chomp;
    }

    if %result<sourcery>:delete {
        meh-for 'edit', <output-file pager result filesystem modify csv>;
        edit-files sourcery $pattern.trim;
        return;
    }

    elsif %result<backtrace>:delete {
        meh-for 'edit', <output-file pager result filesystem modify csv>;

        $reading-from-stdin
          ?? go-edit-error($*IN.slurp(:close))
          !! @positionals
            ?? meh "Can only specify a single file with a backtrace."
            !! $pattern.IO.e
              ?? go-edit-error($pattern.IO.slurp)
              !! meh "handling backtrace from file(s) NYI";
        return;
    }

    elsif %result<execute-raku>:delete -> $raku {
        if Bool.ACCEPTS($raku) {
            meh "Must specify name of file with Raku code to execute"
              unless $pattern;

            my $io := $pattern.IO;
            meh "File '$pattern' is not found or not readable"
              unless $io.r;

            my $proc := run $*EXECUTABLE,
              ('--ll-exception' if $verbose), $io, @positionals, :out, :err;
            if $proc.err.slurp(:close) -> $backtrace {
                go-edit-error($backtrace);
                return;
            }
            elsif $proc.out.slurp(:close) -> $output {
                meh $output;
            }
            else {
                meh "No error or output produced";
            }
        }
        else {
            meh "Can only edit backtraces from actual file";
        }
    }

    %rak<max-matches-per-source> := $_
      with %result<max-matches-per-file>:delete;
    my $find                  := %result<find>:delete;
    my $files-with-matches    := %result<files-with-matches>:delete;
    my $files-without-matches := %result<files-without-matches>:delete;

    meh-for 'edit', <output-file pager result modify csv>;

    prepare-needle;
    move-filesystem-options-to-rak;
    my $editor := Bool.ACCEPTS($action) ?? Any !! $action;

    # Find filenames to edit
    if $find {
        %rak<find>             := True;
        %rak<omit-item-number> := True;
        %rak<mapper> := -> $, @files --> Empty {
            edit-files @files, :$editor;
        }
    }

    # Use filenames of (non-)matches
    elsif $files-with-matches || $files-without-matches {
        %rak<sources-only>         := True if $files-with-matches;
        %rak<sources-without-only> := True if $files-without-matches;

        my @files;
        %rak<mapper> := -> $io --> Empty {
            LAST edit-files @files, :$editor;
            @files.push: $io.relative;
        }
    }

    # Look for locations in files to edit
    else {
        my @files;
        %rak<mapper> := -> $source, @matches --> Empty {
            LAST edit-files @files, :$editor;

            my $path   := $source.relative;
            my $target := Regex.ACCEPTS($needle) ?? $needle !! $pattern;

            @files.append: @matches.map: {
                $path => .key => (columns(
                  .value, $target,
                  :$ignorecase, :$smartcase, :$ignoremark, :$smartmark,
                  |(:$type if $type)
               ).head // 0)
            }
        }
    }

    run-rak(:eagerly);
    rak-stats;
}

my sub action-help(--> Nil) {
    # nothing to do
    return if Bool.ACCEPTS($action) && !$action;

    meh-for 'how', <csv modify filesystem result>;

    my constant @sections = <
      argument code content debugging examples faq filesystem general
      haystack item listing option pattern philosophy resource result
      special string
    >;

    class Hows {
        my sub paragraphize($name) {
            my @paragraphs;
            my @lines;
            for %?RESOURCES{"help/$name.txt"}.lines -> $line {
                if (($line.ends-with(':')
                     && !($line eq 'Example:' | 'Examples:'))
                ) || $line.starts-with('--')
                  || $line.starts-with('Q: ') {
                    @paragraphs.push: @lines.join("\n") if @lines;
                    @lines = $line;
                }
                else {
                    @lines.push: $line;
                }
            }
            @paragraphs.push: @lines.join("\n") if @lines;
            @paragraphs ?? @paragraphs.Slip !! Empty
        }

        method lines() {
            Bool.ACCEPTS($action)
              ?? (@sections.map: &paragraphize)
              !! $action (elem) @sections
                ?? paragraphize($action)
                !! meh "'$action' is not a recognized help subject";
        }
    }

    sub highlight-header($_, $first?) {
        $first
          || .starts-with('--')
          || .starts-with('Q: ')
          || (.ends-with(':') && !.starts-with(' '))
          ?? BON ~ $_ ~ BOFF
          !! $_
    }

    activate-output-options;

    # let's produce the necessary help and search in it / paragraph
    if @patterns.head -> $pattern {
        %rak<sources>          := (Hows,);
        %rak<omit-item-number> := True;
        %rak<degree>           := 1;

        %rak<map-all> := True;
        %rak<mapper>  := -> $, @matches {
            '<how>' => ((@matches
              ?? "Found @matches.elems() match&es(@matches) for '$pattern':"
              !! "Nothing found for '$pattern'"
            ), @matches.map({
                ('-' x 80)
                  ~ "\n"
                  ~ ($writing-to-stdout
                      ?? .lines.map({
                             my $result;
                             FIRST $result = highlight-header($_);
                             $result // $_
                         }).join("\n") ~ "\n"
                      !! $_
                    )
            }).Slip).Slip
        }

        prepare-needle;
        run-rak;

        %listing<show-filename> := False unless %listing<show-filename>:exists;
        %listing<trim>          := False unless %listing<trim>:exists;
        rak-results;
        rak-stats;
    }

    # no pattern, generic help request
    else {
        my $text := do if Bool.ACCEPTS($action) {
            %?RESOURCES<help.txt>.slurp(:close)
        }
        elsif $action {
            meh "'$action' is not a recognized help subject"
              unless $action (elem) @sections;
            %?RESOURCES{"help/$action.txt"}.slurp(:close)
        }

        if $text {
            my $SCRIPT := $*PROGRAM.basename;
            my $header := "$SCRIPT - " ~ DESCRIPTION;
            my str @parts;
            @parts.push: $header;
            @parts.push: "-" x $header.chars;
            @parts.push: $writing-to-stdout
              ?? $text.lines.map({
                     highlight-header($_, !$++)  # always first line
                 }).join("\n")
              !! $text;

            if $verbose {
                @parts.push: "";
                @parts.push: CREDITS;
                @parts.push: "";
                @parts.push: "Thank you for using $SCRIPT!";
            }

            sayer @parts.join("\n");
        }
    }
}

my sub action-json-per-elem(--> Nil) {
    meh-for 'json-per-elem', <csv modify>;

    # Normal json-per-elem handling
    %filesystem<file> //= codify-extensions %exts<#json>
      unless $reading-from-stdin;
    move-filesystem-options-to-rak;

    move-result-options-to-rak;
    %rak<omit-item-number> := True
      unless %rak<unique frequencies omit-item-number>:k;

    $enc := %rak<encoding>:delete // 'utf8-c8';

    prepare-needle;
    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

my sub action-json-per-file(--> Nil) {
    meh-for 'json-per-file', <csv modify>;

    prepare-needle;
    %filesystem<file> //= codify-extensions %exts<#json>
      unless $reading-from-stdin;
    move-filesystem-options-to-rak;
    move-result-options-to-rak;

    $enc := %rak<encoding>:delete // 'utf8-c8';

    activate-output-options;
    run-rak;
    rak-results;
    rak-stats;
}

