unit class Rainbow::RakuGrammarActions;

use Rainbow::RakuGrammar;
use Rainbow::Token;

method mt($type, $/) {
    make (Rainbow::Token.new($type, $/.Str),);
}

method compact(@tokens) {
    my @compacted;
    my $last = -1;
    for @tokens -> $t {
        if @compacted && $t.type == @compacted[*-1].type == TEXT | STRING | RAKUDOC_TEXT | REGEX_LITERAL | REGEX_SPECIAL {
            @compacted[*-1].text ~= $t.text;
        }
        else {
            @compacted.push: $t;
        }
    }
    @compacted
}

method TOP($/) {
    make $<raku-TOP>.made
}


# Raku Code ====================================================================

method raku-TOP($/) {
    my @tokens;
    for $0.list {
        @tokens.append: $_.hash.values[0].made;
    };
    make self.compact(@tokens);
}

method text($/) { self.mt: TEXT, $/ }
method number-literal($/) { self.mt: NUM_LITERAL, $/ }
method sporco($/) {
    my @tokens;
    for $0.list {
        @tokens.push(Rainbow::Token.new(TEXT, $_.Str)) with $_.hash<space>;
        @tokens.append($_.made) with $_.hash<comment>;
    }
    make @tokens;
}
method scope($/) {
    make (
        Rainbow::Token.new(TEXT, $<opener>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(TEXT, $<closer>.Str),
    );
}
method parens-scope($/) {
    make (
        Rainbow::Token.new(TEXT, $<opener>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(TEXT, $<closer>.Str),
    );
}
method method-call($/) {
    my @tokens;
    @tokens.push(Rainbow::Token.new(OPERATOR, $<dot>.Str));
    @tokens.append: $_.made                            with $<sporco1>;
    @tokens.push(Rainbow::Token.new(ROUTINE, $<identifier>.Str));
    @tokens.append: $_.made                            with $<sporco2>;
    @tokens.push(Rainbow::Token.new(OPERATOR, $_.Str)) with $<colon>;
    @tokens.append: $_.made                            with $<parens-scope>;
    make @tokens;
}
method array-access($/) { self.mt: OPERATOR, $/ }
method sub-call($/) {
    my @tokens;
    @tokens.push(Rainbow::Token.new(ROUTINE, $<identifier>.Str));
    @tokens.append: $_.made                            with $<sporco>;
    @tokens.append: $<parens-scope>.made;
    make @tokens;
}
method enum-decl($/) {
    make (
        Rainbow::Token.new(KEYWORD, $<enum>.Str),
        |$<sporco>.made,
        Rainbow::Token.new(NAME, $<identifier>.Str),
    );
}
method name-scalar($/) {
    my @tokens = Rainbow::Token.new(NAME_SCALAR, $<name>.Str);
    @tokens.append($_.made) with $<hash-access>;
    make @tokens;
}
method name-array($/) { self.mt: NAME_ARRAY, $/ }
method name-hash($/) {
    my @tokens = Rainbow::Token.new(NAME_HASH, $<name>.Str);
    @tokens.append($_.made) with $<hash-access>;
    make @tokens;
}
method name-code($/) { self.mt: NAME_CODE, $/ }
method name-unsigiled($/) {
    my @tokens = Rainbow::Token.new(NAME, $<name>.Str);
    @tokens.append($_.made) with $<hash-access>;
    make @tokens;
}
method hash-access($/) {
    make $_.made with $<hash-normal-access>;
    make $_.made with $<hash-string-access>;
}
method hash-normal-access($/) {
    make (
        Rainbow::Token.new(OPERATOR, $<opener>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(OPERATOR, $<closer>.Str),
    );
}
method hash-string-access($/) {
    make (
        Rainbow::Token.new(OPERATOR, $<opener>.Str),
        Rainbow::Token.new(STRING, $<content>.Str),
        Rainbow::Token.new(OPERATOR, $<closer>.Str),
    );
}
method keyword($/) { self.mt: KEYWORD, $/ }
method operator($/) { self.mt: OPERATOR, $/ }
method type($/) { self.mt: TYPE, $/ }
method builtin-routine($/) { self.mt: ROUTINE, $/ }

# Comments =====================================================================

method comment($/) {
    make $/.hash.values[0].made;
}
method simple-comment($/) { self.mt: COMMENT, $/ }
method delimited-comment($/) {
    if $<delimited-rakudoc-comment-content> {
        make (
            Rainbow::Token.new(RAKUDOC_MARKUP, $<pre>.Str),
            |$<delimited-rakudoc-comment-content>.made,
            Rainbow::Token.new(RAKUDOC_MARKUP, $<closer>.Str),
        );
    }
    else {
        self.mt: COMMENT, $/;
    }
}

# Regexes ======================================================================

method regex($/) {
    make $/.hash.values[0].made;
}

method regex-TOP($/) {
    my @tokens;
    for $0.list {
        @tokens.append: $_.hash.values[0].made;
    }
    make self.compact(@tokens);
}

# Starters --------------------------------------------------------------------

method regex-m($/) {
    make (
        Rainbow::Token.new(REGEX_DELIMITER, $<begin>.Str),
        |($<regex-TOP>.made),
        |($<regex-closer>.made),
    );
}
method regex-s($/) {
    my @tokens = Rainbow::Token.new(REGEX_DELIMITER, $<begin>.Str),
        |$<regex-TOP>.made,
        |$<end1>.made,
    ;
    @tokens.append($_.made) with $<subst>;
    @tokens.append($_.made) with $<end2>;
    make @tokens;
}
method regex-routine($/) {
    make (
        Rainbow::Token.new(KEYWORD, $<type>.Str),
        Rainbow::Token.new(TEXT, $<name>.Str),
        Rainbow::Token.new(REGEX_DELIMITER, $<begin>.Str),
        |$<regex-TOP>.made,
        Rainbow::Token.new(REGEX_DELIMITER, $<end>.Str),
    );
}
method regex-closer($/) { self.mt: REGEX_DELIMITER, $/ }
method subst($/) {
    # TODO
    self.mt: STRING, $/;
}

# Content ---------------------------------------------------------------------

method regex-literal($/) { self.mt: REGEX_LITERAL, $/ }
method regex-special($/) { self.mt: REGEX_SPECIAL, $/ }
method regex-escape-literal($/) {
    make (
        Rainbow::Token.new(REGEX_SPECIAL, $<escaper>.Str),
        Rainbow::Token.new(REGEX_LITERAL, $<literal>.Str),
    );
}
method regex-escape-special($/) {
    make (
        Rainbow::Token.new(REGEX_SPECIAL, $<escaper>.Str),
        Rainbow::Token.new(REGEX_SPECIAL, $<special>.Str),
    );
}
method regex-quote($/) {
    my @tokens = Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str);
    for $0.list {
        for $_.hash.kv -> $k, $v {
            given $k {
                when 'esc-close' | 'esc-esc' {
                    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $v.Str);
                }
                when 'literal' {
                    @tokens.push: Rainbow::Token.new(REGEX_LITERAL, $v.Str);
                }
            }
        }
    }
    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str);
    make @tokens;
}
method regex-variable-declaration($/) {
    my @tokens;
    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str);
    @tokens.push: Rainbow::Token.new(KEYWORD, $<keyword>.Str);
    @tokens.push: Rainbow::Token.new(TEXT, $<ws1>.Str);

    @tokens.append: $_.made with $<name-scalar>;
    @tokens.append: $_.made with $<name-array>;
    @tokens.append: $_.made with $<name-hash>;
    @tokens.append: $_.made with $<name-code>;

    @tokens.push: Rainbow::Token.new(TEXT, $<ws2>.Str);

    @tokens.push: Rainbow::Token.new(OPERATOR, $_.Str) with $<eq>;
    @tokens.push: Rainbow::Token.new(TEXT, $_.Str) with $<ws3>;
    @tokens.push: Rainbow::Token.new(TEXT, $_.Str) with $<val>;

    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str);
    make @tokens;
}
method regex-named-capture($/) { self.mt: REGEX_SPECIAL, $/ }
method regex-regex-variable-interpolation($/) {
    my @tokens;
    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str);
    @tokens.append: $_.made with $<name-scalar>;
    @tokens.append: $_.made with $<name-array>;
    @tokens.push: Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str);
    make @tokens;
}
method regex-literal-closure-interpolation($/) {
    make (
        Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str),
    );
}
method regex-regex-closure-interpolation($/) {
    make (
        Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str),
    );
}
method regex-code($/) {
    make (
        Rainbow::Token.new(REGEX_SPECIAL, $<pre>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(REGEX_SPECIAL, $<post>.Str),
    );
}
method regex-before-after($/) { self.mt: REGEX_SPECIAL, $/ }
method regex-angled-bracket-special($/) { self.mt: REGEX_SPECIAL, $/ }

# Strings ======================================================================

method string-TOP($/) {
    my @tokens;
    for $0.list {
        @tokens.append: $_.hash.values[0].made;
    };
    make self.compact(@tokens);
}

# Starters --------------------------------------------------------------------

method string($/) {
    make (|$<quote-opener>.made,
          |$<string-TOP>.made,
          |$<quote-closer>.made);
}
method quote-opener($/) { self.mt: STRING_DELIMITER, $/ }
method quote-closer($/) { self.mt: STRING_DELIMITER, $/ }

method q-string($/) {
    my $adverbs = $<q-adverb>.list.map(*.Str).join("");
    my @tokens = Rainbow::Token.new(STRING_DELIMITER, $<q-letter>.Str ~ $adverbs);
    @tokens.append(Rainbow::Token.new(STRING_DELIMITER, $<q-quote-opener>.Str))
        with $<q-quote-opener>;
    @tokens.append($_.made) with $<string-TOP>;
    @tokens.append($_.made) with $<quote-closer>;
    make @tokens;
}
method free-quote-opener($/) { self.mt: STRING_DELIMITER, $/ }

method heredoc($/) {
    make (
        Rainbow::Token.new(TEXT, $<nl>.Str),
        |($<string-TOP>.made),
        |($<heredoc-closer>.made),
    );
}
method heredoc-closer($/) { self.mt: STRING_DELIMITER, $/ }

# Interpolation ---------------------------------------------------------------

method backslash-quote-interpolation($/) {
    make (
        Rainbow::Token.new(ESCAPE, $<bs>.Str),
        |$<q-string>.made,
    );
}
method q-interpolation($/) {
    make $0.hash.values[0].made;
}
method b-interpolation($/) { self.mt: ESCAPE, $/ }
method s-interpolation($/) {
    make $<name-scalar>.made;
}
method a-interpolation($/) {
    make $<name-array>.made;
}
method h-interpolation($/) {
    make $<name-hash>.made;
}
method f-interpolation($/) {
    make $<name-code>.made;
}
method c-interpolation($/) {
    make (
        Rainbow::Token.new(ESCAPE, $<opener>.Str),
        |$<raku-TOP>.made,
        Rainbow::Token.new(ESCAPE, $<closer>.Str),
    );
}


# Content ---------------------------------------------------------------------

method quote-content($/) { self.mt: STRING, $/ }
method q-escape($/) { self.mt: ESCAPE, $/ }


# RakuDoc =====================================================================

method rakudoc-TOP($/) {
    my @tokens;
    for $0.list {
        @tokens.append: $_.hash.values[0].made;
    };
    make self.compact(@tokens);
}

# Starters --------------------------------------------------------------------

# Sadly I had to copy this function over from Rainbow.rakumod to prevent a
# circular dependency.
sub tokenize-raku(Str $source) {
    my $actions = Rainbow::RakuGrammarActions.new;
    my $match = Rainbow::RakuGrammar.parse: $source, :$actions;
    $match.made
}

my %lang-tokenizers =
    raku => &tokenize-raku,
;

sub highlight-rakudoc-code(@tokens, $lang) {
    class Markup {
        has $.pos;
        has $.token;
    }

    # Strip out and shelve rakudoc tokens.
    my @rakudoc-markup;
    my @text-pieces;
    my $pos = 0;
    for @tokens -> $token {
        if $token.type == RAKUDOC_TEXT {
            @text-pieces.push: $token.text;
            $pos += $token.text.chars;
        }
        else {
            @rakudoc-markup.push: Markup.new(
                :$pos,
                :$token,
            );
        }
    }
    my $text = @text-pieces.join: '';

    # Tokenize the resulting text.
    my @lang-tokens;
    with %lang-tokenizers{$lang} {
        @lang-tokens = $_($text);
    }
    else {
        @lang-tokens.push: Rainbow::Token.new(RAKUDOC_TEXT, $text);
    }

    # Insert the rakudoc tokens again.
    $pos = 0;
    my @merged;
    for @rakudoc-markup -> $markup {
        my $ins-at = $markup.pos;
        while @lang-tokens.elems && $pos + @lang-tokens[0].text.chars <= $ins-at {
            $pos += @lang-tokens[0].text.chars;
            @merged.push: @lang-tokens.shift;
        }
        if $pos == $ins-at {
            @merged.push: $markup.token;
        }
        else {
            # Target pos is in the middle of a token. -> Split it!
            my $split-pos = $ins-at - $pos;
            @merged.push: @lang-tokens[0].copy-with-text(@lang-tokens[0].text.substr(0, $split-pos));
            @lang-tokens[0].text = @lang-tokens[0].text.substr: $split-pos;
        }
    }
    @merged.append: @lang-tokens;
    @merged
}

method rakudoc-block($/) {
    my @body-tokens;
    if $<name> eq 'code' {
        @body-tokens = highlight-rakudoc-code($<rakudoc-TOP>.made, $*rakudoc-block-lang);
    }
    else {
        @body-tokens = $<rakudoc-TOP>.made;
    }

    make (
        Rainbow::Token.new(RAKUDOC_MARKUP, $<start>.Str ~ ($<type>.defined ?? $<type>.Str !! '') ~ $<name>.Str),
        |(|$_.made with $<rakudoc-block-configs>),
        Rainbow::Token.new(RAKUDOC_MARKUP, $<nl>.Str),
        |@body-tokens,
        Rainbow::Token.new(RAKUDOC_MARKUP, $<end>.Str),
    );
}
method rakudoc-comment($/) {
    make (
        Rainbow::Token.new(RAKUDOC_MARKUP, $<pre>.Str),
        |$<rakudoc-TOP>.made,
        Rainbow::Token.new(TEXT, $<post>.Str),
    );
}
method delimited-rakudoc-comment-content($/) {
    make $<rakudoc-TOP>.made;
}

# Content ---------------------------------------------------------------------

method rakudoc-block-configs($/) {
    my @tokens;
    @tokens.push: Rainbow::Token.new(RAKUDOC_MARKUP, $<pre-space>.Str);
    @tokens.append: self.combine-list-sep(
        $<rakudoc-block-config-line>.map(*.made),
        $<sep>.map({ Rainbow::Token.new(RAKUDOC_MARKUP, $_.Str) })
    );
    make @tokens;
}

method rakudoc-block-config-line($/) {
    make self.combine-list-sep(
        $<rakudoc-block-config>.map(*.made),
        $<sep>.map({ Rainbow::Token.new(RAKUDOC_MARKUP, $_.Str) }),
    );
}

method rakudoc-block-config($/) {
    make (
        Rainbow::Token.new(RAKUDOC_MARKUP, $<starter>.Str),
        Rainbow::Token.new(KEYWORD, $<neg>.Str),
        Rainbow::Token.new(RAKUDOC_MARKUP, $<name>.Str),
        |$<rakudoc-block-config-value>.made,
    );
}

method combine-list-sep(@elems, @seps) {
    roundrobin(@elems, @seps, :slip)>>.List.flat.list;
}

method rakudoc-block-config-value($/) {
    my @tokens;
    with $<opener> {
        @tokens.push: Rainbow::Token.new(TEXT, $<opener>.Str);
        @tokens.push: Rainbow::Token.new(TEXT, $<s1>.Str);

        if $<opener>.Str eq '(' {
            @tokens.append: self.combine-list-sep(
                $<rakudoc-block-config-value-literal>.map(*.made),
                $<sep>.map({ Rainbow::Token.new(TEXT, $_.Str) })
            );
        }
        if $<opener>.Str eq '{' {
            @tokens.append: self.combine-list-sep(
                $<rakudoc-block-config-value-pair>.map(*.made),
                $<sep>.map({ Rainbow::Token.new(TEXT, $_.Str) })
            );
        }
        if $<opener>.Str eq '<' {
            @tokens.append: self.combine-list-sep(
                $<rakudoc-block-config-value-literal>.map(*.made),
                $<sep>.map({ Rainbow::Token.new(TEXT, $_.Str) })
            );
        }

        @tokens.push: Rainbow::Token.new(TEXT, $<s2>.Str);
        @tokens.push: Rainbow::Token.new(TEXT, $_.Str) with $<closer>;
    }
    make @tokens;
}

method rakudoc-block-config-value-literal($/) {
    my @tokens;
    @tokens.push: |$_.made with $<string>;
    @tokens.push: Rainbow::Token.new(TEXT, $_.Str) with $<stuff>;
    make @tokens;
}

method rakudoc-block-config-value-word-list-entry($/) { self.mt: TEXT, $/ }

method rakudoc-block-config-value-pair($/) {
    with $<starter> {
        make (
            Rainbow::Token.new(TEXT, $<starter>.Str),
            Rainbow::Token.new(KEYWORD, $<neg>.Str),
            Rainbow::Token.new(RAKUDOC_MARKUP, $<name>.Str),
            Rainbow::Token.new(TEXT, $<s1>.Str),
            |$<rakudoc-block-config-value>.made,
        );
    }
    with $<between> {
        make (
            |(|$<string>.made with $<string>),
            |(Rainbow::Token.new(RAKUDOC_MARKUP, $<no-string-key>.Str) with $<no-string-key>),
            Rainbow::Token.new(KEYWORD, $<between>.Str),
            |$<rakudoc-block-config-value-literal>.made,
        );
    }
    with $<unknown> {
        make (
            Rainbow::Token.new(TEXT, $<unknown>.Str),
        );
    }
}

# IMPORTANT: It's necessary, that ONLY actual content is marked as RAKUDOC_TEXT
# within rakudoc-markup. Otherwise the logic to format code blocks (which
# relies on the ability to turn a list of tokens to a markup free version by
# removing everything that is not RAKUDOC_TEXT.
method rakudoc-markup($/) {
    my @tokens;
    @tokens.push: Rainbow::Token.new(RAKUDOC_MARKUP, $<pre>.Str ~ $<opener>.Str);
    @tokens.append: $<rakudoc-TOP>.made;
    with $<pipe> {
        @tokens.push: Rainbow::Token.new(RAKUDOC_MARKUP, $<pipe>.Str);
        @tokens.append: $<rakudoc-meta>.made;
        with $<rakudoc-more-meta> {
            for $<rakudoc-more-meta>.list {
                @tokens.append: $_.made;
            }
        }
    }
    @tokens.push: Rainbow::Token.new(RAKUDOC_MARKUP, $<closer>.Str);
    make @tokens;
}

method rakudoc-meta($/) { self.mt: RAKUDOC_MARKUP, $/ }
method rakudoc-more-meta($/) {
    make (
        Rainbow::Token.new(RAKUDOC_MARKUP, $<meta-sep>.Str),
        |(|$<rakudoc-meta>.made with $<rakudoc-meta>),
    );
}
method rakudoc-text($/) { self.mt: RAKUDOC_TEXT, $/ }
