"""
SPARQL 1.1 Parser

based on pyparsing
"""
from __future__ import annotations

import re
import sys
from typing import Any, BinaryIO, List
from typing import Optional as OptionalType
from typing import TextIO, Tuple, Union

from pyparsing import CaselessKeyword as Keyword  # watch out :)
from pyparsing import (
    Combine,
    Forward,
    Group,
    Literal,
    OneOrMore,
    Optional,
    ParseResults,
    Regex,
    Suppress,
    ZeroOrMore,
    delimitedList,
    restOfLine,
)

import rdflib
from rdflib.compat import decodeUnicodeEscape

from . import operators as op
from .parserutils import Comp, CompValue, Param, ParamList

# from pyparsing import Keyword as CaseSensitiveKeyword


DEBUG = False

# ---------------- ACTIONS


def neg(literal: rdflib.Literal) -> rdflib.Literal:
    return rdflib.Literal(-literal, datatype=literal.datatype)


def setLanguage(terms: Tuple[Any, OptionalType[str]]) -> rdflib.Literal:
    return rdflib.Literal(terms[0], lang=terms[1])


def setDataType(terms: Tuple[Any, OptionalType[str]]) -> rdflib.Literal:
    return rdflib.Literal(terms[0], datatype=terms[1])


def expandTriples(terms: ParseResults) -> List[Any]:
    """
    Expand ; and , syntax for repeat predicates, subjects
    """
    # import pdb; pdb.set_trace()
    try:
        res: List[Any] = []
        if DEBUG:
            print("Terms", terms)
        l_ = len(terms)
        for i, t in enumerate(terms):
            if t == ",":
                res.extend([res[-3], res[-2]])
            elif t == ";":
                if i + 1 == len(terms) or terms[i + 1] == ";" or terms[i + 1] == ".":
                    continue  # this semicolon is spurious
                res.append(res[0])
            elif isinstance(t, list):
                # BlankNodePropertyList
                # is this bnode the object of previous triples?
                if (len(res) % 3) == 2:
                    res.append(t[0])
                # is this a single [] ?
                if len(t) > 1:
                    res += t
                # is this bnode the subject of more triples?
                if i + 1 < l_ and terms[i + 1] not in ".,;":
                    res.append(t[0])
            elif isinstance(t, ParseResults):
                res += t.asList()
            elif t != ".":
                res.append(t)
            if DEBUG:
                print(len(res), t)
        if DEBUG:
            import json

            print(json.dumps(res, indent=2))

        return res
        # print res
        # assert len(res)%3 == 0, \
        #       "Length of triple-list is not divisible by 3: %d!"%len(res)

        # return [tuple(res[i:i+3]) for i in range(len(res)/3)]
    except:  # noqa: E722
        if DEBUG:
            import traceback

            traceback.print_exc()
        raise


def expandBNodeTriples(terms: ParseResults) -> List[Any]:
    """
    expand [ ?p ?o ] syntax for implicit bnodes
    """
    # import pdb; pdb.set_trace()
    try:
        if DEBUG:
            print("Bnode terms", terms)
            print("1", terms[0])
            print("2", [rdflib.BNode()] + terms.asList()[0])
        return [expandTriples([rdflib.BNode()] + terms.asList()[0])]
    except Exception as e:
        if DEBUG:
            print(">>>>>>>>", e)
        raise


def expandCollection(terms: ParseResults) -> List[List[Any]]:
    """
    expand ( 1 2 3 ) notation for collections
    """
    if DEBUG:
        print("Collection: ", terms)

    res: List[Any] = []
    other = []
    for x in terms:
        if isinstance(x, list):  # is this a [ .. ] ?
            other += x
            x = x[0]

        b = rdflib.BNode()
        if res:
            res += [res[-3], rdflib.RDF.rest, b, b, rdflib.RDF.first, x]
        else:
            res += [b, rdflib.RDF.first, x]
    res += [b, rdflib.RDF.rest, rdflib.RDF.nil]

    res += other

    if DEBUG:
        print("CollectionOut", res)
    return [res]


# SPARQL Grammar from http://www.w3.org/TR/sparql11-query/#grammar
# ------ TERMINALS --------------
# [139] IRIREF ::= '<' ([^<>"{}|^`\]-[#x00-#x20])* '>'
IRIREF = Combine(
    Suppress("<")
    + Regex(r'[^<>"{}|^`\\%s]*' % "".join("\\x%02X" % i for i in range(33)))
    + Suppress(">")
)
IRIREF.setParseAction(lambda x: rdflib.URIRef(x[0]))

# [164] P_CHARS_BASE ::= [A-Z] | [a-z] | [#x00C0-#x00D6] | [#x00D8-#x00F6] | [#x00F8-#x02FF] | [#x0370-#x037D] | [#x037F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF]

if sys.maxunicode == 0xFFFF:
    # this is narrow python build (default on windows/osx)
    # this means that unicode code points over 0xffff are stored
    # as several characters, which in turn means that regex character
    # ranges with these characters do not work.
    # See
    # * http://bugs.python.org/issue12729
    # * http://bugs.python.org/issue12749
    # * http://bugs.python.org/issue3665
    #
    # Here we simple skip the [#x10000-#xEFFFF] part
    # this means that some SPARQL queries will not parse :(
    # We *could* generate a new regex with \U00010000|\U00010001 ...
    # but it would be quite long :)
    #
    # in py3.3 this is fixed

    PN_CHARS_BASE_re = "A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD"
else:
    # wide python build
    PN_CHARS_BASE_re = "A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\U00010000-\U000EFFFF"

# [165] PN_CHARS_U ::= PN_CHARS_BASE | '_'
PN_CHARS_U_re = "_" + PN_CHARS_BASE_re

# [167] PN_CHARS ::= PN_CHARS_U | '-' | [0-9] | #x00B7 | [#x0300-#x036F] | [#x203F-#x2040]
PN_CHARS_re = "\\-0-9\u00B7\u0300-\u036F\u203F-\u2040" + PN_CHARS_U_re
# PN_CHARS = Regex(u'[%s]'%PN_CHARS_re, flags=re.U)

# [168] PN_PREFIX ::= PN_CHARS_BASE ((PN_CHARS|'.')* PN_CHARS)?
PN_PREFIX = Regex(
    "[%s](?:[%s\\.]*[%s])?" % (PN_CHARS_BASE_re, PN_CHARS_re, PN_CHARS_re), flags=re.U
)

# [140] PNAME_NS ::= PN_PREFIX? ':'
PNAME_NS = Optional(Param("prefix", PN_PREFIX)) + Suppress(":").leaveWhitespace()

# [173] PN_LOCAL_ESC ::= '\' ( '_' | '~' | '.' | '-' | '!' | '$' | '&' | "'" | '(' | ')' | '*' | '+' | ',' | ';' | '=' | '/' | '?' | '#' | '@' | '%' )

PN_LOCAL_ESC_re = "\\\\[_~\\.\\-!$&\"'()*+,;=/?#@%]"
# PN_LOCAL_ESC = Regex(PN_LOCAL_ESC_re) # regex'd
# PN_LOCAL_ESC.setParseAction(lambda x: x[0][1:])

# [172] HEX ::= [0-9] | [A-F] | [a-f]
# HEX = Regex('[0-9A-Fa-f]') # not needed

# [171] PERCENT ::= '%' HEX HEX
PERCENT_re = "%[0-9a-fA-F]{2}"
# PERCENT = Regex(PERCENT_re) # regex'd
# PERCENT.setParseAction(lambda x: chr(int(x[0][1:], 16)))

# [170] PLX ::= PERCENT | PN_LOCAL_ESC
PLX_re = "(%s|%s)" % (PN_LOCAL_ESC_re, PERCENT_re)
# PLX = PERCENT | PN_LOCAL_ESC # regex'd


# [169] PN_LOCAL ::= (PN_CHARS_U | ':' | [0-9] | PLX ) ((PN_CHARS | '.' | ':' | PLX)* (PN_CHARS | ':' | PLX) )?

PN_LOCAL = Regex(
    """([%(PN_CHARS_U)s:0-9]|%(PLX)s)
                     (([%(PN_CHARS)s\\.:]|%(PLX)s)*
                      ([%(PN_CHARS)s:]|%(PLX)s) )?"""
    % dict(PN_CHARS_U=PN_CHARS_U_re, PN_CHARS=PN_CHARS_re, PLX=PLX_re),
    flags=re.X | re.UNICODE,
)


# [141] PNAME_LN ::= PNAME_NS PN_LOCAL
PNAME_LN = PNAME_NS + Param("localname", PN_LOCAL.leaveWhitespace())

# [142] BLANK_NODE_LABEL ::= '_:' ( PN_CHARS_U | [0-9] ) ((PN_CHARS|'.')* PN_CHARS)?
BLANK_NODE_LABEL = Regex(
    "_:[0-9%s](?:[\\.%s]*[%s])?" % (PN_CHARS_U_re, PN_CHARS_re, PN_CHARS_re),
    flags=re.U,
)
BLANK_NODE_LABEL.setParseAction(lambda x: rdflib.BNode(x[0][2:]))


# [166] VARNAME ::= ( PN_CHARS_U | [0-9] ) ( PN_CHARS_U | [0-9] | #x00B7 | [#x0300-#x036F] | [#x203F-#x2040] )*
VARNAME = Regex(
    "[%s0-9][%s0-9\u00B7\u0300-\u036F\u203F-\u2040]*" % (PN_CHARS_U_re, PN_CHARS_U_re),
    flags=re.U,
)

# [143] VAR1 ::= '?' VARNAME
VAR1 = Combine(Suppress("?") + VARNAME)

# [144] VAR2 ::= '$' VARNAME
VAR2 = Combine(Suppress("$") + VARNAME)

# [145] LANGTAG ::= '@' [a-zA-Z]+ ('-' [a-zA-Z0-9]+)*
LANGTAG = Combine(Suppress("@") + Regex("[a-zA-Z]+(?:-[a-zA-Z0-9]+)*"))

# [146] INTEGER ::= [0-9]+
INTEGER = Regex(r"[0-9]+")
# INTEGER.setResultsName('integer')
INTEGER.setParseAction(lambda x: rdflib.Literal(x[0], datatype=rdflib.XSD.integer))

# [155] EXPONENT ::= [eE] [+-]? [0-9]+
EXPONENT_re = "[eE][+-]?[0-9]+"

# [147] DECIMAL ::= [0-9]* '.' [0-9]+
DECIMAL = Regex(r"[0-9]*\.[0-9]+")  # (?![eE])
# DECIMAL.setResultsName('decimal')
DECIMAL.setParseAction(lambda x: rdflib.Literal(x[0], datatype=rdflib.XSD.decimal))

# [148] DOUBLE ::= [0-9]+ '.' [0-9]* EXPONENT | '.' ([0-9])+ EXPONENT | ([0-9])+ EXPONENT
DOUBLE = Regex(r"[0-9]+\.[0-9]*%(e)s|\.([0-9])+%(e)s|[0-9]+%(e)s" % {"e": EXPONENT_re})
# DOUBLE.setResultsName('double')
DOUBLE.setParseAction(lambda x: rdflib.Literal(x[0], datatype=rdflib.XSD.double))


# [149] INTEGER_POSITIVE ::= '+' INTEGER
INTEGER_POSITIVE = Suppress("+") + INTEGER.copy().leaveWhitespace()
INTEGER_POSITIVE.setParseAction(
    lambda x: rdflib.Literal("+" + x[0], datatype=rdflib.XSD.integer)
)

# [150] DECIMAL_POSITIVE ::= '+' DECIMAL
DECIMAL_POSITIVE = Suppress("+") + DECIMAL.copy().leaveWhitespace()

# [151] DOUBLE_POSITIVE ::= '+' DOUBLE
DOUBLE_POSITIVE = Suppress("+") + DOUBLE.copy().leaveWhitespace()

# [152] INTEGER_NEGATIVE ::= '-' INTEGER
INTEGER_NEGATIVE = Suppress("-") + INTEGER.copy().leaveWhitespace()
INTEGER_NEGATIVE.setParseAction(lambda x: neg(x[0]))

# [153] DECIMAL_NEGATIVE ::= '-' DECIMAL
DECIMAL_NEGATIVE = Suppress("-") + DECIMAL.copy().leaveWhitespace()
DECIMAL_NEGATIVE.setParseAction(lambda x: neg(x[0]))

# [154] DOUBLE_NEGATIVE ::= '-' DOUBLE
DOUBLE_NEGATIVE = Suppress("-") + DOUBLE.copy().leaveWhitespace()
DOUBLE_NEGATIVE.setParseAction(lambda x: neg(x[0]))

# [160] ECHAR ::= '\' [tbnrf\"']
# ECHAR = Regex('\\\\[tbnrf"\']')


# [158] STRING_LITERAL_LONG1 ::= "'''" ( ( "'" | "''" )? ( [^'\] | ECHAR ) )* "'''"
# STRING_LITERAL_LONG1 = Literal("'''") + ( Optional( Literal("'") | "''"
# ) + ZeroOrMore( ~ Literal("'\\") | ECHAR ) ) + "'''"
STRING_LITERAL_LONG1 = Regex("'''((?:'|'')?(?:[^'\\\\]|\\\\['ntbrf\\\\]))*'''")
STRING_LITERAL_LONG1.setParseAction(
    lambda x: rdflib.Literal(decodeUnicodeEscape(x[0][3:-3]))
)

# [159] STRING_LITERAL_LONG2 ::= '"""' ( ( '"' | '""' )? ( [^"\] | ECHAR ) )* '"""'
# STRING_LITERAL_LONG2 = Literal('"""') + ( Optional( Literal('"') | '""'
# ) + ZeroOrMore( ~ Literal('"\\') | ECHAR ) ) +  '"""'
STRING_LITERAL_LONG2 = Regex('"""(?:(?:"|"")?(?:[^"\\\\]|\\\\["ntbrf\\\\]))*"""')
STRING_LITERAL_LONG2.setParseAction(
    lambda x: rdflib.Literal(decodeUnicodeEscape(x[0][3:-3]))
)

# [156] STRING_LITERAL1 ::= "'" ( ([^#x27#x5C#xA#xD]) | ECHAR )* "'"
# STRING_LITERAL1 = Literal("'") + ZeroOrMore(
# Regex(u'[^\u0027\u005C\u000A\u000D]',flags=re.U) | ECHAR ) + "'"

STRING_LITERAL1 = Regex("'(?:[^'\\n\\r\\\\]|\\\\['ntbrf\\\\])*'(?!')", flags=re.U)
STRING_LITERAL1.setParseAction(
    lambda x: rdflib.Literal(decodeUnicodeEscape(x[0][1:-1]))
)

# [157] STRING_LITERAL2 ::= '"' ( ([^#x22#x5C#xA#xD]) | ECHAR )* '"'
# STRING_LITERAL2 = Literal('"') + ZeroOrMore (
# Regex(u'[^\u0022\u005C\u000A\u000D]',flags=re.U) | ECHAR ) + '"'

STRING_LITERAL2 = Regex('"(?:[^"\\n\\r\\\\]|\\\\["ntbrf\\\\])*"(?!")', flags=re.U)
STRING_LITERAL2.setParseAction(
    lambda x: rdflib.Literal(decodeUnicodeEscape(x[0][1:-1]))
)

# [161] NIL ::= '(' WS* ')'
NIL = Literal("(") + ")"
NIL.setParseAction(lambda x: rdflib.RDF.nil)

# [162] WS ::= #x20 | #x9 | #xD | #xA
# Not needed?
# WS = #x20 | #x9 | #xD | #xA
# [163] ANON ::= '[' WS* ']'
ANON = Literal("[") + "]"
ANON.setParseAction(lambda x: rdflib.BNode())

# A = CaseSensitiveKeyword('a')
A = Literal("a")
A.setParseAction(lambda x: rdflib.RDF.type)


# ------ NON-TERMINALS --------------

# [5] BaseDecl ::= 'BASE' IRIREF
BaseDecl = Comp("Base", Keyword("BASE") + Param("iri", IRIREF))

# [6] PrefixDecl ::= 'PREFIX' PNAME_NS IRIREF
PrefixDecl = Comp("PrefixDecl", Keyword("PREFIX") + PNAME_NS + Param("iri", IRIREF))

# [4] Prologue ::= ( BaseDecl | PrefixDecl )*
Prologue = Group(ZeroOrMore(BaseDecl | PrefixDecl))

# [108] Var ::= VAR1 | VAR2
Var = VAR1 | VAR2
Var.setParseAction(lambda x: rdflib.term.Variable(x[0]))

# [137] PrefixedName ::= PNAME_LN | PNAME_NS
PrefixedName = Comp("pname", PNAME_LN | PNAME_NS)

# [136] iri ::= IRIREF | PrefixedName
iri = IRIREF | PrefixedName

# [135] String ::= STRING_LITERAL1 | STRING_LITERAL2 | STRING_LITERAL_LONG1 | STRING_LITERAL_LONG2
String = STRING_LITERAL_LONG1 | STRING_LITERAL_LONG2 | STRING_LITERAL1 | STRING_LITERAL2

# [129] RDFLiteral ::= String ( LANGTAG | ( '^^' iri ) )?

RDFLiteral = Comp(
    "literal",
    Param("string", String)
    + Optional(
        Param("lang", LANGTAG.leaveWhitespace())
        | Literal("^^").leaveWhitespace() + Param("datatype", iri).leaveWhitespace()
    ),
)

# [132] NumericLiteralPositive ::= INTEGER_POSITIVE | DECIMAL_POSITIVE | DOUBLE_POSITIVE
NumericLiteralPositive = DOUBLE_POSITIVE | DECIMAL_POSITIVE | INTEGER_POSITIVE

# [133] NumericLiteralNegative ::= INTEGER_NEGATIVE | DECIMAL_NEGATIVE | DOUBLE_NEGATIVE
NumericLiteralNegative = DOUBLE_NEGATIVE | DECIMAL_NEGATIVE | INTEGER_NEGATIVE

# [131] NumericLiteralUnsigned ::= INTEGER | DECIMAL | DOUBLE
NumericLiteralUnsigned = DOUBLE | DECIMAL | INTEGER

# [130] NumericLiteral ::= NumericLiteralUnsigned | NumericLiteralPositive | NumericLiteralNegative
NumericLiteral = (
    NumericLiteralUnsigned | NumericLiteralPositive | NumericLiteralNegative
)

# [134] BooleanLiteral ::= 'true' | 'false'
BooleanLiteral = Keyword("true").setParseAction(lambda: rdflib.Literal(True)) | Keyword(
    "false"
).setParseAction(lambda: rdflib.Literal(False))

# [138] BlankNode ::= BLANK_NODE_LABEL | ANON
BlankNode = BLANK_NODE_LABEL | ANON

# [109] GraphTerm ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | BlankNode | NIL
GraphTerm = iri | RDFLiteral | NumericLiteral | BooleanLiteral | BlankNode | NIL

# [106] VarOrTerm ::= Var | GraphTerm
VarOrTerm = Var | GraphTerm

# [107] VarOrIri ::= Var | iri
VarOrIri = Var | iri

# [46] GraphRef ::= 'GRAPH' iri
GraphRef = Keyword("GRAPH") + Param("graphiri", iri)

# [47] GraphRefAll ::= GraphRef | 'DEFAULT' | 'NAMED' | 'ALL'
GraphRefAll = (
    GraphRef
    | Param("graphiri", Keyword("DEFAULT"))
    | Param("graphiri", Keyword("NAMED"))
    | Param("graphiri", Keyword("ALL"))
)

# [45] GraphOrDefault ::= 'DEFAULT' | 'GRAPH'? iri
GraphOrDefault = ParamList("graph", Keyword("DEFAULT")) | Optional(
    Keyword("GRAPH")
) + ParamList("graph", iri)

# [65] DataBlockValue ::= iri | RDFLiteral | NumericLiteral | BooleanLiteral | 'UNDEF'
DataBlockValue = iri | RDFLiteral | NumericLiteral | BooleanLiteral | Keyword("UNDEF")

# [78] Verb ::= VarOrIri | A
Verb = VarOrIri | A


# [85] VerbSimple ::= Var
VerbSimple = Var

# [97] Integer ::= INTEGER
Integer = INTEGER


TriplesNode = Forward()
TriplesNodePath = Forward()

# [104] GraphNode ::= VarOrTerm | TriplesNode
GraphNode = VarOrTerm | TriplesNode

# [105] GraphNodePath ::= VarOrTerm | TriplesNodePath
GraphNodePath = VarOrTerm | TriplesNodePath


# [93] PathMod ::= '?' | '*' | '+'
PathMod = Literal("?") | "*" | "+"

# [96] PathOneInPropertySet ::= iri | A | '^' ( iri | A )
PathOneInPropertySet = iri | A | Comp("InversePath", "^" + (iri | A))

Path = Forward()

# [95] PathNegatedPropertySet ::= PathOneInPropertySet | '(' ( PathOneInPropertySet ( '|' PathOneInPropertySet )* )? ')'
PathNegatedPropertySet = Comp(
    "PathNegatedPropertySet",
    ParamList("part", PathOneInPropertySet)
    | "("
    + Optional(
        ParamList("part", PathOneInPropertySet)
        + ZeroOrMore("|" + ParamList("part", PathOneInPropertySet))
    )
    + ")",
)

# [94] PathPrimary ::= iri | A | '!' PathNegatedPropertySet | '(' Path ')' | 'DISTINCT' '(' Path ')'
PathPrimary = (
    iri
    | A
    | Suppress("!") + PathNegatedPropertySet
    | Suppress("(") + Path + Suppress(")")
    | Comp("DistinctPath", Keyword("DISTINCT") + "(" + Param("part", Path) + ")")
)

# [91] PathElt ::= PathPrimary Optional(PathMod)
PathElt = Comp(
    "PathElt",
    Param("part", PathPrimary) + Optional(Param("mod", PathMod.leaveWhitespace())),
)

# [92] PathEltOrInverse ::= PathElt | '^' PathElt
PathEltOrInverse = PathElt | Suppress("^") + Comp(
    "PathEltOrInverse", Param("part", PathElt)
)

# [90] PathSequence ::= PathEltOrInverse ( '/' PathEltOrInverse )*
PathSequence = Comp(
    "PathSequence",
    ParamList("part", PathEltOrInverse)
    + ZeroOrMore("/" + ParamList("part", PathEltOrInverse)),
)


# [89] PathAlternative ::= PathSequence ( '|' PathSequence )*
PathAlternative = Comp(
    "PathAlternative",
    ParamList("part", PathSequence) + ZeroOrMore("|" + ParamList("part", PathSequence)),
)

# [88] Path ::= PathAlternative
Path <<= PathAlternative

# [84] VerbPath ::= Path
VerbPath = Path

# [87] ObjectPath ::= GraphNodePath
ObjectPath = GraphNodePath

# [86] ObjectListPath ::= ObjectPath ( ',' ObjectPath )*
ObjectListPath = ObjectPath + ZeroOrMore("," + ObjectPath)


GroupGraphPattern = Forward()


# [102] Collection ::= '(' OneOrMore(GraphNode) ')'
Collection = Suppress("(") + OneOrMore(GraphNode) + Suppress(")")
Collection.setParseAction(expandCollection)

# [103] CollectionPath ::= '(' OneOrMore(GraphNodePath) ')'
CollectionPath = Suppress("(") + OneOrMore(GraphNodePath) + Suppress(")")
CollectionPath.setParseAction(expandCollection)

# [80] Object ::= GraphNode
Object = GraphNode

# [79] ObjectList ::= Object ( ',' Object )*
ObjectList = Object + ZeroOrMore("," + Object)

# [83] PropertyListPathNotEmpty ::= ( VerbPath | VerbSimple ) ObjectListPath ( ';' ( ( VerbPath | VerbSimple ) ObjectList )? )*
PropertyListPathNotEmpty = (
    (VerbPath | VerbSimple)
    + ObjectListPath
    + ZeroOrMore(";" + Optional((VerbPath | VerbSimple) + ObjectListPath))
)

# [82] PropertyListPath ::= Optional(PropertyListPathNotEmpty)
PropertyListPath = Optional(PropertyListPathNotEmpty)

# [77] PropertyListNotEmpty ::= Verb ObjectList ( ';' ( Verb ObjectList )? )*
PropertyListNotEmpty = Verb + ObjectList + ZeroOrMore(";" + Optional(Verb + ObjectList))


# [76] PropertyList ::= Optional(PropertyListNotEmpty)
PropertyList = Optional(PropertyListNotEmpty)

# [99] BlankNodePropertyList ::= '[' PropertyListNotEmpty ']'
BlankNodePropertyList = Group(Suppress("[") + PropertyListNotEmpty + Suppress("]"))
BlankNodePropertyList.setParseAction(expandBNodeTriples)

# [101] BlankNodePropertyListPath ::= '[' PropertyListPathNotEmpty ']'
BlankNodePropertyListPath = Group(
    Suppress("[") + PropertyListPathNotEmpty + Suppress("]")
)
BlankNodePropertyListPath.setParseAction(expandBNodeTriples)

# [98] TriplesNode ::= Collection | BlankNodePropertyList
TriplesNode <<= Collection | BlankNodePropertyList

# [100] TriplesNodePath ::= CollectionPath | BlankNodePropertyListPath
TriplesNodePath <<= CollectionPath | BlankNodePropertyListPath

# [75] TriplesSameSubject ::= VarOrTerm PropertyListNotEmpty | TriplesNode PropertyList
TriplesSameSubject = VarOrTerm + PropertyListNotEmpty | TriplesNode + PropertyList
TriplesSameSubject.setParseAction(expandTriples)

# [52] TriplesTemplate ::= TriplesSameSubject ( '.' TriplesTemplate? )?
# NOTE: pyparsing.py handling of recursive rules is limited by python's recursion
# limit.
# (https://docs.python.org/3/library/sys.html#sys.setrecursionlimit)
# To accommodate arbitrary amounts of triples this rule is rewritten to not be
# recursive:
# [52*] TriplesTemplate ::= TriplesSameSubject ( '.' TriplesSameSubject? )*
TriplesTemplate = ParamList("triples", TriplesSameSubject) + ZeroOrMore(
    Suppress(".") + Optional(ParamList("triples", TriplesSameSubject))
)

# [51] QuadsNotTriples ::= 'GRAPH' VarOrIri '{' Optional(TriplesTemplate) '}'
QuadsNotTriples = Comp(
    "QuadsNotTriples",
    Keyword("GRAPH") + Param("term", VarOrIri) + "{" + Optional(TriplesTemplate) + "}",
)

# [50] Quads ::= Optional(TriplesTemplate) ( QuadsNotTriples '.'? Optional(TriplesTemplate) )*
Quads = Comp(
    "Quads",
    Optional(TriplesTemplate)
    + ZeroOrMore(
        ParamList("quadsNotTriples", QuadsNotTriples)
        + Optional(Suppress("."))
        + Optional(TriplesTemplate)
    ),
)

# [48] QuadPattern ::= '{' Quads '}'
QuadPattern = "{" + Param("quads", Quads) + "}"

# [49] QuadData ::= '{' Quads '}'
QuadData = "{" + Param("quads", Quads) + "}"

# [81] TriplesSameSubjectPath ::= VarOrTerm PropertyListPathNotEmpty | TriplesNodePath PropertyListPath
TriplesSameSubjectPath = (
    VarOrTerm + PropertyListPathNotEmpty | TriplesNodePath + PropertyListPath
)
TriplesSameSubjectPath.setParseAction(expandTriples)

# [55] TriplesBlock ::= TriplesSameSubjectPath ( '.' Optional(TriplesBlock) )?
TriplesBlock = Forward()
TriplesBlock <<= ParamList("triples", TriplesSameSubjectPath) + Optional(
    Suppress(".") + Optional(TriplesBlock)
)


# [66] MinusGraphPattern ::= 'MINUS' GroupGraphPattern
MinusGraphPattern = Comp(
    "MinusGraphPattern", Keyword("MINUS") + Param("graph", GroupGraphPattern)
)

# [67] GroupOrUnionGraphPattern ::= GroupGraphPattern ( 'UNION' GroupGraphPattern )*
GroupOrUnionGraphPattern = Comp(
    "GroupOrUnionGraphPattern",
    ParamList("graph", GroupGraphPattern)
    + ZeroOrMore(Keyword("UNION") + ParamList("graph", GroupGraphPattern)),
)


Expression = Forward()

# [72] ExpressionList ::= NIL | '(' Expression ( ',' Expression )* ')'
ExpressionList = NIL | Group(Suppress("(") + delimitedList(Expression) + Suppress(")"))

# [122] RegexExpression ::= 'REGEX' '(' Expression ',' Expression ( ',' Expression )? ')'
RegexExpression = Comp(
    "Builtin_REGEX",
    Keyword("REGEX")
    + "("
    + Param("text", Expression)
    + ","
    + Param("pattern", Expression)
    + Optional("," + Param("flags", Expression))
    + ")",
)
RegexExpression.setEvalFn(op.Builtin_REGEX)

# [123] SubstringExpression ::= 'SUBSTR' '(' Expression ',' Expression ( ',' Expression )? ')'
SubstringExpression = Comp(
    "Builtin_SUBSTR",
    Keyword("SUBSTR")
    + "("
    + Param("arg", Expression)
    + ","
    + Param("start", Expression)
    + Optional("," + Param("length", Expression))
    + ")",
).setEvalFn(op.Builtin_SUBSTR)

# [124] StrReplaceExpression ::= 'REPLACE' '(' Expression ',' Expression ',' Expression ( ',' Expression )? ')'
StrReplaceExpression = Comp(
    "Builtin_REPLACE",
    Keyword("REPLACE")
    + "("
    + Param("arg", Expression)
    + ","
    + Param("pattern", Expression)
    + ","
    + Param("replacement", Expression)
    + Optional("," + Param("flags", Expression))
    + ")",
).setEvalFn(op.Builtin_REPLACE)

# [125] ExistsFunc ::= 'EXISTS' GroupGraphPattern
ExistsFunc = Comp(
    "Builtin_EXISTS", Keyword("EXISTS") + Param("graph", GroupGraphPattern)
).setEvalFn(op.Builtin_EXISTS)

# [126] NotExistsFunc ::= 'NOT' 'EXISTS' GroupGraphPattern
NotExistsFunc = Comp(
    "Builtin_NOTEXISTS",
    Keyword("NOT") + Keyword("EXISTS") + Param("graph", GroupGraphPattern),
).setEvalFn(op.Builtin_EXISTS)


# [127] Aggregate ::= 'COUNT' '(' 'DISTINCT'? ( '*' | Expression ) ')'
# | 'SUM' '(' Optional('DISTINCT') Expression ')'
# | 'MIN' '(' Optional('DISTINCT') Expression ')'
# | 'MAX' '(' Optional('DISTINCT') Expression ')'
# | 'AVG' '(' Optional('DISTINCT') Expression ')'
# | 'SAMPLE' '(' Optional('DISTINCT') Expression ')'
# | 'GROUP_CONCAT' '(' Optional('DISTINCT') Expression ( ';' 'SEPARATOR' '=' String )? ')'

_Distinct = Optional(Keyword("DISTINCT"))
_AggregateParams = "(" + Param("distinct", _Distinct) + Param("vars", Expression) + ")"

Aggregate = (
    Comp(
        "Aggregate_Count",
        Keyword("COUNT")
        + "("
        + Param("distinct", _Distinct)
        + Param("vars", "*" | Expression)
        + ")",
    )
    | Comp("Aggregate_Sum", Keyword("SUM") + _AggregateParams)
    | Comp("Aggregate_Min", Keyword("MIN") + _AggregateParams)
    | Comp("Aggregate_Max", Keyword("MAX") + _AggregateParams)
    | Comp("Aggregate_Avg", Keyword("AVG") + _AggregateParams)
    | Comp("Aggregate_Sample", Keyword("SAMPLE") + _AggregateParams)
    | Comp(
        "Aggregate_GroupConcat",
        Keyword("GROUP_CONCAT")
        + "("
        + Param("distinct", _Distinct)
        + Param("vars", Expression)
        + Optional(";" + Keyword("SEPARATOR") + "=" + Param("separator", String))
        + ")",
    )
)

# [121] BuiltInCall ::= Aggregate
# | 'STR' '(' + Expression + ')'
# | 'LANG' '(' + Expression + ')'
# | 'LANGMATCHES' '(' + Expression + ',' + Expression + ')'
# | 'DATATYPE' '(' + Expression + ')'
# | 'BOUND' '(' Var ')'
# | 'IRI' '(' + Expression + ')'
# | 'URI' '(' + Expression + ')'
# | 'BNODE' ( '(' + Expression + ')' | NIL )
# | 'RAND' NIL
# | 'ABS' '(' + Expression + ')'
# | 'CEIL' '(' + Expression + ')'
# | 'FLOOR' '(' + Expression + ')'
# | 'ROUND' '(' + Expression + ')'
# | 'CONCAT' ExpressionList
# | SubstringExpression
# | 'STRLEN' '(' + Expression + ')'
# | StrReplaceExpression
# | 'UCASE' '(' + Expression + ')'
# | 'LCASE' '(' + Expression + ')'
# | 'ENCODE_FOR_URI' '(' + Expression + ')'
# | 'CONTAINS' '(' + Expression + ',' + Expression + ')'
# | 'STRSTARTS' '(' + Expression + ',' + Expression + ')'
# | 'STRENDS' '(' + Expression + ',' + Expression + ')'
# | 'STRBEFORE' '(' + Expression + ',' + Expression + ')'
# | 'STRAFTER' '(' + Expression + ',' + Expression + ')'
# | 'YEAR' '(' + Expression + ')'
# | 'MONTH' '(' + Expression + ')'
# | 'DAY' '(' + Expression + ')'
# | 'HOURS' '(' + Expression + ')'
# | 'MINUTES' '(' + Expression + ')'
# | 'SECONDS' '(' + Expression + ')'
# | 'TIMEZONE' '(' + Expression + ')'
# | 'TZ' '(' + Expression + ')'
# | 'NOW' NIL
# | 'UUID' NIL
# | 'STRUUID' NIL
# | 'MD5' '(' + Expression + ')'
# | 'SHA1' '(' + Expression + ')'
# | 'SHA256' '(' + Expression + ')'
# | 'SHA384' '(' + Expression + ')'
# | 'SHA512' '(' + Expression + ')'
# | 'COALESCE' ExpressionList
# | 'IF' '(' Expression ',' Expression ',' Expression ')'
# | 'STRLANG' '(' + Expression + ',' + Expression + ')'
# | 'STRDT' '(' + Expression + ',' + Expression + ')'
# | 'sameTerm' '(' + Expression + ',' + Expression + ')'
# | 'isIRI' '(' + Expression + ')'
# | 'isURI' '(' + Expression + ')'
# | 'isBLANK' '(' + Expression + ')'
# | 'isLITERAL' '(' + Expression + ')'
# | 'isNUMERIC' '(' + Expression + ')'
# | RegexExpression
# | ExistsFunc
# | NotExistsFunc

BuiltInCall = (
    Aggregate
    | Comp(
        "Builtin_STR", Keyword("STR") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_STR)
    | Comp(
        "Builtin_LANG", Keyword("LANG") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_LANG)
    | Comp(
        "Builtin_LANGMATCHES",
        Keyword("LANGMATCHES")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_LANGMATCHES)
    | Comp(
        "Builtin_DATATYPE", Keyword("DATATYPE") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_DATATYPE)
    | Comp("Builtin_BOUND", Keyword("BOUND") + "(" + Param("arg", Var) + ")").setEvalFn(
        op.Builtin_BOUND
    )
    | Comp(
        "Builtin_IRI", Keyword("IRI") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_IRI)
    | Comp(
        "Builtin_URI", Keyword("URI") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_IRI)
    | Comp(
        "Builtin_BNODE", Keyword("BNODE") + ("(" + Param("arg", Expression) + ")" | NIL)
    ).setEvalFn(op.Builtin_BNODE)
    | Comp("Builtin_RAND", Keyword("RAND") + NIL).setEvalFn(op.Builtin_RAND)
    | Comp(
        "Builtin_ABS", Keyword("ABS") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_ABS)
    | Comp(
        "Builtin_CEIL", Keyword("CEIL") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_CEIL)
    | Comp(
        "Builtin_FLOOR", Keyword("FLOOR") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_FLOOR)
    | Comp(
        "Builtin_ROUND", Keyword("ROUND") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_ROUND)
    | Comp(
        "Builtin_CONCAT", Keyword("CONCAT") + Param("arg", ExpressionList)
    ).setEvalFn(op.Builtin_CONCAT)
    | SubstringExpression
    | Comp(
        "Builtin_STRLEN", Keyword("STRLEN") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_STRLEN)
    | StrReplaceExpression
    | Comp(
        "Builtin_UCASE", Keyword("UCASE") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_UCASE)
    | Comp(
        "Builtin_LCASE", Keyword("LCASE") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_LCASE)
    | Comp(
        "Builtin_ENCODE_FOR_URI",
        Keyword("ENCODE_FOR_URI") + "(" + Param("arg", Expression) + ")",
    ).setEvalFn(op.Builtin_ENCODE_FOR_URI)
    | Comp(
        "Builtin_CONTAINS",
        Keyword("CONTAINS")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_CONTAINS)
    | Comp(
        "Builtin_STRSTARTS",
        Keyword("STRSTARTS")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRSTARTS)
    | Comp(
        "Builtin_STRENDS",
        Keyword("STRENDS")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRENDS)
    | Comp(
        "Builtin_STRBEFORE",
        Keyword("STRBEFORE")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRBEFORE)
    | Comp(
        "Builtin_STRAFTER",
        Keyword("STRAFTER")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRAFTER)
    | Comp(
        "Builtin_YEAR", Keyword("YEAR") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_YEAR)
    | Comp(
        "Builtin_MONTH", Keyword("MONTH") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_MONTH)
    | Comp(
        "Builtin_DAY", Keyword("DAY") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_DAY)
    | Comp(
        "Builtin_HOURS", Keyword("HOURS") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_HOURS)
    | Comp(
        "Builtin_MINUTES", Keyword("MINUTES") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_MINUTES)
    | Comp(
        "Builtin_SECONDS", Keyword("SECONDS") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_SECONDS)
    | Comp(
        "Builtin_TIMEZONE", Keyword("TIMEZONE") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_TIMEZONE)
    | Comp(
        "Builtin_TZ", Keyword("TZ") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_TZ)
    | Comp("Builtin_NOW", Keyword("NOW") + NIL).setEvalFn(op.Builtin_NOW)
    | Comp("Builtin_UUID", Keyword("UUID") + NIL).setEvalFn(op.Builtin_UUID)
    | Comp("Builtin_STRUUID", Keyword("STRUUID") + NIL).setEvalFn(op.Builtin_STRUUID)
    | Comp(
        "Builtin_MD5", Keyword("MD5") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_MD5)
    | Comp(
        "Builtin_SHA1", Keyword("SHA1") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_SHA1)
    | Comp(
        "Builtin_SHA256", Keyword("SHA256") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_SHA256)
    | Comp(
        "Builtin_SHA384", Keyword("SHA384") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_SHA384)
    | Comp(
        "Builtin_SHA512", Keyword("SHA512") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_SHA512)
    | Comp(
        "Builtin_COALESCE", Keyword("COALESCE") + Param("arg", ExpressionList)
    ).setEvalFn(op.Builtin_COALESCE)
    | Comp(
        "Builtin_IF",
        Keyword("IF")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ","
        + Param("arg3", Expression)
        + ")",
    ).setEvalFn(op.Builtin_IF)
    | Comp(
        "Builtin_STRLANG",
        Keyword("STRLANG")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRLANG)
    | Comp(
        "Builtin_STRDT",
        Keyword("STRDT")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_STRDT)
    | Comp(
        "Builtin_sameTerm",
        Keyword("sameTerm")
        + "("
        + Param("arg1", Expression)
        + ","
        + Param("arg2", Expression)
        + ")",
    ).setEvalFn(op.Builtin_sameTerm)
    | Comp(
        "Builtin_isIRI", Keyword("isIRI") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_isIRI)
    | Comp(
        "Builtin_isURI", Keyword("isURI") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_isIRI)
    | Comp(
        "Builtin_isBLANK", Keyword("isBLANK") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_isBLANK)
    | Comp(
        "Builtin_isLITERAL", Keyword("isLITERAL") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_isLITERAL)
    | Comp(
        "Builtin_isNUMERIC", Keyword("isNUMERIC") + "(" + Param("arg", Expression) + ")"
    ).setEvalFn(op.Builtin_isNUMERIC)
    | RegexExpression
    | ExistsFunc
    | NotExistsFunc
)

# [71] ArgList ::= NIL | '(' 'DISTINCT'? Expression ( ',' Expression )* ')'
ArgList = (
    NIL
    | "("
    + Param("distinct", _Distinct)
    + delimitedList(ParamList("expr", Expression))
    + ")"
)

# [128] iriOrFunction ::= iri Optional(ArgList)
iriOrFunction = (
    Comp("Function", Param("iri", iri) + ArgList).setEvalFn(op.Function)
) | iri

# [70] FunctionCall ::= iri ArgList
FunctionCall = Comp("Function", Param("iri", iri) + ArgList).setEvalFn(op.Function)


# [120] BrackettedExpression ::= '(' Expression ')'
BrackettedExpression = Suppress("(") + Expression + Suppress(")")

# [119] PrimaryExpression ::= BrackettedExpression | BuiltInCall | iriOrFunction | RDFLiteral | NumericLiteral | BooleanLiteral | Var
PrimaryExpression = (
    BrackettedExpression
    | BuiltInCall
    | iriOrFunction
    | RDFLiteral
    | NumericLiteral
    | BooleanLiteral
    | Var
)

# [118] UnaryExpression ::= '!' PrimaryExpression
# | '+' PrimaryExpression
# | '-' PrimaryExpression
# | PrimaryExpression
UnaryExpression = (
    Comp("UnaryNot", "!" + Param("expr", PrimaryExpression)).setEvalFn(op.UnaryNot)
    | Comp("UnaryPlus", "+" + Param("expr", PrimaryExpression)).setEvalFn(op.UnaryPlus)
    | Comp("UnaryMinus", "-" + Param("expr", PrimaryExpression)).setEvalFn(
        op.UnaryMinus
    )
    | PrimaryExpression
)

# [117] MultiplicativeExpression ::= UnaryExpression ( '*' UnaryExpression | '/' UnaryExpression )*
MultiplicativeExpression = Comp(
    "MultiplicativeExpression",
    Param("expr", UnaryExpression)
    + ZeroOrMore(
        ParamList("op", "*") + ParamList("other", UnaryExpression)
        | ParamList("op", "/") + ParamList("other", UnaryExpression)
    ),
).setEvalFn(op.MultiplicativeExpression)

# [116] AdditiveExpression ::= MultiplicativeExpression ( '+' MultiplicativeExpression | '-' MultiplicativeExpression | ( NumericLiteralPositive | NumericLiteralNegative ) ( ( '*' UnaryExpression ) | ( '/' UnaryExpression ) )* )*

# NOTE: The second part of this production is there because:
# "In signed numbers, no white space is allowed between the sign and the number. The AdditiveExpression grammar rule allows for this by covering the two cases of an expression followed by a signed number. These produce an addition or subtraction of the unsigned number as appropriate."

# Here (I think) this is not necessary since pyparsing doesn't separate
# tokenizing and parsing


AdditiveExpression = Comp(
    "AdditiveExpression",
    Param("expr", MultiplicativeExpression)
    + ZeroOrMore(
        ParamList("op", "+") + ParamList("other", MultiplicativeExpression)
        | ParamList("op", "-") + ParamList("other", MultiplicativeExpression)
    ),
).setEvalFn(op.AdditiveExpression)


# [115] NumericExpression ::= AdditiveExpression
NumericExpression = AdditiveExpression

# [114] RelationalExpression ::= NumericExpression ( '=' NumericExpression | '!=' NumericExpression | '<' NumericExpression | '>' NumericExpression | '<=' NumericExpression | '>=' NumericExpression | 'IN' ExpressionList | 'NOT' 'IN' ExpressionList )?
RelationalExpression = Comp(
    "RelationalExpression",
    Param("expr", NumericExpression)
    + Optional(
        Param("op", "=") + Param("other", NumericExpression)
        | Param("op", "!=") + Param("other", NumericExpression)
        | Param("op", "<") + Param("other", NumericExpression)
        | Param("op", ">") + Param("other", NumericExpression)
        | Param("op", "<=") + Param("other", NumericExpression)
        | Param("op", ">=") + Param("other", NumericExpression)
        | Param("op", Keyword("IN")) + Param("other", ExpressionList)
        | Param(
            "op",
            Combine(Keyword("NOT") + Keyword("IN"), adjacent=False, joinString=" "),
        )
        + Param("other", ExpressionList)
    ),
).setEvalFn(op.RelationalExpression)


# [113] ValueLogical ::= RelationalExpression
ValueLogical = RelationalExpression

# [112] ConditionalAndExpression ::= ValueLogical ( '&&' ValueLogical )*
ConditionalAndExpression = Comp(
    "ConditionalAndExpression",
    Param("expr", ValueLogical) + ZeroOrMore("&&" + ParamList("other", ValueLogical)),
).setEvalFn(op.ConditionalAndExpression)

# [111] ConditionalOrExpression ::= ConditionalAndExpression ( '||' ConditionalAndExpression )*
ConditionalOrExpression = Comp(
    "ConditionalOrExpression",
    Param("expr", ConditionalAndExpression)
    + ZeroOrMore("||" + ParamList("other", ConditionalAndExpression)),
).setEvalFn(op.ConditionalOrExpression)

# [110] Expression ::= ConditionalOrExpression
Expression <<= ConditionalOrExpression


# [69] Constraint ::= BrackettedExpression | BuiltInCall | FunctionCall
Constraint = BrackettedExpression | BuiltInCall | FunctionCall

# [68] Filter ::= 'FILTER' Constraint
Filter = Comp("Filter", Keyword("FILTER") + Param("expr", Constraint))


# [16] SourceSelector ::= iri
SourceSelector = iri

# [14] DefaultGraphClause ::= SourceSelector
DefaultGraphClause = SourceSelector

# [15] NamedGraphClause ::= 'NAMED' SourceSelector
NamedGraphClause = Keyword("NAMED") + Param("named", SourceSelector)

# [13] DatasetClause ::= 'FROM' ( DefaultGraphClause | NamedGraphClause )
DatasetClause = Comp(
    "DatasetClause",
    Keyword("FROM") + (Param("default", DefaultGraphClause) | NamedGraphClause),
)

# [20] GroupCondition ::= BuiltInCall | FunctionCall | '(' Expression ( 'AS' Var )? ')' | Var
GroupCondition = (
    BuiltInCall
    | FunctionCall
    | Comp(
        "GroupAs",
        "("
        + Param("expr", Expression)
        + Optional(Keyword("AS") + Param("var", Var))
        + ")",
    )
    | Var
)

# [19] GroupClause ::= 'GROUP' 'BY' GroupCondition+
GroupClause = Comp(
    "GroupClause",
    Keyword("GROUP")
    + Keyword("BY")
    + OneOrMore(ParamList("condition", GroupCondition)),
)


_Silent = Optional(Param("silent", Keyword("SILENT")))

# [31] Load ::= 'LOAD' 'SILENT'? iri ( 'INTO' GraphRef )?
Load = Comp(
    "Load",
    Keyword("LOAD")
    + _Silent
    + Param("iri", iri)
    + Optional(Keyword("INTO") + GraphRef),
)

# [32] Clear ::= 'CLEAR' 'SILENT'? GraphRefAll
Clear = Comp("Clear", Keyword("CLEAR") + _Silent + GraphRefAll)

# [33] Drop ::= 'DROP' _Silent GraphRefAll
Drop = Comp("Drop", Keyword("DROP") + _Silent + GraphRefAll)

# [34] Create ::= 'CREATE' _Silent GraphRef
Create = Comp("Create", Keyword("CREATE") + _Silent + GraphRef)

# [35] Add ::= 'ADD' _Silent GraphOrDefault 'TO' GraphOrDefault
Add = Comp(
    "Add", Keyword("ADD") + _Silent + GraphOrDefault + Keyword("TO") + GraphOrDefault
)

# [36] Move ::= 'MOVE' _Silent GraphOrDefault 'TO' GraphOrDefault
Move = Comp(
    "Move", Keyword("MOVE") + _Silent + GraphOrDefault + Keyword("TO") + GraphOrDefault
)

# [37] Copy ::= 'COPY' _Silent GraphOrDefault 'TO' GraphOrDefault
Copy = Comp(
    "Copy", Keyword("COPY") + _Silent + GraphOrDefault + Keyword("TO") + GraphOrDefault
)

# [38] InsertData ::= 'INSERT DATA' QuadData
InsertData = Comp("InsertData", Keyword("INSERT") + Keyword("DATA") + QuadData)

# [39] DeleteData ::= 'DELETE DATA' QuadData
DeleteData = Comp("DeleteData", Keyword("DELETE") + Keyword("DATA") + QuadData)

# [40] DeleteWhere ::= 'DELETE WHERE' QuadPattern
DeleteWhere = Comp("DeleteWhere", Keyword("DELETE") + Keyword("WHERE") + QuadPattern)

# [42] DeleteClause ::= 'DELETE' QuadPattern
DeleteClause = Comp("DeleteClause", Keyword("DELETE") + QuadPattern)

# [43] InsertClause ::= 'INSERT' QuadPattern
InsertClause = Comp("InsertClause", Keyword("INSERT") + QuadPattern)

# [44] UsingClause ::= 'USING' ( iri | 'NAMED' iri )
UsingClause = Comp(
    "UsingClause",
    Keyword("USING") + (Param("default", iri) | Keyword("NAMED") + Param("named", iri)),
)

# [41] Modify ::= ( 'WITH' iri )? ( DeleteClause Optional(InsertClause) | InsertClause ) ZeroOrMore(UsingClause) 'WHERE' GroupGraphPattern
Modify = Comp(
    "Modify",
    Optional(Keyword("WITH") + Param("withClause", iri))
    + (
        Param("delete", DeleteClause) + Optional(Param("insert", InsertClause))
        | Param("insert", InsertClause)
    )
    + ZeroOrMore(ParamList("using", UsingClause))
    + Keyword("WHERE")
    + Param("where", GroupGraphPattern),
)


# [30] Update1 ::= Load | Clear | Drop | Add | Move | Copy | Create | InsertData | DeleteData | DeleteWhere | Modify
Update1 = (
    Load
    | Clear
    | Drop
    | Add
    | Move
    | Copy
    | Create
    | InsertData
    | DeleteData
    | DeleteWhere
    | Modify
)


# [63] InlineDataOneVar ::= Var '{' ZeroOrMore(DataBlockValue) '}'
InlineDataOneVar = (
    ParamList("var", Var) + "{" + ZeroOrMore(ParamList("value", DataBlockValue)) + "}"
)

# [64] InlineDataFull ::= ( NIL | '(' ZeroOrMore(Var) ')' ) '{' ( '(' ZeroOrMore(DataBlockValue) ')' | NIL )* '}'
InlineDataFull = (
    (NIL | "(" + ZeroOrMore(ParamList("var", Var)) + ")")
    + "{"
    + ZeroOrMore(
        ParamList(
            "value",
            Group(Suppress("(") + ZeroOrMore(DataBlockValue) + Suppress(")") | NIL),
        )
    )
    + "}"
)

# [62] DataBlock ::= InlineDataOneVar | InlineDataFull
DataBlock = InlineDataOneVar | InlineDataFull


# [28] ValuesClause ::= ( 'VALUES' DataBlock )?
ValuesClause = Optional(
    Param("valuesClause", Comp("ValuesClause", Keyword("VALUES") + DataBlock))
)


# [74] ConstructTriples ::= TriplesSameSubject ( '.' Optional(ConstructTriples) )?
ConstructTriples = Forward()
ConstructTriples <<= ParamList("template", TriplesSameSubject) + Optional(
    Suppress(".") + Optional(ConstructTriples)
)

# [73] ConstructTemplate ::= '{' Optional(ConstructTriples) '}'
ConstructTemplate = Suppress("{") + Optional(ConstructTriples) + Suppress("}")


# [57] OptionalGraphPattern ::= 'OPTIONAL' GroupGraphPattern
OptionalGraphPattern = Comp(
    "OptionalGraphPattern", Keyword("OPTIONAL") + Param("graph", GroupGraphPattern)
)

# [58] GraphGraphPattern ::= 'GRAPH' VarOrIri GroupGraphPattern
GraphGraphPattern = Comp(
    "GraphGraphPattern",
    Keyword("GRAPH") + Param("term", VarOrIri) + Param("graph", GroupGraphPattern),
)

# [59] ServiceGraphPattern ::= 'SERVICE' _Silent VarOrIri GroupGraphPattern
ServiceGraphPattern = Comp(
    "ServiceGraphPattern",
    Keyword("SERVICE")
    + _Silent
    + Param("term", VarOrIri)
    + Param("graph", GroupGraphPattern),
)

# [60] Bind ::= 'BIND' '(' Expression 'AS' Var ')'
Bind = Comp(
    "Bind",
    Keyword("BIND")
    + "("
    + Param("expr", Expression)
    + Keyword("AS")
    + Param("var", Var)
    + ")",
)

# [61] InlineData ::= 'VALUES' DataBlock
InlineData = Comp("InlineData", Keyword("VALUES") + DataBlock)

# [56] GraphPatternNotTriples ::= GroupOrUnionGraphPattern | OptionalGraphPattern | MinusGraphPattern | GraphGraphPattern | ServiceGraphPattern | Filter | Bind | InlineData
GraphPatternNotTriples = (
    GroupOrUnionGraphPattern
    | OptionalGraphPattern
    | MinusGraphPattern
    | GraphGraphPattern
    | ServiceGraphPattern
    | Filter
    | Bind
    | InlineData
)

# [54] GroupGraphPatternSub ::= Optional(TriplesBlock) ( GraphPatternNotTriples '.'? Optional(TriplesBlock) )*
GroupGraphPatternSub = Comp(
    "GroupGraphPatternSub",
    Optional(ParamList("part", Comp("TriplesBlock", TriplesBlock)))
    + ZeroOrMore(
        ParamList("part", GraphPatternNotTriples)
        + Optional(".")
        + Optional(ParamList("part", Comp("TriplesBlock", TriplesBlock)))
    ),
)


# ----------------
# [22] HavingCondition ::= Constraint
HavingCondition = Constraint

# [21] HavingClause ::= 'HAVING' HavingCondition+
HavingClause = Comp(
    "HavingClause",
    Keyword("HAVING") + OneOrMore(ParamList("condition", HavingCondition)),
)

# [24] OrderCondition ::= ( ( 'ASC' | 'DESC' ) BrackettedExpression )
# | ( Constraint | Var )
OrderCondition = Comp(
    "OrderCondition",
    Param("order", Keyword("ASC") | Keyword("DESC"))
    + Param("expr", BrackettedExpression)
    | Param("expr", Constraint | Var),
)

# [23] OrderClause ::= 'ORDER' 'BY' OneOrMore(OrderCondition)
OrderClause = Comp(
    "OrderClause",
    Keyword("ORDER")
    + Keyword("BY")
    + OneOrMore(ParamList("condition", OrderCondition)),
)

# [26] LimitClause ::= 'LIMIT' INTEGER
LimitClause = Keyword("LIMIT") + Param("limit", INTEGER)

# [27] OffsetClause ::= 'OFFSET' INTEGER
OffsetClause = Keyword("OFFSET") + Param("offset", INTEGER)

# [25] LimitOffsetClauses ::= LimitClause Optional(OffsetClause) | OffsetClause Optional(LimitClause)
LimitOffsetClauses = Comp(
    "LimitOffsetClauses",
    LimitClause + Optional(OffsetClause) | OffsetClause + Optional(LimitClause),
)

# [18] SolutionModifier ::= GroupClause? HavingClause? OrderClause? LimitOffsetClauses?
SolutionModifier = (
    Optional(Param("groupby", GroupClause))
    + Optional(Param("having", HavingClause))
    + Optional(Param("orderby", OrderClause))
    + Optional(Param("limitoffset", LimitOffsetClauses))
)


# [9] SelectClause ::= 'SELECT' ( 'DISTINCT' | 'REDUCED' )? ( ( Var | ( '(' Expression 'AS' Var ')' ) )+ | '*' )
SelectClause = (
    Keyword("SELECT")
    + Optional(Param("modifier", Keyword("DISTINCT") | Keyword("REDUCED")))
    + (
        OneOrMore(
            ParamList(
                "projection",
                Comp(
                    "vars",
                    Param("var", Var)
                    | (
                        Literal("(")
                        + Param("expr", Expression)
                        + Keyword("AS")
                        + Param("evar", Var)
                        + ")"
                    ),
                ),
            )
        )
        | "*"
    )
)

# [17] WhereClause ::= 'WHERE'? GroupGraphPattern
WhereClause = Optional(Keyword("WHERE")) + Param("where", GroupGraphPattern)

# [8] SubSelect ::= SelectClause WhereClause SolutionModifier ValuesClause
SubSelect = Comp(
    "SubSelect", SelectClause + WhereClause + SolutionModifier + ValuesClause
)

# [53] GroupGraphPattern ::= '{' ( SubSelect | GroupGraphPatternSub ) '}'
GroupGraphPattern <<= Suppress("{") + (SubSelect | GroupGraphPatternSub) + Suppress("}")

# [7] SelectQuery ::= SelectClause DatasetClause* WhereClause SolutionModifier
SelectQuery = Comp(
    "SelectQuery",
    SelectClause
    + ZeroOrMore(ParamList("datasetClause", DatasetClause))
    + WhereClause
    + SolutionModifier
    + ValuesClause,
)

# [10] ConstructQuery ::= 'CONSTRUCT' ( ConstructTemplate DatasetClause* WhereClause SolutionModifier | DatasetClause* 'WHERE' '{' TriplesTemplate? '}' SolutionModifier )
# NOTE: The CONSTRUCT WHERE alternative has unnecessarily many Comp/Param pairs
# to allow it to through the same algebra translation process
ConstructQuery = Comp(
    "ConstructQuery",
    Keyword("CONSTRUCT")
    + (
        ConstructTemplate
        + ZeroOrMore(ParamList("datasetClause", DatasetClause))
        + WhereClause
        + SolutionModifier
        + ValuesClause
        | ZeroOrMore(ParamList("datasetClause", DatasetClause))
        + Keyword("WHERE")
        + "{"
        + Optional(
            Param(
                "where",
                Comp(
                    "FakeGroupGraphPatten",
                    ParamList("part", Comp("TriplesBlock", TriplesTemplate)),
                ),
            )
        )
        + "}"
        + SolutionModifier
        + ValuesClause
    ),
)

# [12] AskQuery ::= 'ASK' DatasetClause* WhereClause SolutionModifier
AskQuery = Comp(
    "AskQuery",
    Keyword("ASK")
    + Param("datasetClause", ZeroOrMore(DatasetClause))
    + WhereClause
    + SolutionModifier
    + ValuesClause,
)

# [11] DescribeQuery ::= 'DESCRIBE' ( VarOrIri+ | '*' ) DatasetClause* WhereClause? SolutionModifier
DescribeQuery = Comp(
    "DescribeQuery",
    Keyword("DESCRIBE")
    + (OneOrMore(ParamList("var", VarOrIri)) | "*")
    + ZeroOrMore(ParamList("datasetClause", DatasetClause))
    + Optional(WhereClause)
    + SolutionModifier
    + ValuesClause,
)

# [29] Update ::= Prologue ( Update1 ( ';' Update )? )?
Update = Forward()
Update <<= ParamList("prologue", Prologue) + Optional(
    ParamList("request", Update1) + Optional(";" + Update)
)


# [2] Query ::= Prologue
# ( SelectQuery | ConstructQuery | DescribeQuery | AskQuery )
# ValuesClause
# NOTE: ValuesClause was moved to individual queries
Query = Prologue + (SelectQuery | ConstructQuery | DescribeQuery | AskQuery)

# [3] UpdateUnit ::= Update
UpdateUnit = Comp("Update", Update)

# [1] QueryUnit ::= Query
QueryUnit = Query

QueryUnit.ignore("#" + restOfLine)
UpdateUnit.ignore("#" + restOfLine)


expandUnicodeEscapes_re: re.Pattern = re.compile(
    r"\\u([0-9a-f]{4}(?:[0-9a-f]{4})?)", flags=re.I
)


def expandUnicodeEscapes(q: str) -> str:
    r"""
    The syntax of the SPARQL Query Language is expressed over code points in Unicode [UNICODE]. The encoding is always UTF-8 [RFC3629].
    Unicode code points may also be expressed using an \ uXXXX (U+0 to U+FFFF) or \ UXXXXXXXX syntax (for U+10000 onwards) where X is a hexadecimal digit [0-9A-F]
    """

    def expand(m: re.Match) -> str:
        try:
            return chr(int(m.group(1), 16))
        except (ValueError, OverflowError) as e:
            raise ValueError("Invalid unicode code point: " + m.group(1)) from e

    return expandUnicodeEscapes_re.sub(expand, q)


def parseQuery(q: Union[str, bytes, TextIO, BinaryIO]) -> ParseResults:
    if hasattr(q, "read"):
        q = q.read()
    if isinstance(q, bytes):
        q = q.decode("utf-8")

    q = expandUnicodeEscapes(q)
    return Query.parseString(q, parseAll=True)


def parseUpdate(q: Union[str, bytes, TextIO, BinaryIO]) -> CompValue:
    if hasattr(q, "read"):
        q = q.read()

    if isinstance(q, bytes):
        q = q.decode("utf-8")

    q = expandUnicodeEscapes(q)
    return UpdateUnit.parseString(q, parseAll=True)[0]
