import copy
from difflib import unified_diff
import re

from typing import TYPE_CHECKING

from finesse.exceptions import FinesseException
from finesse.env import is_interactive

if TYPE_CHECKING:
    from finesse.components.general import Connector


class MissingReplacementError(FinesseException):
    pass


class ConnectorReplacer:

    def __init__(self, katscript, connector: "Connector") -> None:
        self.katscript: str = katscript
        self.connector: "Connector" = connector

    def replace(
        self,
        sub: str,
        component: str | None = None,
        optical_ports: None | list[str] = None,
        mechanical_ports: None | list[str] = None,
        electrical_ports: None | list[str] = None,
        verbose: bool = False,
    ) -> str:
        # prevents merging two lines together
        if not sub.endswith("\n"):
            sub += "\n"

        # don't override, so you could call this multiple times
        new_kat = copy.copy(self.katscript)

        new_kat = self._replace_definition_line(new_kat, sub)
        new_kat = self._replace_ports_and_nodes(
            new_kat, optical_ports, mechanical_ports, electrical_ports
        )
        new_kat = self._replace_component_name(new_kat, component)

        # TODO maybe not necessary?
        new_kat = new_kat.replace(
            "# Items below could not be matched to original script, or were not present when the model was originally parsed.",
            "",
        )
        new_kat = new_kat.strip()
        if verbose:
            print_diff(self.katscript.strip(), new_kat)
        return new_kat

    def _replace_definition_line(self, old_kat: str, sub: str) -> str:
        # prevent circular import
        from finesse.script.spec import KATSPEC

        aliases = KATSPEC.elements[self.connector.__class__.__name__].aliases
        found = False
        # replace the line constructing the connector we are replacing
        for alias in aliases:
            new_kat = re.sub(
                f"^[ \f\t]*?{alias}[ \f\t]+{self.connector.name}(?=[\\s#]).*?$\n?",
                sub,
                old_kat,
                flags=re.MULTILINE,
            )
            if new_kat != old_kat:
                found = True
                break
        if found is False:
            raise FinesseException(
                f"Could not find definition line for '{self.connector}'"
            )
        return new_kat

    def _replace_ports_and_nodes(
        self, new_kat, optical_ports, mechanical_ports, electrical_ports
    ) -> str:
        # TODO this also replaces ports as part of nodes, which may not be what the
        # user intended?
        connector_ports = (
            self.connector.optical_ports,
            self.connector.mechanical_ports,
            self.connector.electrical_ports,
        )
        replacement_ports = (
            optical_ports,
            mechanical_ports,
            electrical_ports,
        )
        for conn_ports, repl_ports in zip(connector_ports, replacement_ports):
            for i, port in enumerate(conn_ports):
                if port.full_name in new_kat:
                    if repl_ports is None:
                        raise MissingReplacementError(
                            f"No replacement port for '{port.full_name}'"
                        )
                    try:
                        replace_port = repl_ports[i]
                    except IndexError:
                        raise MissingReplacementError(
                            f"No replacement port for '{port.full_name}'"
                        )
                    new_kat = new_kat.replace(port.full_name, replace_port)
        return new_kat

    def _replace_component_name(
        self, new_kat: str, component: str | None = None
    ) -> str:
        # replace references to the component name itself (like in a pendulum)
        # we have to use regex patterns, since, unlike port/node names, a component
        # name can be used inside another name (m m1, m m1_AR)

        lines = new_kat.splitlines(keepends=True)
        patterns = [
            " {} ",  # name surrounded by whitespace
            " {}\n",  # name surrounded by whitespace and newline
            " {}#",  # name directly followed by a comment
            "={} ",  # equal sign and whitespace
            "={}\n",  # equal sign and newline
            "={}#",  # equal sign and comment
            "{}.",  # component parameter being accessed (like in symbolics)
        ]
        new_kat = ""
        for line in lines:
            for pattern in patterns:
                search = pattern.format(self.connector.name)
                if search in line:
                    if component is None:
                        raise ValueError(
                            f"No replacement for '{self.connector.name}' in line '{line}'"
                        )
                    replace = pattern.format(component)
                    line = line.replace(search, replace)
            new_kat += line
        return new_kat


def red(text, html=False):
    if html:
        return f'<span style="color: red">{text}</span>'
    else:
        return f"\033[38;2;255;0;0m{text}\033[38;2;255;255;255m"


def green(text, html=False):
    if html:
        return f'<span style="color: green">{text}</span>'
    else:
        return f"\033[38;2;0;255;0m{text}\033[38;2;255;255;255m"


def print_diff(old: str, new: str) -> None:
    html = is_interactive()
    for line in unified_diff(old.splitlines(), new.splitlines()):
        if line.startswith("-") and not line[1] == "-":
            line = red(line, html)
        elif line.startswith("+") and not line[1] == "+":
            line = green(line, html)
        if html:
            from IPython.display import display, HTML

            display(HTML(line))
        else:
            print(line)
