### ----------------------------------------------------
### -- Config::BINDish
### -- Licenses: Artistic-2.0
### -- Authors: Vadim Belman <vrurg@lflat.org>
### -- File: lib/Config/BINDish/Grammar.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 2
Parse::MissingPart,
                    :what('body'),
                    :spec($*CFG-CTX.description)
            }
            False }> ]
}

multi rule statement:sym<empty> {
    <?after <.statement-terminator>> ';'
}

token statement-terminator {
    [
    | [ <.ws> $ ]
    | [ <.ws> <?before \}> ]
    | [ <?after \}> <.ws> $<terminator>=';'? ]
    | [ <.ws> $<terminator>=';' ]
    ]
}

token statement-terminate {
    <statement-terminator>
    {
        unless !$*CFG-GRAMMAR.strict.syntax
               || ($<statement-terminator>
                   && $<statement-terminator><terminator>
                   && ~$<statement-terminator><terminator> eq ';')
        {
            self.panic: "Missing semicolon"
        }
    }
}

token bad-block {
    :my Value $*CFG-BLOCK-TYPE;
    :my Value $*CFG-BLOCK-NAME;
    :my Value $*CFG-BLOCK-CLASS;
    :my $*CFG-CTX = Nil;
    <block-head>
}

token bad-option {
    :my Value $*CFG-VALUE;
    <statement:sym<option>>
}

token bad-statement(:$expected, Bool :$contextish) {
    :my $*CFG-KEYWORD;
    :my $*CFG-NON-STRICT = True;
    :my $*CFG-SIMULATION = True;
    :my $*CFG-CTX-PROPS;
    $<err-pos> = <?>
    [
      [
        [ <?{ $contextish }> && <?before <.keyword>> ]
        [
            [ <?before <bad-block>>
              {
                  my $keyword = $*CFG-KEYWORD.coerced;
                  my \exception = $keyword ∈ $*CFG-GRAMMAR.keywords<block>
                                      ?? Config::BINDish::X::Parse::Context
                                      !! Config::BINDish::X::Parse::Unknown;
                  $<err-pos>.panic: exception, :what<block>, :keyword($*CFG-KEYWORD), :ctx($*CFG-CTX)
              } ]
            || [ <?before <bad-option>>
              {
                  my $keyword = $*CFG-KEYWORD.coerced;
                  my \exception = $keyword ∈ $*CFG-GRAMMAR.keywords<option>
                                      ?? Config::BINDish::X::Parse::Context
                                      !! Config::BINDish::X::Parse::Unknown;
                  $<err-pos>.panic: exception, :what<option>, :keyword($*CFG-KEYWORD), :ctx($*CFG-CTX)
              } ]
            || [ <keyword> <?{ $*CFG-KEYWORD.coerced ∈ $*CFG-GRAMMAR.keywords{'option'|'block'} }>
              {
                   $<err-pos>.panic:
                       Config::BINDish::X::Parse::Context, :what<keyword>, :keyword($*CFG-KEYWORD), :ctx($*CFG-CTX)
              } ]
        ]
      ]
      || { $<err-pos>.panic: "Unrecognized statement" ~ ($expected andthen ", expected $_") }
    ]
}

token enter-option { <?> }

token block-head {
    :my Value $*CFG-KEYWORD;
    $<block-type>=<.keyword> <.ws>
    <?{
        my $kwd = $*CFG-KEYWORD.coerced;
        ! $*CFG-GRAMMAR.is-reserved( :block($kwd) )
        && $*CFG-GRAMMAR.may-be-tried('block', $kwd)
    }>
    { $*CFG-BLOCK-TYPE = $*CFG-KEYWORD }
    [ <block-name> <.ws> [<block-class> <.ws>]? ]?
    # If we know properties then this block keyword was found
    [ <?{ $*CFG-CTX-PROPS.defined }> || <?before \{> ]
    [ <?{ ? $*CFG-SIMULATION }> || <.enter-block> ]
}

token block-name {
    :my Value $*CFG-VALUE;
    <value> { $*CFG-BLOCK-NAME = $*CFG-VALUE }
}

token block-class {
    :my Value $*CFG-KEYWORD;
    :my Value $*CFG-VALUE;
      $<class>=<.value>    { $*CFG-BLOCK-CLASS = $*CFG-VALUE }
    | $<class>=<.keyword>  { $*CFG-BLOCK-CLASS = $*CFG-KEYWORD }
}

token block-body {
    [ \{ <.ws> ] ~ [ <.ws> \} <.statement-terminate> ]
    <statement-list>
    { self.leave-block }
}

token enter-block {
    <?>
    {
        my $block-type = $*CFG-BLOCK-TYPE;
        self.enter-ctx:
            :type<block>,
            :keyword( $block-type ),
            :name( $*CFG-BLOCK-NAME ),
            :class( $*CFG-BLOCK-CLASS );
        .validate-block with $*CFG-BLOCK-ERR-POS;
    }
}

token C-comment {
    '/*' ~ '*/' $<comment-body>=.*?
}

token CPP-comment {
    '//' $<comment-body>=.*? $$
}

token UNIX-comment {
    [ ^^ '#line' <.ws>
        $<line>=\d+ { self.set-line-relative: Int($<line>) }
        [ <.ws>
          \" ~ \" $<file>=<.qstring('"')> { $*CFG-GRAMMAR.set-file($<file>.caps.map(*.value).join) } ]?
        \s*? $$ ]
    || [ '#' $<comment-body>=.*? $$ ]
}

token keyword {
    <.wb> $<kwd>=[ <.alpha> [ \w | '-' ]* ] <.wb>
    { $*CFG-KEYWORD = Value.new: :type(Str), :type-name<keyword>, :payload($/) }
}

token bool-true {
    yes | on | true
}
token bool-false {
    no | off | false
}

token boolean {
    <.wb> [<bool-true> | <bool-false>] <.wb>
}

token qstring($quote) {
    [ [ $<chunk>=<{ '<-[ \\\\ ' ~ $quote ~ ' ]>+'  }> ] | [ \\ $<char>=. ] ]*? <?before $($quote)>
}

token dq-string {
    \" ~ \" $<string>=<.qstring('"')> { self.set-value: Str, :dq-string($<string>) }
}
token sq-string {
    \' ~ \' $<string>=<.qstring("'")> { self.set-value: Str, :sq-string($<string>) }
}

token decimal($max = 9) {
    [ <{"<[0..$max]>"}>+ ]+ % '_'
}
token heximal {
    [ <xdigit>+ ]+ % '_'
}

token natural_num {
    [
        | [ 0 <?before <[bodx]>>: [
                | [ b <decimal(1)> ]
                | [ o <decimal(7)> ]
                | [ d <decimal> ]
                | [ x <heximal> ]
            ] ]
        | <decimal>
    ]
}

token num_suffix {
    <?after <xdigit>> <[KkMmGgTtPp]>
}

method maybe-specific-value(Str:D $what --> Mu) is raw {
    # A value cannot start with a comment-like sequence.
    return self.new(:$.from, to => -3) if self.postmatch.starts-with('#' | '//' | '/*');
    my $ctx = $*CFG-CTX;
    my $props = $ctx.props;
    with $props {
        if $props ~~ ContainerProps && $ctx.props.value-sym {
            for $props.value-sym<> -> $sym {
                # Make it possible for value parsers to know they're expected.
                my $*CFG-SPECIFIC-VALUE-SYM = $sym;
                with self."value:sym<$sym>"() {
                    return $_ if $_;
                }
            }
            # We either have a non-value or a wrong value type. Try parsing as an option first if current context is a
            # value-only block as the rule would throw correct context exception then.
            my $expected-specific = $ctx ~~ Context::Option && $ctx.props.value-sym;
            if !$expected-specific && $ctx ~~ Context::Block && $ctx.props.value-only {
                self."statement:sym<option>"();
                $expected-specific = True;
            }
            self.panic: Config::BINDish::X::Parse::SpecificValue, :$what, :$ctx, :keyword( $ctx.keyword ) if $expected-specific;
            return self.new(:$.from, to => -3);
        }
    }
    self.value
}

proto token value {*}
multi token value:sym<string> {
    <dq-string> | <sq-string>
}

multi token value:sym<bool> {
    # Make sure we don't parse for pre-declared non-boolean containers
    <?{ (my $ctx = $*CFG-CTX)
        && (!(my $props = $ctx.props).defined
            || ($props.can-type(Bool)
                && $props.can-type-name('bool'))) }>
    <boolean> { self.set-value: Bool, :bool($/) }
}

multi token value:sym<keyword> {
    <?{ (my $ctx = $*CFG-CTX) and
        (($ctx ~~ Context::Option)
         || ($ctx.cur-block-ctx.props andthen .value-only)) }>
    :my Value $*CFG-KEYWORD;
    [
        <?{ $*CFG-SPECIFIC-VALUE-SYM andthen $_ eq 'keyword' }> <keyword>
        | <!before <.boolean>> <keyword>
    ]
    { $*CFG-GRAMMAR.set-value: Str, :keyword($*CFG-KEYWORD.payload) }
}

multi token value:sym<num> {
    $<err-pos>=<?before <[-+]>? [\d | '.' \d]>
    $<sign>=<[-+]>? [
        | [ $<int>=\d* '.' $<frac>=\d+ ]
        | [ $<int>=\d+ ]
    ]
    e $<exp>=[<[-+]>? \d+] <.wb> { self.set-value: Num, :num($/) }
}

multi token value:sym<rat> {
    $<err-pos>=<?before <[-+]>? [\d | '.' \d]>
    $<sign>=<[-+]>? [
        | [ $<numerator>=<.decimal> '.' <!before <.decimal>> ]
        | [ [ $<numerator>=<.decimal>? '.' $<denominator>=<.decimal> ]
            <num_suffix>? <.wb> ]
    ]
    { self.set-value: Rat, :rat($/) }
}

multi token value:sym<int> {
    $<err-pos>=<?before <[-+]>? \d>
    [ $<icard>=[ <[-+]>?: <natural_num> ]
        [ [<num_suffix>? <.wb> ]
          | { $<err-pos>.panic: Config::BINDish::X::Parse::BadNum } ]
    ]
    <!before <[.e]>>
    { self.set-value: Int, :int($/) }
}

token path-component {
    <[\N] - [;/]>+
}
multi token value:sym<file-path> {
    :my Value $*CFG-KEYWORD;
    [
        | [['.' | '..' ]? '/' <.path-component>* %% '/']
        | [ <?{ $*CFG-SPECIFIC-VALUE-SYM andthen $_ eq 'file-path' }>
            # Option-like looking path is only accepted when:
            # - it's an option value
            # - it belongs to a value-only block
            [ <?{ $_ ~~ Context::Option || .cur-block-ctx.props.value-only with $*CFG-CTX }>
            | <!before <.option-name>> ]
            <.path-component>+ %% '/' ]
    ]
    { self.set-value: Str, :file-path($/) }
}


