### ----------------------------------------------------
### -- rak
### -- Licenses: Artistic-2.0
### -- Authors: Elizabeth Mattijsen
### -- File: lib/rak.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
        }
        elsif %n<frequencies>:delete {
            $frequencies := $eager := True;
            $item-number := $max-matches-per-source := False;
        }
        elsif %n<classify>:delete -> &classifier {
            $classify    := &classifier;
            $eager       := True;
            $item-number := $max-matches-per-source := False;
        }
        elsif %n<categorize>:delete -> &categorizer {
            $categorize  := &categorizer;
            $eager       := True;
            $item-number := $max-matches-per-source := False;
        }
        elsif %n<produce-one>:exists {
            $item-number := False;
        }
        else {
            $item-number := !(%n<omit-item-number>:delete);
        }
    }

    # Step 3: producer Callable
    my &producer := do if (%n<produce-one>:delete) -> $produce-one {
          # no item numbers produced ever
          -> $source {
              (my $producer := $produce-one($source)) =:= Nil
                ?? ()
                !! ($producer,)
          }
    }
    elsif (%n<produce-many-pairs>:delete)<> -> $produce-many-pairs {
        $item-number := True;
        $produce-many-pairs
    }
    elsif (%n<produce-many>:delete)<> -> $produce-many {
        $item-number
          ?? -> $source {
                 my uint $line-number;
                 $produce-many($source).map: {
                     PairContext.new: ++$line-number, $_
                 }
             }
          # no item numbers produced
          !! $produce-many
    }
    elsif %n<find>:delete {
        my $seq := $sources-seq<>;
        $sources-seq = ("<find>",);
        my uint $line-number;
        $item-number
          ?? -> $ { $seq.map: { PairContext.new: ++$line-number, $_ } }
          !! -> $ { $seq }
    }
    else {
        my $chomp := !(%n<with-line-endings>:delete);
        my uint $ok-to-hyper = !$any-context;
        -> $source {
            my $is-IO := IO::Path.ACCEPTS($source);

            # It's a Path and it exists and is readable, or not a path
            if ($is-IO && $source.r) || !$is-IO {
                my $seq := $source.lines(:$chomp, :$enc);

                # Make sure we have item numbering if so requested
                my uint $line-number;
                $seq := $seq.map({ PairContext.new: ++$line-number, $_ })
                  if $item-number;

$seq

                # Hyperize if it is a path
#                $is-IO && $ok-to-hyper
#                  ?? $seq.&hyperize(2048, $degree)
#                  !! $seq.Seq   # XXX why???
            }
        }
    }

    # Step 4: matching logic
    # The matcher Callable should take a haystack as the argument, and
    # call the pattern with that.  And optionally do some massaging to make
    # sure we get the right thing.  But in all other aspects, the matcher
    # has the same API as the pattern.
    my &matcher = &pattern =:= &defined
      ?? &defined
      !! make-matcher(
           Regex.ACCEPTS(&pattern) ?? *.contains(&pattern) !! &pattern,
           %n
         );

    # Want to have pair of old/new
    if %n<old-new>:delete {
        my &old-matcher = &matcher;
        &matcher = -> $old {
            my $new := old-matcher($old);
            unless Bool.ACCEPTS($new) || $new =:= Empty || $new =:= Nil {
                Pair.new($old, $new) unless $new eqv $old;
            }
        }
    }

    # Stats keeping stuff: these lexical variables will be directly accessible
    # through the Rak object that is returned.  This is ok to do, since each
    # call to the "rak" subroutine will result in a unique Rak object, so
    # there's no danger of these lexicals seeping into the wrong Rak object
    my $stats;
    my $stats-only;
    my atomicint $nr-sources;
    my atomicint $nr-items;
    my atomicint $nr-matches;
    my atomicint $nr-passthrus;
    my atomicint $nr-changes;

    # Progress monitoring class, to be passed on when :progress is specified
    my $progress := %n<progress>:delete;
    my $cancellation;
    class Progress {
        method nr-sources()   { ⚛$nr-sources   }
        method nr-items()     { ⚛$nr-items     }
        method nr-matches()   { ⚛$nr-matches   }
        method nr-passthrus() { ⚛$nr-passthrus }
        method nr-changes()   { ⚛$nr-changes   }
    }

    # The result class returned by the rak call
    class Rak {
        has $.result    is built(:bind) = Empty;  # search result
        has $.completed is built(:bind) = False;  # True if done already
        has $.exception is built(:bind) = Nil;    # What was thrown

        # Final stats access
        method stats() {
            $stats || $stats-only
              ?? Map.new((
                  :$nr-changes, :$nr-items, :$nr-matches,
                  :$nr-passthrus, :$nr-sources
                 ))
              !! BEGIN Map.new
        }

        # Final progress access
        method nr-sources()   { ⚛$nr-sources   }
        method nr-items()     { ⚛$nr-items     }
        method nr-matches()   { ⚛$nr-matches   }
        method nr-passthrus() { ⚛$nr-passthrus }
        method nr-changes()   { ⚛$nr-changes   }

        method stop-progress() {
            if $cancellation {
                $cancellation.cancel;
                $progress();  # indicate we're done to the progress logic
            }
        }
    }

    # Only interested in counts, so update counters and remove result
    if $stats-only := %n<stats-only>:delete {
        my &old-matcher = &matcher;
        &matcher = -> $_ {
            ++⚛$nr-items;
            my $result := old-matcher($_);
            if $result =:= Empty || $result eqv $_ {
                ++$nr-passthrus;
            }
            elsif Bool.ACCEPTS($result) {
                ++⚛$nr-matches if $result;
            }
            else {
                ++⚛$nr-matches;
                ++⚛$nr-changes;
            }
            Empty
        }
    }

    # Add any stats keeping if necessary
    elsif ($stats := %n<stats>:delete) || $progress {
        my &old-matcher = &matcher;
        &matcher = -> $_ {
            ++⚛$nr-items;
            my $result := old-matcher($_);
            if $result =:= Empty || $result eqv $_ {
                ++$nr-passthrus;
            }
            elsif Bool.ACCEPTS($result) {
                ++⚛$nr-matches if $result;
            }
            else {
                ++⚛$nr-matches;
                ++⚛$nr-changes;
            }
            $result
        }
    }

    # Step 5: contextualizing logic
    # The runner Callable should take a PairContext object as the argument,
    # and call the matcher with that.  If the result is True, then it should
    # produce that line as a PairMatched object with the original value, and
    # any other items as as PairContext objects.  If the result is False, it
    # should produce Empty.  In any other case, it should produce a
    # PairMatched object with the original key, and the value returned by
    # the matcher as its value.  To make sure each source gets its own
    # closure clone, the runner is actually a Callable returning the actual
    # runner code Callable.
    my &runner := do if $stats-only {
        $eager := True;
        # simplest runner for just counting
        -> $source { make-runner $source, &matcher, $item-number }
    }

    # We have some context specification
    elsif $any-context {

        # Special passthru contexts
        if $passthru {
            -> $source {
                make-passthru-runner $source, &matcher, $item-number
            }
        }
        elsif $also-first-context -> $items {
            -> $source {
                make-also-first-runner $source, &matcher, $items, $item-number
            }
        }
        elsif $always-first-context -> $items {
            -> $source {
                make-always-first-runner $source, &matcher, $items, $item-number
            }
        }
        elsif $passthru-context {
            -> $source {
                make-passthru-context-runner $source, &matcher, $item-number
            }
        }

        # Limit on number of matches
        elsif $max-matches-per-source -> uint $max {
            if $context {
                -> $source {
                    make-numeric-context-runner $source, &matcher,
                      $item-number, $context, $context, $max
                }
            }
            elsif $before-context {
                $after-context := %n<after-context>:delete;
                -> $source {
                    make-numeric-context-runner $source, &matcher,
                      $item-number, $before-context, $after-context, $max
                }
            }
            elsif $after-context {
                -> $source {
                    make-numeric-context-runner $source, &matcher,
                      $item-number, Any, $after-context, $max
                }
            }
            elsif $paragraph-context {
                -> $source {
                    make-paragraph-context-runner $source, &matcher,
                      $item-number, $max
                }
            }
            else {
                -> $source {
                    make-runner $source, &matcher, $item-number, $max
                }
            }
        }

        # No limit on number of matches
        elsif $context {
            -> $source {
                make-numeric-context-runner $source, &matcher,
                  $item-number, $context, $context
            }
        }
        elsif $before-context {
            $after-context := %n<after-context>:delete;
            -> $source {
                make-numeric-context-runner $source, &matcher,
                  $item-number, $before-context, $after-context
            }
        }
        elsif $after-context {
            -> $source {
                make-numeric-context-runner $source, &matcher,
                  $item-number, Any, $after-context
            }
        }
        elsif $paragraph-context {
            -> $source {
                make-paragraph-context-runner $source, &matcher, $item-number
            }
        }
    }
    else {
        -> $source { make-runner $source, &matcher, $item-number }
    }

    # Step 6: run the sequences
    my &first-phaser;
    my &next-phaser;
    my &last-phaser;
    if &pattern.has-loop-phasers {
        &first-phaser = &pattern.callable_for_phaser('FIRST');
        &next-phaser  = &pattern.callable_for_phaser('NEXT');
        &last-phaser  = &pattern.callable_for_phaser('LAST');
    }

    # Set up any mapper if not just counting
    my &mapper;
    my $map-all;
    my &next-mapper-phaser;
    my &last-mapper-phaser;
    if !$stats-only && (%n<mapper>:exists) {
        $map-all := %n<map-all>:delete;
        &mapper   = %n<mapper>:delete;
        if &mapper.has-loop-phasers {
            $_() with &mapper.callable_for_phaser('FIRST');
            &next-mapper-phaser = $_ with &mapper.callable_for_phaser('NEXT');
            &last-mapper-phaser = $_ with &mapper.callable_for_phaser('LAST');
        }
    }

    # Set up result sequence
    first-phaser() if &first-phaser;

    # A mapper was specified
    my $result-seq := do if &mapper {
        my $lock := Lock.new;
        $sources-seq.map: $sources-without-only
          ?? -> $source {
                 ++⚛$nr-sources;
                 if producer($source)
                   .map(runner($source)).iterator.pull-one =:= IterationEnd {
                     # thread-safely run mapper and associated phasers
                     $lock.protect: {
                         my \result := mapper($source);
                         next-phaser()        if &next-phaser;
                         next-mapper-phaser() if &next-mapper-phaser;
                         result
                     }
                 }
             }
          !! $sources-only
            ?? -> $source {
                   ++⚛$nr-sources;
                   unless producer($source)
                     .map(runner($source)).iterator.pull-one =:= IterationEnd {
                       # thread-safely run mapper and associated phasers
                       $lock.protect: {
                           my \result := mapper($source);
                           next-phaser()        if &next-phaser;
                           next-mapper-phaser() if &next-mapper-phaser;
                           result
                       }
                   }
               }
            !! -> $source {
                   ++⚛$nr-sources;
                   producer($source).map(runner($source)).iterator.push-all(
                     my $buffer := IterationBuffer.new
                   );

                   if $map-all || $buffer.elems {
                       # thread-safely run mapper and associated phasers
                       $lock.protect: {
                           my \result := mapper($source, $buffer.List);
                           next-phaser()        if &next-phaser;
                           next-mapper-phaser() if &next-mapper-phaser;
                           result
                       }
                   }
               }
    }

    # Only want sources
    elsif $sources-only {
        my $lock := Lock.new if &next-phaser;
        $sources-seq.map: -> $source {
            ++⚛$nr-sources;
            my \result :=
              producer($source).map(runner($source)).iterator.pull-one;
            $lock.protect(&next-phaser) if $lock;
            if $sources-without-only {
                $source if result =:= IterationEnd
            }
            else {
                $source unless result =:= IterationEnd
            }
        }
    }

    # Want results
    else {
        my Lock $lock := Lock.new if &next-phaser;
        my uint $head  = (!$any-context && $max-matches-per-source) // 0;
        $sources-seq.map: -> $source {
            ++⚛$nr-sources;
            my $seq := producer($source).List.map: runner($source);
            $seq := $seq.head($head) if $head;
            my $result := Pair.new: $source, $seq;
            $lock.protect(&next-phaser) if $lock;
            $result
        }
    }

    # Set up any progress reporting and associated cancellation
    $cancellation := $*SCHEDULER.cue(-> { $progress(Progress) }, :every(.2))
      if $progress;

    # Just counting, make sure all results are produced/discarded
    if $stats-only {
        for $result-seq {
            my $*SOURCE := $_;
            (Pair.ACCEPTS($_) ?? .value !! $_).iterator.sink-all;
        }
        $result-seq := ();
    }

    # Not just counting
    else {
        my $lock := Lock.new;  # all of these need race protection

        # Only want unique matches
        if $unique {
            my %seen;
            $result-seq := $result-seq.map: {
                my $outer := Pair.ACCEPTS($_) ?? .value !! $_;
                if Iterable.ACCEPTS($outer) {
                    $outer.map({
                        my $inner := Pair.ACCEPTS($_) ?? .value !! $_;
                        $inner
                          unless $lock.protect: -> { %seen{$inner.WHICH}++ }
                    }).Slip
                }
                else {
                    $outer
                      unless $lock.protect: -> { %seen{$outer.WHICH}++ }
                }
            }
            if $sort {
                $result-seq := Bool.ACCEPTS($sort)
                  ?? $result-seq.sort(*.fc)
                  !! $result-seq.sort($sort)
            }
        }

        # Want classification
        elsif $classify -> &classifier {
            my %classified{Any};

            my sub store(\key, $value) {
                $lock.protect: {
                    (%classified{key} //
                      (%classified{key} := IterationBuffer.new)
                    ).push: $value;
                }
            }

            for $result-seq {
                my $outer := Pair.ACCEPTS($_) ?? .value !! $_;
                if Iterable.ACCEPTS($outer) {
                    for $outer {
                        my $value := Pair.ACCEPTS($_) ?? .value !! $_;
                        store classifier($value), $value;
                    }
                }
                else {
                    store classifier($outer), $outer;
                    my $key := classifier($outer);
                    $lock.protect: {
                        (%classified{$key} // (%classified{$key} := []))
                          .push: $outer;
                    }
                }
            }
            $result-seq := %classified.sort(-*.value.elems).map: {
                Pair.new: .key, .value.List
            }
        }

        # Want categorization
        elsif $categorize -> &categorizer {
            my %categorized{Any};

            my sub store(\keys, $value) {
                $lock.protect: {
                    for keys -> $key {
                        (%categorized{$key} //
                          (%categorized{$key} := IterationBuffer.new)
                        ).push: $value;
                    }
                }
            }

            for $result-seq {
                my $outer := Pair.ACCEPTS($_) ?? .value !! $_;
                if Iterable.ACCEPTS($outer) {
                    for $outer {
                        my $value := Pair.ACCEPTS($_) ?? .value !! $_;
                        store categorizer($value), $value;
                    }
                }
                else {
                    store categorizer($outer), $outer;
                }
            }
            $result-seq := %categorized.sort(-*.value.elems).map: {
                Pair.new: .key, .value.List
            }
        }
    }

    # Need to run all searches before returning
    my %args;
    if $eager
      || $stats    # XXX this should really be lazy
      || &last-mapper-phaser
      || &last-phaser {
        my $buffer := IterationBuffer.new;

        # Convert to frequency map if so requested
        if $frequencies {
            my %bh is Bag = $result-seq.map: *.value.Slip;
            %bh.sort({
                $^b.value cmp $^a.value || $^a.key cmp $^b.key
            }).iterator.push-all: $buffer;
        }

        # Normal collection
        else {
            $result-seq.iterator.push-all: $buffer;
        }

        # Run the phasers if any
        last-phaser()        if &last-phaser;
        last-mapper-phaser() if &last-mapper-phaser;

        $stats-only
          ?? Rak.new(:completed)
          !! Rak.new(:completed, :result($buffer.List))
    }

    # We can be lazy
    else {
        Rak.new: result => $result-seq<>
    }
}

# vim: expandtab shiftwidth=4


