from pyparsing import ParserElement
from procgraph.core.parsing_elements import parse_input_port, parse_output_port

# Enable memoization; much faster, but we can't use
# parse actions with side effects.
ParserElement.enablePackrat()

from pyparsing import (
    Regex,
    Word,
    delimitedList,
    alphas,
    Optional,
    OneOrMore,
    stringEnd,
    alphanums,
    ZeroOrMore,
    Group,
    Suppress,
    lineEnd,
    Combine,
    nums,
    Literal,
    CaselessLiteral,
    restOfLine,
    QuotedString,
    ParseException,
    Forward,
)

from .parsing_elements import (
    VariableReference,
    ParsedBlock,
    ParsedAssignment,
    ImportStatement,
    ParsedModel,
    ParsedSignal,
    ParsedSignalList,
    Connection,
    Where,
    output_from_tokens,
    input_from_tokens,
    config_from_tokens,
)
from .exceptions import PGSyntaxError


def eval_dictionary(s, loc, tokens):  # @UnusedVariable
    if not "content" in tokens:
        return {}
    d = {}
    for a in tokens:
        if "value" in a:
            d[a["key"]] = a["value"]
    return d


def eval_array(s, loc, tokens):  # @UnusedVariable
    elements = tokens.asList()
    res = []
    for i in range(len(elements)):
        t = elements[i]
        res.append(t)
    return res


# Shortcuts
S = Suppress
O = Optional

# Important: should be at the beginning
# make end of lines count
ParserElement.setDefaultWhitespaceChars(" \t")

# These are just values
# Definition of numbers
number = Word(nums)
point = Literal(".")
e = CaselessLiteral("E")
plusorminus = Literal("+") | Literal("-")
integer = Combine(O(plusorminus) + number)
# Note that '42' is not a valid float...
floatnumber = Combine(integer + point + O(number) + O(e + integer)) | Combine(integer + e + integer)

ellipsis_value = Literal("...").setParseAction(lambda _: Ellipsis)


def convert_int(tokens):
    assert len(tokens) == 1
    return int(tokens[0])


integer.setParseAction(convert_int)
floatnumber.setParseAction(lambda tokens: float(tokens[0]))

# comments
comment = S(Literal("#") + restOfLine)
good_name = Combine(Word(alphas) + O(Word(alphanums + "_")))

# All kinds of python strings

single_quoted = QuotedString('"', "\\", unquoteResults=True) | QuotedString("'", "\\", unquoteResults=True)
multi_quoted = QuotedString(
    quoteChar='"""', escChar="\\", multiline=True, unquoteResults=True
) | QuotedString(quoteChar="'''", escChar="\\", multiline=True, unquoteResults=True)
quoted = multi_quoted | single_quoted

reference = Combine(S("$") + good_name("variable"))

reference.setParseAction(VariableReference.from_tokens)

dictionary = Forward()
array = Forward()

value = (
    ellipsis_value
    | quoted
    | array
    | dictionary
    | reference
    | good_name
    | floatnumber  # order is important...
    | integer
)("val")

# dictionaries

dict_key = good_name | quoted
dictionary << (
    S("{") + O(delimitedList(Group(dict_key("key") + S(":") + value("value"))))("content") + S("}")
)


dictionary.setParseAction(eval_dictionary)

array << Group(S("[") + O(delimitedList(value)("elements")) + S("]"))

array.setParseAction(eval_array)


def parse_value(string, filename=None):
    """ This is useful for debugging """

    try:
        ret_value = value.parseString(string, parseAll=True)
        return ret_value["val"]

    except ParseException as e:
        where = Where(filename, string, line=e.lineno, column=e.col)
        raise PGSyntaxError("Error in parsing string: %s" % e, where=where)


def create_model_grammar():

    # We pass a "where" object to the constructors
    def wrap(constructor):
        def from_tokens(string, location, tokens):
            element = constructor(tokens)
            element.where = Where(ParsedModel.static_filename, string, location)
            return element

        return from_tokens

    arrow = S(Regex(r"-+>"))
    newline = S(lineEnd)

    # (don't put '.' at the beginning)
    qualified_name = Combine(good_name + "." + (integer | good_name))

    block_name = good_name
    block_type = good_name | Word("_+-/*") | quoted | reference

    signal = (
        O(S("[") + (integer | good_name)("local_input") + S("]"))
        + O(block_name("block_name") + S("."))
        + (integer | good_name)("name")
        + O(S("[") + (integer | good_name)("local_output") + S("]"))
    )
    signal.setParseAction(wrap(ParsedSignal.from_tokens))

    signals = delimitedList(signal)
    signals.setParseAction(wrap(ParsedSignalList.from_tokens))

    # Note that here the order matters (as qualified = good + something)
    key = qualified_name | good_name

    key_value_pair = Group(key("key") + S("=") + value("value"))

    parameter_list = OneOrMore(key_value_pair)
    parameter_list.setParseAction(lambda s, l, t: dict([(a[0], a[1]) for a in t]))  # @UnusedVariable

    block = (
        S("|")
        + O(block_name("name") + S(":"))
        + block_type("blocktype")
        + O(parameter_list("config"))
        + S("|")
    )
    block.setParseAction(wrap(ParsedBlock.from_tokens))

    input_port = S("(") + block_name("port_name") + S(")")
    output_port = S("(") + block_name("port_name") + S(")")
    output_port.setParseAction(wrap(parse_output_port))
    input_port.setParseAction(wrap(parse_input_port))

    between1 = arrow + O(signals + arrow)
    between2 = arrow + S(newline + arrow) + O(signals + arrow)
    between3 = newline + arrow + O(signals + arrow)
    between = between3 | between2 | between1

    # Different patterns

    # a -> |b| -> c
    arrow_arrow = signals + arrow + O(block + ZeroOrMore(between + block)) + arrow + signals
    # a  -> c
    arrow_arrow2 = signals + arrow + signals
    source = (input_port | block) + ZeroOrMore(between + block) + arrow + signals
    sink = signals + arrow + ZeroOrMore(block + between) + (block | output_port)
    # |a| -> |b| -> |c|
    source_sink = block + ZeroOrMore(between + block)
    # (in) -> |b| -> |c|
    source_sink2 = input_port + ZeroOrMore(between + block)
    # |a| ->  |b| -> (out)
    source_sink3 = ZeroOrMore(block + between) + output_port
    # (in) -> |a| -> (out)
    source_sink4 = input_port + between + ZeroOrMore(block + between) + output_port

    # all of those are called a "connection"
    connection = (
        arrow_arrow ^ sink ^ source ^ source_sink ^ source_sink2 ^ source_sink3 ^ source_sink4 ^ arrow_arrow2
    )

    connection.setParseAction(wrap(Connection.from_tokens))

    # allow breaking lines with backslash
    continuation = "\\" + lineEnd
    connection.ignore(continuation)

    assignment = key("key") + S("=") + value("value")
    assignment.setParseAction(wrap(ParsedAssignment.from_tokens))

    package_name = good_name + ZeroOrMore("." + good_name)
    import_statement = S("import") + package_name("package")
    import_statement.setParseAction(wrap(ImportStatement.from_tokens))

    config = S("config") + good_name("variable") + O(S("=") + value("default")) + O(quoted("docstring"))
    config.setParseAction(wrap(config_from_tokens))

    input_s = S("input") + good_name("name") + O(quoted("docstring"))
    input_s.setParseAction(wrap(input_from_tokens))

    output = S("output") + good_name("name") + O(quoted("docstring"))
    output.setParseAction(wrap(output_from_tokens))

    docs = S(ZeroOrMore(multi_quoted + OneOrMore(newline)))

    action = (
        comment
        | config
        | input_s
        | output
        | (docs + connection)
        | (docs + assignment)
        | (docs + import_statement)
    )

    model_content = (
        ZeroOrMore(newline) + action + ZeroOrMore(OneOrMore(newline) + action) + ZeroOrMore(newline)
    )

    named_model = (
        S(Combine("---" + O(Word("-"))))
        + S("model")
        + good_name("model_name")
        + OneOrMore(newline)
        + O(quoted("docstring"))
        + model_content("content")
    )

    named_model.setParseAction(wrap(ParsedModel.from_named_model))

    anonymous_model = model_content.copy()
    anonymous_model.setParseAction(wrap(ParsedModel.from_anonymous_model))

    comments = ZeroOrMore((comment + newline) | newline)
    pg_file = comments + (OneOrMore(named_model) | anonymous_model) + stringEnd  # important

    return pg_file


pg_file = create_model_grammar()


def parse_model(string, filename=None):
    """ Returns a list of ParsedModel """
    # make this check a special case, otherwise it's hard to debug
    if not string.strip():
        msg = "Passed empty string."
        raise PGSyntaxError(msg, Where(filename, string, 0))

    # this is not threadsafe (but we don't have threads, so it's all good)
    ParsedModel.static_filename = filename

    try:
        parsed = pg_file.parseString(string)
        return list(parsed)
    except ParseException as e:
        where = Where(filename, string, line=e.lineno, column=e.col)
        msg = "Error in parsing string: %s" % e
        raise PGSyntaxError(msg, where=where)


# Register handler for ellipsis, so that ParsedModel is pickable.
# Ellipsis is they only object we use that is not pickable by default.
import copyreg


def code_unpickler(data):  # @UnusedVariable
    return Ellipsis


def code_pickler(code):  # @UnusedVariable
    return code_unpickler, (None,)


copyreg.pickle(type(Ellipsis), code_pickler, code_unpickler)
