#! /usr/bin/env raku
use v6.d;


# Track block and counter configurations across the nested scopes defined by =section blocks...
state @scope .= push({ counters=>{}, configs=>{} });

# Track global counter values...
state %count-for;

# Find the superordinate of a given block name (e.g. =head2 is the superordinate of =head3 )...
multi superordinate-of ($name where /     \D   1   $/) { ""          }  # Top level has no super
multi superordinate-of ($name where /^ (.*?) (\d+) $/) { $0 ~ ($1-1) }  # Otherwise N-1

# Normalize names by removing any leading "num" and ensuring a trailing level number...
sub normalize-name ($name) {
    $name ~~ / ^ [num]? $<basename>=(.*?) $<level>=(\d*) $ /;
    return $<basename> ~ ($<level>.chars ?? $<level> !! 1);
}

# Each section creates a nested scope on the stack, starting as a copy of the surrounding scope...
sub begin-section { @scope.push( @scope.tail.deepmap: *.clone ) }
sub   end-section { @scope.pop }

# Configure a named counter...
sub counter ( $name           is copy,   # Name of counter
              :$prefix         is copy,   # Name of another counter to be prefixed to this one
              :$restart,                  # Is the counter to restart (or increment normally)?
              :$restart-after,            # After which blocks is the counter restarted?
              :$restart-except-after,     # After which blocks is the counter NOT restarted?
             ) {
    # These counters intrinsically have "except after" trigger semantics...
    constant \LISTLIKE = rx{ ^ [num]? [item|defn] \d* $ };

    # Clean the name...
    $name = normalize-name($name);

    # Work out its hierarchy...
    my $superordinate = superordinate-of($name);

    # Get (or create a default for) the named counter's configuration...
    my $current = @scope.tail<counters>{$name}
            //= { :$name,
                  :prefix($superordinate),
                  :restart-status(1),
                  :restart-trigger($name ~~ LISTLIKE ?? build-except-trigger([$name])
                  !! build-after-trigger([$superordinate])),
            };

    # Update any counter configuration for which metaoptions were specified...
    with $prefix  {
        if prefix-is-cyclic($name, $prefix) -> $cycle {
            say "Ignoring cyclic :prefix<$prefix> ($cycle)";
        }
        else {
            $current<prefix> = $prefix;
        }
    }
    with $restart {
        $current<restart-status> = $restart;
    }
    with $restart-except-after {
        $current<restart-trigger> = build-except-trigger( $restart-except-after );
    }
    orwith $restart-after {
        $current<restart-trigger> = build-after-trigger( $restart-after );
    }
}

# Detect a potentially cyclic prefix...
sub prefix-is-cyclic ($from, $to, @cycle = [$from]) {
    return @cycle.join('→')~"→$to" if @cycle.grep($to);
    with @scope.tail<counters>{$to}<prefix> -> $next-prefix {
        return prefix-is-cyclic($to, $next-prefix, [|@cycle, $to]);
    }
    return;
}

# Generate the (next or current) hierarchical numbering for the counter of the specified name...
multi counter-value ("") { () }   # Handle non-existent prefix counts for level-1 blocks
multi counter-value ($name is copy, :$next) {

    # Convert requested name to the appropriate key for look-up...
    $name = normalize-name($name);

    # Get counter (autovivifying if this is the first use of this counter)...
    without @scope.tail<counters>{$name} { counter($name) }
    my $counter = @scope.tail<counters>{$name};

    # Restart if a restart is pending (or is implied by the count not yet being defined)...
    %count-for{$name} //= 1;
    if $counter<restart-status>.defined {
        %count-for{$name} = do given $counter<restart-status> {
            when Bool { $counter<restart-status>  ??  1  !!  %count-for{$name} + 1    }
            when Int  { $counter<restart-status>                                      }
            default   { die "Internal error: invalid restart status value: $_.gist()" }
        }
        $counter<restart-status> = Nil;
    }
    # Otherwise if next value was requested, update the counter...
    elsif $next {
        %count-for{$name}++;
    }

    # Return full hierarchical numbering for this counter...
    return |counter-value($counter<prefix>), %count-for{$name};
}


# Simulate setting a block's counter (for this demo &config does not handle other block config)...
sub config ($blockname, :$counter, *%ignore-other-config) {

    # Change the counter assigned to a given block...
    @scope.tail<blocks><counter>{ normalize-name($blockname) } = normalize-name($counter);
}

# Simulate block rendering...
sub block ($name is copy, $content = "Block $name at line {callframe(3).line}" ) {

    # Is the block numbered???
    my $is-numbered = $name ~~ /^num/;

    # Clean up the block's name...
    $name = normalize-name($name);

    # Find the name of the counter that was configured for the block
    # (or by default, use the counter of the same name)...
    my $counter-name = @scope.tail<blocks><counter>{$name} // $name;

    # Always get next value for associated counter and format it (even if not a numbered block)...
    my $block-number = counter-value($counter-name, :next).join('.') ~ '.';

    # Simulate rendering of a block
    # (In a real renderer this would be where the RakuDoc-to-Whatever transformational magic happens)...
    render-block($name, $content, $is-numbered ?? $block-number !! q{});

    # Does rendering this block trigger any restarts???
    for @scope.tail<counters>.values -> $counter {
        if $name ~~ $counter<restart-trigger> {
            $counter<restart-status> //= 1;      # //= so we don't override any explicit :restart
        }
    }
}


# Utility sub to decompose a counter name...
sub split-base-and-level ($name) {
    $name ~~ /^ $<base>=(.*?) $<level>=(\d+) $/;
    return $<base>, $<level>;
}

# Convert a :restart-after specification to a single smartmatchable value...
multi build-after-trigger( $after-names ) {
    return any
            $after-names.values».&normalize-name.map: -> $after-name {

                # Extract basename and maximal trigger level...
                my ($NAME-BASE, $MAXIMAL-LEVEL) = split-base-and-level( $after-name );

                # Create a regex that matches that basename plus any equal-or-superordinate level...
                / ^ $NAME-BASE $<level>=(\d+) $  <?{ $<level> <= $MAXIMAL-LEVEL }> /
            }
}

# Convert a :restart-except-after specification to a single smartmatchable value...
multi build-except-trigger( $except-after-names ) {
    return none
            $except-after-names.values».&normalize-name.map: -> $except-after-name {

                # Extract basename and minimal trigger level...
                my ($NAME-BASE, $MINIMAL-LEVEL) = split-base-and-level( $except-after-name );

                # Create a regex that matches that basename plus any equal-or-subordinate level...
                / ^ $NAME-BASE $<level>=(\d+) $  <?{ $<level> >= $MINIMAL-LEVEL }> /
            }
}


#### Support for testing #######################################################

# Colour-code output (or not, if Terminal::ANSIColor is unavailable)...
my &colored = (try require Terminal::ANSIColor) !=== Nil
        ?? &Terminal::ANSIColor::EXPORT::DEFAULT::colored
        !! sub ($text, $format) { $text }

# Report outcomes and track failure rate...
my ($test-count, $fail-count);
sub pass ($text)  { $test-count++;                say colored($text, 'bold cyan')       }
sub fail ($text)  { $test-count++; $fail-count++; say colored($text, 'bold italic red') }

# Simulate rendering of a block for testing purposes...
sub render-block ($blockname, $content, $number) {
    my $as-Expect = $content ~~ /^ \s* Expect\: \s* $number \s* $/;
    $as-Expect ?? pass("$number A $blockname block")
    !! fail("$number A $blockname block  ($content)");
}

# Report failure rate at the end of the testing...
END {
    TEST 'OUTCOME';
    $fail-count ?? fail("Failed $fail-count of $test-count")
    !! pass("All $test-count tests passed");
}

# Quick-and-dirty parser for RakuDoc so we can specify tests in (a subset of) the standard syntax...
sub TEST ($test-desc, $rakudoc-src = q{}) {

    # Assume we're starting fresh each time at the beginning of a document...
    %count-for = ();

    # Display highlighted description...
    say colored($test-desc.join(' ').fmt("\n# %-80s "), 'bold black on_white');

    # Parse source lines and run the associated tests...
    for $rakudoc-src.lines {
        when m:s{^ '=begin'    [section|rakudoc] $} { begin-section                      }
        when m:s{^ '=end'      [section|rakudoc] $} {   end-section                      }
        when m:s{^ '=config'   <ident>  (.+)     $} {       "config '$<ident>', $0".EVAL }
        when m:s{^ '=counter'  <ident>  (.+)     $} {      "counter '$<ident>', $0".EVAL }
        when m:s{^ '='<ident>  (.+)              $} {         block ~$<ident>, ~$0       }
        when m:s{^ [ '#' .* ]?                   $} { }
        warn "Unknown line: $_";
    }
}


#### Test as many possibilities as we can think of #############################

TEST < Standard headN blocks, restart after superordinate block >, q{

    =begin rakudoc
        =numhead1          Expect: 1.
            =numhead2      Expect: 1.1.
            =numhead2      Expect: 1.2.
        =numhead1          Expect: 2.
            =numhead2     Expect: 2.1.
                =numhead3 Expect: 2.1.1.
            =numhead2     Expect: 2.2.
            =numhead2     Expect: 2.3.
    =end rakudoc
};


TEST < Headings with :!restart, :restart, and :restart(N) configs >, q{

    =begin rakudoc
        =numhead1           Expect: 1.
            =numhead2       Expect: 1.1.
            =numhead2       Expect: 1.2.
            =counter numhead2 :!restart

        =numhead1           Expect: 2.
                =numhead3   Expect: 2.3.1.

            =numhead2       Expect: 2.4.
            =numhead2       Expect: 2.5.

            =counter numhead2 :restart
            =numhead2       Expect: 2.1.

            =counter numhead2 :restart(42)
            =numhead2       Expect: 2.42.

        =numhead1           Expect: 3.

            =counter numhead2 :!restart
            =numhead2       Expect: 3.43.

    =end rakudoc
};


TEST < Table with head1 prefix >, q{

    =begin rakudoc

        =counter table1 :prefix<head1>

        =numhead1      Expect: 1.
            =numtable  Expect: 1.1.
            =numtable  Expect: 1.2.
            =numtable  Expect: 1.3.
        =numhead1      Expect: 2.
            =numtable  Expect: 2.4.
            =numtable  Expect: 2.5.
            =numtable  Expect: 2.6.
    =end rakudoc
};


TEST < Table with head1 prefix, restarting after each head1 >, q{

    =begin rakudoc

        =counter numtable  :prefix<head1> :restart-after<head1>

        =numhead1       Expect: 1.
            =numtable1  Expect: 1.1.
            =numtable1  Expect: 1.2.
            =numtable1  Expect: 1.3.
        =numhead1       Expect: 2.
            =numtable1  Expect: 2.1.
            =numtable1  Expect: 2.2.
            =numtable1  Expect: 2.3.
    =end rakudoc
};

TEST < Ignore cyclic prefixes >, q{

    =begin rakudoc
        =begin section
            =counter formula1 :prefix<table1>
            =counter table1   :prefix<formula1>

            =numhead1      Expect: 1.
                =numtable  Expect: 1.
                =numtable  Expect: 2.
                =numtable  Expect: 3.
        =end section

        =begin section
            =counter formula1 :prefix<head2>
            =counter head2    :prefix<table2>
            =counter table2   :prefix<item7>
            =counter item7    :prefix<table1>
            =counter table1   :prefix<formula1>

            =numhead1      Expect: 1.
                =numtable  Expect: 1.
                =numtable  Expect: 2.
                =numtable  Expect: 3.
        =end section

    =end rakudoc
};



TEST < Formula shares table counter >, q{

    =begin rakudoc

        =config formula :counter<table>

        =numtable   Expect: 1.
        =numtable   Expect: 2.
        =numformula Expect: 3.
        =numtable   Expect: 4.
        =numformula Expect: 5.
        =numformula Expect: 6.
        =numtable   Expect: 7.
    =end rakudoc
};


TEST < Formula and table share user-defined counter >, q{

    =begin rakudoc

        =config table   :counter<TableOrFormula>
        =config formula :counter<TableOrFormula>

        =numtable   Expect: 1.
        =numtable   Expect: 2.
        =numformula Expect: 3.
        =numtable   Expect: 4.
        =numformula Expect: 5.
        =numformula Expect: 6.
        =numtable   Expect: 7.
    =end rakudoc
};


TEST < Independent numbering of a single section >, q{

    =begin rakudoc
        =numhead3             Expect: 1.1.1.
            =numtable         Expect: 1.
            =numtable         Expect: 2.
            =numformula       Expect: 1.
            =numformula       Expect: 2.

        =numhead3             Expect: 1.1.2.
            =begin section

                =config table   :counter< AsideNumbering >
                =config formula :counter< AsideNumbering >

                =numtable     Expect: 1.
                =numtable     Expect: 2.
                =numformula   Expect: 3.
                =numformula   Expect: 4.
            =end section

        =numhead3             Expect: 1.1.3.
            =numtable         Expect: 3.
            =numtable         Expect: 4.
            =numformula       Expect: 3.
            =numformula       Expect: 4.
    =end rakudoc
};


TEST < Item and definition blocks restart differently >, q{

    =begin rakudoc
        =numhead1          Expect: 1.
            =numitem       Expect: 1.
            =numitem2      Expect: 1.1.
            =numitem2      Expect: 1.2.
            =numitem       Expect: 2.
            =numitem       Expect: 3.
            =numformula    Expect: 1.
            =numitem       Expect: 1.
            =numitem6      Expect: 1.1.1.1.1.1.
            =numitem6      Expect: 1.1.1.1.1.2.
            =numitem       Expect: 2.
            =numitem       Expect: 3.
        =numhead1          Expect: 2.
            =numitem       Expect: 1.
            =numdefn       Expect: 1.
            =numdefn       Expect: 2.
            =numdefn       Expect: 3.
            =numformula    Expect: 2.
            =numdefn       Expect: 1.
            =numdefn       Expect: 2.
            =numdefn       Expect: 3.
        =numhead1          Expect: 3.

            =counter MyItem1 :restart-except-after<MyItem1 MyComment>
            =counter MyItem2 :restart-except-after<MyItem2 MyComment>

            =numMyItem     Expect: 1.
            =numMyItem     Expect: 2.
            =numMyItem     Expect: 3.
            =numformula    Expect: 3.
            =numMyItem     Expect: 1.
            =numMyItem     Expect: 2.
            =numMyComment  Expect: 1.
            =numMyItem     Expect: 3.
            =numformula    Expect: 4.
            =numMyItem     Expect: 1.
            =numMyItem2    Expect: 1.1.
            =numMyItem6    Expect: 1.1.1.1.1.1.
            =numMyItem2    Expect: 1.2.
            =numMyItem6    Expect: 1.2.1.1.1.1.
            =numMyItem     Expect: 2.
            =numMyItem     Expect: 3.
    =end rakudoc
};


TEST < Unnumbered blocks still consume numbers >, q{

    =begin rakudoc
        =numhead1               Expect: 1.
            =numformula         Expect: 1.
            =formula            Expect:
                =numformula2    Expect: 2.1.
            =numformula         Expect: 3.

        =head1                  Expect:
            =numitem            Expect: 1.
            =item               Expect:
            =numitem            Expect: 3.
        =numhead1               Expect: 3.

    =end rakudoc
};


TEST < A1. Independent counters that don't reset >, q{

# For example...
#
#   1. Heading
#   1.1. Subheading
#   1.1.1. Subsubheading
#       Table 1. Caption for first table
#       Formula 1. Caption for first formula
#       Table 2. Caption for second table
#       Formula 2. Caption for second formula
#   1.1.2. Subsubheading
#       Table 3. Caption for first table
#       Table 4. Caption for second table
#       Formula 3. Caption for first formula

    =begin rakudoc
        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.
                    =numformula1  Expect: 1.
                    =numtable1    Expect: 2.
                    =numformula1  Expect: 2.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 3.
                    =numtable1    Expect: 4.
                    =numformula1  Expect: 3.
    =end rakudoc
};


TEST < A2. Independent counters that reset >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1. Caption for first table
#        Formula 1. Caption for first formula
#        Table 2. Caption for second table
#        Formula 2. Caption for second formula
#    1.1.2. Subsubheading
#        Table 1. Caption for first table
#        Table 2. Caption for second table
#        Formula 1. Caption for first formula

    =begin rakudoc

        =counter table1   :restart-after<head3>
        =counter formula1 :restart-after<head3>

        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.
                    =numformula1  Expect: 1.
                    =numtable1    Expect: 2.
                    =numformula1  Expect: 2.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 1.
                    =numtable1    Expect: 2.
                    =numformula1  Expect: 1.
    =end rakudoc
};


TEST < A3. Shared counters that don't reset >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1. Caption for first table
#        Formula 2. Caption for first formula
#        Table 3. Caption for second table
#        Formula 4. Caption for second formula
#    1.1.2. Subsubheading
#        Table 5. Caption for first table
#        Table 6. Caption for second table
#        Formula 7. Caption for first formula

    =begin rakudoc

        =config formula1 :counter<table1>

        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.
                    =numformula1  Expect: 2.
                    =numtable1    Expect: 3.
                    =numformula1  Expect: 4.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 5.
                    =numtable1    Expect: 6.
                    =numformula1  Expect: 7.
    =end rakudoc
};


TEST < A3. Shared counters that don't reset (approach 2) >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1. Caption for first table
#        Formula 2. Caption for first formula
#        Table 3. Caption for second table
#        Formula 4. Caption for second formula
#    1.1.2. Subsubheading
#        Table 5. Caption for first table
#        Table 6. Caption for second table
#        Formula 7. Caption for first formula

    =begin rakudoc

        =config formula1 :counter<TableAndFormula>
        =config table1   :counter<TableAndFormula>

        =numhead1             Expect: 1.
            =numhead2         Expect: 1.1.
                =numhead3     Expect: 1.1.1.
                =numtable1    Expect: 1.
                =numformula1  Expect: 2.
                =numtable1    Expect: 3.
                =numformula1  Expect: 4.
            =numhead3         Expect: 1.1.2.
                =numtable1    Expect: 5.
                =numtable1    Expect: 6.
                =numformula1  Expect: 7.
    =end rakudoc
};


TEST < A4. Shared counters that reset >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1. Caption for first table
#        Formula 2. Caption for first formula
#        Table 3. Caption for second table
#        Formula 4. Caption for second formula
#    1.1.2. Subsubheading
#        Table 1. Caption for first table
#        Table 2. Caption for second table
#        Formula 3. Caption for first formula

    =begin rakudoc

        =config formula1 :counter<TableAndFormula>
        =config table1   :counter<TableAndFormula>

        =counter TableAndFormula :restart-after<head3>

        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.
                    =numformula1  Expect: 2.
                    =numtable1    Expect: 3.
                    =numformula1  Expect: 4.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 1.
                    =numtable1    Expect: 2.
                    =numformula1  Expect: 3.
    =end rakudoc
};


TEST < A5-A8. Hierarchy numbering borrowed from another block >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1.1.1.1. Caption for first table
#        Formula 1.1.1.1. Caption for first formula
#        Table 1.1.1.2. Caption for second table
#        Formula 1.1.1.2. Caption for second formula
#    1.1.2. Subsubheading
#        Table 1.1.2.1. Caption for first table
#        Table 1.1.2.2. Caption for second table
#        Formula 1.1.2.1. Caption for first formula

    =begin rakudoc

        =counter table   :restart-after<head3> :prefix<head3>
        =counter formula :restart-after<head3> :prefix<head3>

        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.1.1.1.
                    =numformula1  Expect: 1.1.1.1.
                    =numtable1    Expect: 1.1.1.2.
                    =numformula1  Expect: 1.1.1.2.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 1.1.2.1.
                    =numtable1    Expect: 1.1.2.2.
                    =numformula1  Expect: 1.1.2.1.
    =end rakudoc
};


TEST < A5-A8. Another example of hierarchy numbering borrowed from another block >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1.1. Caption for first table
#        Formula 1.2. Caption for first formula
#        Table 1.3. Caption for second table
#        Formula 1.4. Caption for second formula
#    1.1.2. Subsubheading
#        Table 1.5. Caption for first table
#        Table 1.6. Caption for second table
#        Formula 1.7. Caption for first formula
#    2. Second heading
#        Table 2.8. Caption for next table

    =begin rakudoc

        =config formula1 :counter<TableAndFormula>
        =config table1   :counter<TableAndFormula>

        =counter TableAndFormula :prefix<head1>

        =numhead1                 Expect: 1.
            =numhead2             Expect: 1.1.
                =numhead3         Expect: 1.1.1.
                    =numtable1    Expect: 1.1.
                    =numformula1  Expect: 1.2.
                    =numtable1    Expect: 1.3.
                    =numformula1  Expect: 1.4.
                =numhead3         Expect: 1.1.2.
                    =numtable1    Expect: 1.5.
                    =numtable1    Expect: 1.6.
                    =numformula1  Expect: 1.7.
        =numhead1                 Expect: 2.
             =numtable1           Expect: 2.8.
    =end rakudoc
};


TEST < A9-A16. Localized independent numbering >, q{

# For example...
#
#    1. Heading
#    1.1. Subheading
#    1.1.1. Subsubheading
#        Table 1.1. Caption for first table
#        Formula 1.2. Caption for first formula
#        Table 1.3. Caption for second table
#        Formula 1.4. Caption for second formula
#    1.1.2. Subsubheading
#        Table 1000. Caption for first table
#        Formula 1.5 Caption for first formula
#        Table 1001. Caption for second table
#    2. Second heading
#        Table 2.6. Caption for next table
#        Formula 2.7 Caption for next formula

    =begin rakudoc

        =config formula1 :counter<TableAndFormula>
        =config table1   :counter<TableAndFormula>

        =counter TableAndFormula :prefix<head1>

        =numhead1                     Expect: 1.
            =numhead2                 Expect: 1.1.
                =numhead3             Expect: 1.1.1.
                    =numtable1        Expect: 1.1.
                    =numformula1      Expect: 1.2.
                    =numtable1        Expect: 1.3.
                    =numformula1      Expect: 1.4.

            =begin section
                =config table1   :counter<SpecialTable>
                =counter SpecialTable :restart(1000)

                    =numhead3         Expect: 1.1.2.
                        =numtable1    Expect: 1000.
                        =numformula1  Expect: 1.5.
                        =numtable1    Expect: 1001.
            =end section

        =numhead1                     Expect: 2.
                        =numtable1    Expect: 2.6.
                        =numformula1  Expect: 2.7.
    =end rakudoc
};

TEST < Skipped superordinates >, q{

=counter head3 :restart(6)
=numhead5  Expect: 1.1.6.1.1.
};

TEST < Skipped superordinates second example >, q{

=counter head3 :restart(6)
=counter item :prefix< head4 >
=counter item :restart(4)

=numitem    Expect: 1.1.6.1.4.

};