# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Thomas Krijnen <thomas@aecgeeks.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell.  If not, see <http://www.gnu.org/licenses/>.


import os
import sys
import string
import operator
import itertools

from pyparsing import *

try:
    from functools import reduce
except:
    pass


class Expression:
    def __init__(self, contents):
        self.contents = contents[0]

    def __repr__(self):
        if self.op is None:
            return repr(self.contents)
        c = [isinstance(c, str) and c or str(c) for c in self.contents]
        if "%s" in self.op:
            return self.op % (" ".join(c))
        else:
            return "(%s)" % (" %s " % self.op).join(c)

    def __iter__(self):
        return self.contents.__iter__()


class Union(Expression):
    op = "|"


class Concat(Expression):
    op = "+"


class Optional(Expression):
    op = "Optional(%s)"


class Repeated(Expression):
    op = "ZeroOrMore(%s)"


class Term(Expression):
    op = None


class Keyword:
    def __init__(self, contents):
        self.contents = contents[0]

    def __repr__(self):
        return self.contents


class Terminal:
    def __init__(self, contents):
        self.contents = contents[0]
        s = self.contents
        self.is_keyword = len(s) >= 4 and s[0 :: len(s) - 1] == '""' and all(c in alphanums + "_" for c in s[1:-1])

    def __repr__(self):
        ty = "CaselessKeyword" if self.is_keyword else "CaselessLiteral"
        return "%s(%s)" % (ty, self.contents)


LPAREN = Suppress("(")
RPAREN = Suppress(")")
LBRACK = Suppress("[")
RBRACK = Suppress("]")
LBRACE = Suppress("{")
RBRACE = Suppress("}")
EQUALS = Suppress("=")
VBAR = Suppress("|")
PERIOD = Suppress(".")
HASH = Suppress("#")

identifier = Word(alphanums + "_")
keyword = Word(alphanums + "_").setParseAction(Keyword)
expression = Forward()
optional = Group(LBRACK + expression + RBRACK).setParseAction(Optional)
repeated = Group(LBRACE + expression + RBRACE).setParseAction(Repeated)
terminal = quotedString.setParseAction(Terminal)
term = (keyword | terminal | optional | repeated | (LPAREN + expression + RPAREN)).setParseAction(Term)
concat = Group(term + OneOrMore(term)).setParseAction(Concat)
factor = concat | term
union = Group(factor + OneOrMore(VBAR + factor)).setParseAction(Union)
rule = identifier + EQUALS + expression + PERIOD

expression << (union | factor)

grammar = OneOrMore(Group(rule))
grammar.ignore(HASH + restOfLine)

express = grammar.parseFile(os.path.join(os.path.dirname(__file__), "express.bnf"))


def find_bytype(expr, ty, li=None):
    if li is None:
        li = []
    if isinstance(expr, Term):
        expr = expr.contents
    if isinstance(expr, ty):
        li.append(expr)
        return set(li)
    elif isinstance(expr, Expression):
        for term in expr:
            find_bytype(term, ty, li)
    return set(li)


actions = {
    "type_decl": "TypeDeclaration",
    "entity_decl": "EntityDeclaration",
    "enumeration_type": "EnumerationType",
    "aggregation_types": "AggregationType",
    "general_aggregation_types": "AggregationType",
    "select_type": "SelectType",
    "binary_type": "BinaryType",
    "subtype_declaration": "SubTypeExpression",
    "supertype_constraint": "SuperTypeExpression",
    "derive_clause": "AttributeList",
    "inverse_clause": "AttributeList",
    "inverse_attr": "InverseAttribute",
    "bound_spec": "BoundSpecification",
    "explicit_attr": "ExplicitAttribute",
    "width_spec": "WidthSpec",
    "string_type": "StringType",
    "named_types": "NamedType",
    "simple_types": "SimpleType",
    "function_decl": "FunctionDeclaration",
    "rule_decl": "RuleDeclaration",
}

to_emit = set(id for id, expr in express)
emitted = set()
to_combine = set(["simple_id"])
statements = []

terminals = reduce(lambda x, y: x | y, (find_bytype(e, Terminal) for id, e in express))
keywords = list(filter(operator.attrgetter("is_keyword"), terminals))
negated_keywords = map(lambda s: "~%s" % s, keywords)
no_action = {"letter", "digit", "digits", "real_literal", "integer_literal", "string_literal", "simple_string_literal", "letter", "not_quote", "not_paren_star_quote_special"}

while True:
    emitted_in_loop = set()
    for id, expr in express:
        kws = map(repr, find_bytype(expr, Keyword))
        found = [k in emitted for k in kws]
        if id in to_emit and all(found):
            emitted_in_loop.add(id)
            emitted.add(id)
            stmt = "(%s)" % expr
            if id in to_combine:
                stmt = " + ".join(itertools.chain(negated_keywords, ("originalTextFor(Combine%s)" % stmt,)))
            if id not in no_action and not isinstance(expr.contents, Keyword) and not id in to_combine:
                node_type = "ListNode" if "ZeroOrMore" in stmt else "Node"
                action = actions.get(id, 'lambda s, loc, t: %s(s, loc, t, rule="%s")' % (node_type, id))
                stmt = "%s.setParseAction(%s)" % (stmt, action)
            statements.append('%s = %s("%s")' % (id, stmt, id))
    to_emit -= emitted_in_loop
    if not emitted_in_loop:
        break

for id in to_emit:
    statements.append('%s = Forward()("%s")' % (id, id))

for id in to_emit:
    expr = [e for k, e in express if k == id][0]
    stmt = "(%s)" % expr
    if id in to_combine:
        stmt = "Suppress%s" % stmt
    if id not in no_action and not isinstance(expr.contents, Keyword):
        children = list(map(operator.attrgetter('contents'), reduce(lambda x, y: x | y, (find_bytype(e, Keyword) for e in [expr]))))
        has_duplicates = len(children) > len(set(children))
        node_type = "ListNode" if ("ZeroOrMore" in stmt or has_duplicates) else "Node"
        action = ".setParseAction(%s)" % (
            actions[id] if id in actions else 'lambda s, loc, t: %s(s, loc, t, rule="%s")' % (node_type, id)
        )
        stmt = "(%s)%s" % (stmt, action)
    statements.append("%s << %s" % (id, stmt))

if __name__ == "__main__":
    print(r"""
# This file is generated by IfcOpenShell ifcexpressparser bootstrap.py

from __future__ import annotations
import os
import sys
import pickle

import schema
import mapping

from pyparsing import *
from nodes import *

def parse(fn: str) -> mapping.Mapping:
    cache_file = fn + ".cache.dat"
    if os.path.exists(cache_file) and os.path.getmtime(cache_file) >= os.path.getmtime(fn):
        with open(cache_file, "rb") as f:
            m = pickle.load(f)
    else:      
        %s

        syntax.ignore("--" + restOfLine)
        syntax.ignore(Regex(r"\((?:\*(?:[^*]*\*+)+?\))"))
        ast = syntax.parseFile(fn)
        s = schema.Schema(ast)
        m = mapping.Mapping(s)

        with open(cache_file, "wb") as f:
            pickle.dump(m, f, protocol=0)
    return m
            
if __name__ == "__main__":
    m = parse(sys.argv[1])
    import importlib
    for output in sys.argv[2:]:
        mdl = importlib.import_module(output)
        mdl.Generator(m).emit()
    sys.stdout.write(m.schema.name)
""" % ("\n        ".join(statements))
    )
