"""Data structure extracted from parsing the EDSL which are added within the
Rust code."""

from __future__ import annotations
# mypy: disallow-untyped-defs, disallow-incomplete-defs, disallow-untyped-calls

import typing
import os

from dataclasses import dataclass
from .utils import keep_until
from .grammar import Element, Grammar, LenientNt, NtDef, Production


@dataclass(frozen=True)
class ImplFor:
    __slots__ = ['param', 'trait', 'for_type']
    param: str
    trait: str
    for_type: str


def eq_productions(grammar: Grammar, prod1: Production, prod2: Production) -> bool:
    s1 = tuple(e for e in prod1.body if grammar.is_shifted_element(e))
    s2 = tuple(e for e in prod2.body if grammar.is_shifted_element(e))
    return s1 == s2


def merge_productions(grammar: Grammar, prod1: Production, prod2: Production) -> Production:
    # Consider all shifted elements as non-moveable elements, and insert other
    # around these.
    assert eq_productions(grammar, prod1, prod2)
    l1 = list(prod1.body)
    l2 = list(prod2.body)
    body: typing.List[Element] = []
    while l1 != [] and l2 != []:
        front1 = list(keep_until(l1, grammar.is_shifted_element))
        front2 = list(keep_until(l2, grammar.is_shifted_element))
        assert front1[-1] == front2[-1]
        l1 = l1[len(front1):]
        l2 = l2[len(front2):]
        if len(front1) == 1:
            body = body + front2
        elif len(front2) == 1:
            body = body + front1
        else:
            raise ValueError("We do not know how to sort operations yet.")
    return prod1.copy_with(body=body)


@dataclass(frozen=True)
class ExtPatch:
    "Patch an existing grammar rule by adding Code"

    prod: typing.Tuple[LenientNt, str, NtDef]

    def apply_patch(
            self,
            filename: os.PathLike,
            grammar: Grammar,
            nonterminals: typing.Dict[LenientNt, NtDef]
    ) -> None:
        # - name: non-terminal.
        # - namespace: ":" for syntactic or "::" for lexical. Always ":" as
        #     defined by rust_nt_def.
        # - nt_def: A single non-terminal definition with a single production.
        (name, namespace, nt_def) = self.prod
        gnt_def = nonterminals[name]
        # Find a matching production in the grammar.
        assert nt_def.params == gnt_def.params
        new_rhs_list = []
        assert len(nt_def.rhs_list) == 1
        patch_prod = nt_def.rhs_list[0]
        applied = False
        for grammar_prod in gnt_def.rhs_list:
            if eq_productions(grammar, grammar_prod, patch_prod):
                grammar_prod = merge_productions(grammar, grammar_prod, patch_prod)
                applied = True
            new_rhs_list.append(grammar_prod)
        if not applied:
            raise ValueError("{}: Unable to find a matching production for {} in the grammar:\n {}"
                             .format(filename, name, grammar.production_to_str(name, patch_prod)))
        result = gnt_def.with_rhs_list(new_rhs_list)
        nonterminals[name] = result


@dataclass
class GrammarExtension:
    """A collection of grammar extensions, with added code, added traits for the
    action functions.

    """

    target: None
    grammar: typing.List[ExtPatch]
    filename: os.PathLike

    def apply_patch(
            self,
            grammar: Grammar,
            nonterminals: typing.Dict[LenientNt, NtDef]
    ) -> None:
        # A grammar extension is composed of multiple production patches.
        for ext in self.grammar:
            if isinstance(ext, ExtPatch):
                ext.apply_patch(self.filename, grammar, nonterminals)
            else:
                raise ValueError("Extension of type {} not yet supported.".format(ext.__class__))
