#!/usr/bin/env python
"""Uses abnfgen to stress test the grammar.

Make sure abnfgen is installed::

    brew install abnfgen # On Mac

"""
import argparse
import tempfile
import subprocess
import jmespath
from jmespath import exceptions

# Add any additional args you want to control
# abnfgen.
#ARGS = ['-y', '10']
ARGS = []


GRAMMAR = r"""
expression        = sub-expression / index-expression / or-expression / identifier / "*"
expression        =/ multi-select-list / multi-select-hash / literal / function-expression / pipe-expression
sub-expression    = expression "." ( identifier /
                                     multi-select-list /
                                     multi-select-hash /
                                     function-expression /
                                     "*" )
or-expression     = expression "||" expression
index-expression  = expression bracket-specifier / bracket-specifier
multi-select-list = "[" ( expression *( "," expression ) ) "]"
multi-select-hash = "{" ( keyval-expr *( "," keyval-expr ) ) "}"
pipe-expression   = expression "|" expression
keyval-expr       = identifier ":" expression
bracket-specifier = "[" (number / "*") "]" / "[]"
bracket-specifier =/ "[?" list-filter-expr "]"
list-filter-expr  = expression comparator expression
comparator        = "<" / "<=" / "==" / ">=" / ">" / "!="
function-expression = unquoted-string  (
                        no-args  /
                        one-or-more-args )
no-args             = "(" ")"
one-or-more-args    = "(" ( function-arg *( "," function-arg ) ) ")"
function-arg        = expression / current-node / expression-type
current-node        = "@"
expression-type     = "&" expression

literal           = "`" json-value "`"
literal           =/ "`" 1*(unescaped-literal / escaped-literal) "`"
unescaped-literal = %x20-21 /       ; space !
                        %x23-5A /   ; # - [
                        %x5D-5F /   ; ] ^ _
                        %x61-7A     ; a-z
                        %x7C-10FFFF ; |}~ ...
escaped-literal   = escaped-char / (escape %x60)
number            = ["-"]1*digit
digit             = %x30-39
identifier        = unquoted-string / quoted-string
unquoted-string   = (%x41-5A / %x61-7A / %x5F) *(  ; a-zA-Z_
                        %x30-39  /  ; 0-9
                        %x41-5A /  ; A-Z
                        %x5F    /  ; _
                        %x61-7A)   ; a-z
quoted-string     = quote 1*(unescaped-char / escaped-char) quote
unescaped-char    = %x20-21 / %x23-5B / %x5D-10FFFF
escape            = %x5C   ; Back slash: \
quote             = %x22   ; Double quote: '"'
escaped-char      = escape (
                        %x22 /          ; "    quotation mark  U+0022
                        %x5C /          ; \    reverse solidus U+005C
                        %x2F /          ; /    solidus         U+002F
                        %x62 /          ; b    backspace       U+0008
                        %x66 /          ; f    form feed       U+000C
                        %x6E /          ; n    line feed       U+000A
                        %x72 /          ; r    carriage return U+000D
                        %x74 /          ; t    tab             U+0009
                        %x75 4HEXDIG )  ; uXXXX                U+XXXX

; The ``json-value`` is any valid JSON value with the one exception that the
; ``%x60`` character must be escaped.  While it's encouraged that implementations
; use any existing JSON parser for this grammar rule (after handling the escaped
; literal characters), the grammar rule is shown below for completeness::

json-value = false / null / true / json-object / json-array /
             json-number / json-quoted-string
false = %x66.61.6c.73.65   ; false
null  = %x6e.75.6c.6c      ; null
true  = %x74.72.75.65      ; true
json-quoted-string = %x22 1*(unescaped-literal / escaped-literal) %x22
begin-array     = ws %x5B ws  ; [ left square bracket
begin-object    = ws %x7B ws  ; { left curly bracket
end-array       = ws %x5D ws  ; ] right square bracket
end-object      = ws %x7D ws  ; } right curly bracket
name-separator  = ws %x3A ws  ; : colon
value-separator = ws %x2C ws  ; , comma
ws              = *(%x20 /              ; Space
                    %x09 /              ; Horizontal tab
                    %x0A /              ; Line feed or New line
                    %x0D                ; Carriage return
                   )
json-object = begin-object [ member *( value-separator member ) ] end-object
member = quoted-string name-separator json-value
json-array = begin-array [ json-value *( value-separator json-value ) ] end-array
json-number = [ minus ] int [ frac ] [ exp ]
decimal-point = %x2E       ; .
digit1-9 = %x31-39         ; 1-9
e = %x65 / %x45            ; e E
exp = e [ minus / plus ] 1*DIGIT
frac = decimal-point 1*DIGIT
int = zero / ( digit1-9 *DIGIT )
minus = %x2D               ; -
plus = %x2B                ; +
zero = %x30                ; 0
"""


def stress(args):
    with tempfile.NamedTemporaryFile('w') as f:
        f.write(GRAMMAR)
        f.flush()
        i = 0
        while True:
            output = subprocess.check_output(['abnfgen'] + ARGS + [f.name])
            output = output.decode('utf-8')
            try:
                parsed = jmespath.compile(output)
            # abnfgen can generate expressions that contain
            # unknown functions.  This is ok, because the
            # grammar doesn't enforce the known functions.
            except exceptions.UnknownFunctionError:
                pass
            except Exception as e:
                print(e)
                print(output)
                import pdb; pdb.set_trace()
            i += 1
            if i % 1000 == 0:
                print("num_expressions: %s" % i)


def main():
    parser = argparse.ArgumentParser()
    args = parser.parse_args()
    stress(args)


if __name__ == '__main__':
    main()
