################################################
# Returns Template Objects Using User Patterns #
################################################

from copy import deepcopy
from typing import Any

from .dtypes.base import MultipliedMeta
from .matrix import Matrix, Slice, empty_rows, matrix_merge, matrix_scatter
from .utils.char import allchars, chargen, chartff
from .utils.pretty import pretty
from .utils.bool import isalpha, ischar, isppm, validate_bitwidth

# -- Template and Slice dependencies  ------------------------------- #


def build_csa(
    char: str, source_slice: Slice
) -> tuple[Slice, Slice]:  # Carry Save Adder -> (template, result)
    """Create CSA template slice with source slice and chosen char.

    Parameters
    ----------
    char : str
        Character to use for the CSA template.
    source_slice : Slice
        Source slice object to use for the CSA template.

    Returns
    -------
    tuple[Slice, Slice]
        Template "slices" marked for a csa reduction and the resulting slice.

    Examples
    --------
    >>> [slice-] || [csa---] || [result]
    >>> ____0000 || ____AaAa || __AaAaAa
    >>> ___0000_ || ___aAaA_ || __aAaA__
    >>> __0000__ || __AaAa__ || ________
    """
    if not ischar(char):
        raise ValueError("Expected character. String length must equal 1")
    if not isinstance(source_slice, Slice):
        raise TypeError(f"Expected type Slice, got {type(source_slice)}")
    if len(source_slice) != 3:
        raise ValueError("Invalid template slice: must be 3 rows")

    # loop setup
    n = len(source_slice[0])
    tff = chartff(char)  # Toggle flip flop
    result = [["_"] * n, ["_"] * n, ["_"] * n]
    csa_slice = deepcopy(source_slice)

    for i in range(n):
        # Generates slice of all possible bit placements, represented
        # with a char. Lower and upper case aide in visualising changes
        # in bit position before, templates, and after, result, operation

        char = next(tff)
        csa_slice[0][i] = char if (y0 := csa_slice[0][i] != "_") else "_"
        csa_slice[1][i] = char if (y1 := csa_slice[1][i] != "_") else "_"
        csa_slice[2][i] = char if (y2 := csa_slice[2][i] != "_") else "_"

        result[0][i] = char if 1 <= (y0 + y1 + y2) else "_"
        result[1][i - 1] = char if 1 < (y0 + y1 + y2) else "_"
    return csa_slice, Slice(result)


def build_adder(
    char: str, source_slice: Slice
) -> tuple[Slice, Slice]:  # Carry Save Adder -> (template, result)
    """Create Adder template slice with zero initialised slice and chosen char.

    Parameters
    ----------
    char : str
        The character to use for the template.
    source_slice : Slice
        The source slice to use for the template.

    Returns
    -------
    tuple[Slice, Slice]
        The template "slices" for addition and the resulting slice.

    Examples
    --------
    >>> [slice-] || [adder-] || [result]
    >>> ___0000_ || ___aAaA_ || _aAaAaA_
    >>> __0000__ || __AaAa__ || ________
    """
    if not ischar(char):
        raise ValueError("Expected character. String length must equal 1")
    if not isinstance(source_slice, Slice):
        raise TypeError(f"Expected type Slice, got {type(source_slice)}")
    if len(source_slice) != 2:
        raise ValueError("Invalid template slice: must be 2 rows")

    # loop setup
    n = len(source_slice[0])
    tff = chartff(char)  # Toggle flip flop
    result = [["_"] * n, ["_"] * n]
    adder_slice = deepcopy(source_slice)  # ensure no references

    for i in range(n):
        # Generates slice of all possible bit placements, represented
        # with a char. Alternating char case aids in visualising changes
        # in bit position before, templates, and after, result, operation

        char = next(tff)
        adder_slice[0][i] = char if (y0 := adder_slice[0][i] != "_") else "_"
        adder_slice[1][i] = char if (y1 := adder_slice[1][i] != "_") else "_"
        result[0][i] = char if y0 or y1 else "_"

    # -- Add final carry -----------------------------------------
    carry = not all(ch == "_" for ch in adder_slice[1])  # sanity check

    # find index of left most instance of char, regardless of case
    index = min(result[0].index(next(tff)), result[0].index(next(tff)))
    if carry and 0 < index:
        result[0][index - 1] = next(tff)  # Final carry place in result template

    return adder_slice, Slice(result)


def build_noop(char: str, source_slice: Slice) -> tuple[Slice, Slice]:
    """Create a No-op template slice with zero initialised slice and chosen char.

    Parameters
    ----------
    char : str
        Character to use for the No-op template.
    source_slice : Slice
        Source slice to use for the No-op template.


    Returns
    -------
    tuple[Slice, Slice]
        Template "slices" and resulting slice. Target row unaffected

    Examples
    --------
    >>> [slice-] || [noop--] || [result]
    >>> ___0000_ || ___aAaA_ || ___aAaA_
    """
    if not ischar(char):
        raise ValueError("Expected character. String length must equal 1")
    if not isinstance(source_slice, Slice):
        raise TypeError(f"Expected type Slice, got {type(source_slice)}")
    if len(source_slice) != 1:
        raise ValueError("Invalid template slice: must be 1 rows")

    n = len(source_slice[0])
    tff = chartff(char)  # Toggle flip flop
    noop_slice = deepcopy(source_slice)  # ensure no references
    for i in range(n):
        noop_slice[0][i] = next(tff) if (noop_slice[0][i] != "_") else "_"

    return noop_slice, deepcopy(noop_slice)  # avoids pointing to same object


def build_empty_slice(source_slice: Slice) -> tuple[Slice, Slice]:
    """Create an empty template slice. Returns template "slices" and resulting slice.
    Variable length, determined by source slice.

    Parameters
    ----------
    source_slice : Slice
        Source slice to use for the empty template.

    Returns
    -------
    tuple[Slice, Slice]
        Tuple of template slices and resulting slice.


    Notes
    -----
    Used for building Templates with large runs of underscore characters.
    Underscores are used to represent empty spaces in the template.

    Examples
    --------
    >>> [slice-] || [empty-] || [result]
    >>> ???????? || ________ || ________
    >>> ???????? || ________ || ________
    >>> ...      || ...      || ...
    """

    if not isinstance(source_slice, Slice):
        raise TypeError(f"Expected type Slice, got {type(source_slice)}")

    empty_slice = deepcopy(source_slice)  # ensure no references
    for row in range(len(source_slice)):
        empty_slice.slice[row] = ["_"] * (empty_slice.bits << 1)
    return empty_slice, deepcopy(empty_slice)


class Pattern(MultipliedMeta):
    """Simplified representation of a Template.

    Parameters
    ----------
    pattern : list[str]
        Pattern to use for the template.

    Attributes
    ----------
    pattern : list[str]
        Pattern to use for the template.
    bits : int
        Number of bits in the pattern.

    """

    def __init__(self, pattern: list[str]) -> None:
        if not (isinstance(pattern, list) and all(ischar(row) for row in pattern)):
            raise ValueError("Invalid pattern format. Expected list[char]")
        self.pattern = pattern
        self.bits = len(pattern)

        self._soft_type = list()
        return None

    def get_runs(self) -> list[tuple[int, int, int]]:
        """Returns list of tuples of length, position, and run of a given char in pattern

        Examples
        --------
        >>> Pattern(["A", "A", "A", "B", "B", "B", "B", "C", "C", "C", "C", "C"]).get_runs()
        [(3, 0, 3), (4, 3, 4), (5, 7, 5)]
        """
        metadata = []
        i = 1
        k = 0
        while i < len(self.pattern):
            run = 1
            while i < len(self.pattern) and self.pattern[i - 1] == self.pattern[i]:
                run += 1
                i += 1
            if run < 4:
                # (arithmetic_unit, starting_row, run_length)
                metadata.append((None, i - run, run))
            else:
                # TODO: Implement Decoders
                # arithmetic_unit = decoder
                # find decoder type
                raise ValueError(f"Unsupported run length {run}")
            i += 1
            k += 1
        return metadata

    def __str__(self) -> str:
        pretty_ = ""
        for p in self.pattern:
            pretty_ += " " + p + "\n"
        return f"{'[' + pretty_[1:-1] + ']'}"

    def __repr__(self) -> str:
        return f"<multiplied.{self.__class__.__name__} object at {hex(id(self))}>"

    def __len__(self) -> int:
        return self.bits

    def __getitem__(self, index: int) -> str:
        return self.pattern[index]


# -- ! [ Preparing For Decoders ] !-------------------------------- #
#
# 1st:
#
#   Implement complex maps, arithmetic unit based isolation and
#   partial product reduction.
#
# 2st: Adopt get_runs() within build_from_pattern()
#
#   get_runs can pass all information needed about a unit then
#   pass it forward.
#
# 3nd: Use named tupes or other structures to pass information
#
#   Improves clarity as library becomes increasingly complex.
#
class Template(MultipliedMeta):
    """A structure representing collections of arithmetic units using characters.
    Generated using a partial product matrix and a Pattern or custom template

    Parameters
    ----------
    source : Pattern | list[list[str]]
        The source of the template.
    result : list[Any], optional
        The result of the template, by default [], automatically computed
    matrix : Any, optional
        The matrix of the template, by default None

    """

    # ! THIS is where checksums need to be implemented ! #
    # > Move checksum logic from Matrix class to Template class
    # > make checksum a named tuple to easily identify x vs y?
    # > passing both checksums will be the first step in optimisation
    #

    def __init__(
        self,
        source: Pattern | list[list[str]],
        *,
        result: list[Any] | Matrix | None = None,
        matrix: Any = None,
    ) -> None:

        validate_bitwidth(len(source))
        self.bits = len(source)
        match result:
            case Matrix():
                self.result = result
            case list():
                if allchars(result) == {}:
                    raise ValueError("Invalid resultant matrix")
                self.result = Matrix(result)
            case None:
                pass
            case _:
                raise TypeError("result must be a Matrix or list[list[str]]")

        # -- pattern handling ---------------------------------------
        if isinstance(source, Pattern):
            self.pattern = source
            if matrix is None:
                matrix = Matrix(self.bits)
            self.build_from_pattern(self.pattern, matrix)

        # -- template handling ---------------------------------------
        elif isinstance(source, list) and all([isinstance(i, list) for i in source]):
            if not isppm(source):
                raise TypeError(f"Expected partial product matrix, got {source}")
            self.template = source
            self._resolve_template_pattern()

        else:
            raise TypeError(f"Expected Pattern or list[list[str]] got {source}")

        if result is None:
            self._reduce_template()

        self.bounds = self.update_bounding_box(self.template)
        self.re_bounds = self.update_bounding_box(self.result.matrix)

        self._soft_type = list()
        return None

    def _resolve_template_pattern(self) -> None:
        """Attempt to resolve pattern from template source"""

        pattern = ["_"] * self.bits
        for i in range(self.bits):
            chars = set(deepcopy(self.template[i]))
            chars = {ch.upper() for ch in chars}
            if 2 < len(chars):
                self.pattern = None
                return None

            for char in chars:
                if char != "_":
                    pattern[i] = char

        self.pattern = Pattern(pattern)
        return None

    def _reduce_template(self) -> None:
        """Produce Template result and it's bounding box."""
        units, bounds = self.collect_template_units()
        re_bound = {}
        results = {}
        chars = list(bounds.keys())
        for ch in chars:
            base_index = bounds[ch][0][1]
            if ch == "_":
                continue

            match bounds[ch][-1][1] - bounds[ch][0][1] + 1:  # row height
                case 1:  # NOOP
                    output = Slice([units[ch][base_index]])

                    re_bound[ch] = bounds[ch]

                case 2:  # ADD
                    unit_slice = Slice(
                        [units[ch][base_index], units[ch][base_index + 1]]
                    )
                    output = build_adder(ch, unit_slice)[1]

                    y = bounds[ch][0][1]
                    x_right = bounds[ch][1][0]
                    x_left = bounds[ch][0][0]
                    while output[0][x_left] != "_" and -1 < x_left:
                        x_left -= 1

                    re_bound[ch] = [(x_left, y), (x_right, y)]

                case 3:  # CSA
                    unit_slice = Slice(
                        [
                            units[ch][base_index],
                            units[ch][base_index + 1],
                            units[ch][base_index + 2],
                        ]
                    )
                    output = build_csa(ch, unit_slice)[1]

                    y = bounds[ch][0][1]
                    x_right = bounds[ch][1][0]
                    x_left = bounds[ch][-2][0]
                    while output[0][x_left] != "_" and -1 < x_left:
                        x_left -= 1

                    re_bound[ch] = [
                        (x_left - 1, y),
                        (x_right, y),
                        (x_left, y + 1),
                        (x_right - 1, y + 1),
                    ]

                case _:
                    raise ValueError(
                        f"Unsupported unit type, len={bounds[ch][-1][1] - bounds[ch][0][1]}"
                    )

            unit_result = [[]] * self.bits
            i = 0
            while i < base_index:
                unit_result[i] = ["_"] * (self.bits << 1)
                i += 1
            for row in output:
                unit_result[i] = row
                i += 1
            while i < self.bits:
                unit_result[i] = ["_"] * (self.bits << 1)
                i += 1
            results[ch] = Matrix(unit_result)

        if 1 < len(results):
            self.result = matrix_merge(results, re_bound)
        else:
            self.result = list(results.values())[0]

        self.re_bounds = self.update_bounding_box(self.result.matrix)

        return None

    # Templates must be built using matrix
    def build_from_pattern(self, pattern: Pattern, matrix: Matrix) -> None:
        """Build a simple template and it's result for a given bitwidth based
        on matrix. Defaults to empty matrix if matrix=None.

        Parameters
        ----------
        pattern : Pattern
            The pattern to build the template from.
        matrix : Matrix
            The matrix to build the template from.

        Returns
        -------
        None

        Raises
        ------
        ValueError
            If the pattern is not a valid Pattern object.

        Examples
        --------
        >>> [matrix] || [pattern] || [templ.] [result]
        >>> ____0000 || [  'a',   || ____AaAa __aAaAaA
        >>> ___0000_ ||    'a',   || ___AaAa_ ________
        >>> __0000__ ||    'b',   || __BbBb__ bBbBbB__
        >>> _0000___ ||    'b'  ] || _BbBb___ ________
        """

        # -- sanity check -------------------------------------------
        if not (isinstance(pattern, Pattern)):
            raise ValueError("Expected Pattern")
        validate_bitwidth(len(pattern))

        # -- find run -----------------------------------------------
        template_slices = {}
        char = chargen()
        i = 1
        while i < len(pattern) + 1:
            run = 1

            while i < len(pattern) and pattern[i - 1] == pattern[i]:
                run += 1
                i += 1

            # -- process noop run -----------------------------------
            if pattern[i - run] == "_":
                allchar = set(pattern.pattern)
                for r in range(1, run + 1):
                    ch = next(char)
                    max_iter = 26

                    while ch.lower() in allchar:
                        if max_iter == 0:
                            raise ValueError("No available characters for template")
                        ch = next(char)
                        max_iter -= 1

                    template_slices[i - r] = build_noop(ch, matrix[i - r])
                    allchar.add(ch)

                i += run  # advance past noop run
                continue

            # -- process arithmetic units ---------------------------
            match run:
                case 1:  # Do nothing
                    template_slices[i - run] = build_noop(
                        pattern[i - run], matrix[i - run : i]
                    )
                case 2:  # Create adder
                    template_slices[i - run] = build_adder(
                        pattern[i - run], matrix[i - run : i]
                    )
                case 3:  # Create CSA row
                    template_slices[i - run] = build_csa(
                        pattern[i - run], matrix[i - run : i]
                    )
                case _:
                    raise ValueError(
                        f"Unsupported run length {run}. Use '_' for empty rows"
                    )

            i += 1

        # -- build template and result ------------------------------
        keys = sorted(template_slices.keys())
        template = []
        result = []
        for k in keys:
            template += template_slices[k][0]
            result += template_slices[k][1]

        self.template, self.result = template, Matrix(result)
        return None

    # ! currently not generalised:
    #  > detect type of transition then use appropriate function
    #
    #  > or just detect empty, '_', characters as the boundary
    #       > This option means figuring out the correct key to use
    def update_bounding_box(
        self, matrix: list[list]
    ) -> dict[str, list[tuple[int, int]]]:
        """Returns dictionary of arithmetic unit and coordinates for their boundaries."""

        rows = self.bits
        items = self.bits << 1
        bounds = {}
        x, y = 0, 0
        while y < rows:
            # -- entry border -------------------------------------------
            key = matrix[y][0].upper()
            if key not in bounds:
                bounds[key] = []
            bounds[key].append((0, y))

            # -- central range ------------------------------------------
            while x < items - 1:
                curr = matrix[y][x].upper()
                next = matrix[y][x + 1].upper()
                if (curr == next) and isalpha(curr):
                    x += 1
                    continue
                if curr != next and (isalpha(curr) or isalpha(next)):
                    if curr not in bounds:
                        bounds[curr] = []
                    bounds[curr].append((x, y))
                    if next not in bounds:
                        bounds[next] = []
                    bounds[next].append((x + 1, y))
                    x += 1
                    continue
                x += 1

            # -- exit border --------------------------------------------
            key = matrix[y][x].upper()
            if key not in bounds:
                bounds[key] = []
            bounds[key].append((x, y))

            x = 0
            y += 1
        return bounds

    # TODO: implement x_checksum (current checksum is y_checksum)
    # IDEA: implement x_signature and maybe y_signature:
    # - A given signature will create a set for all member of an axis
    # - Should help when error checking, though I don't see a use for y_signature
    #
    def collect_template_units(
        self,
    ) -> tuple[dict[str, list], dict[str, list[tuple[int, int]]]]:
        """Return dict of isolated arithmetic units and their bounding box."""

        bounds = self.update_bounding_box(self.template)
        units = matrix_scatter(self.template, bounds)
        return (units, bounds)

    # To be used in complex template results
    def merge(self, templates: list[Any]) -> None:
        """
        Merge multiple template slices into a single template.
        """
        assert isinstance(templates, list)
        # This looks terrible... Works tho?
        # templates[template[row[str]]]
        assert isinstance(templates[0][0][0][0], str)

        if len(templates) == 0:
            raise ValueError("No templates provided")

        self.merged = None  # PLACEHOLDER #
        ...

    def __str__(self) -> str:

        # ! figure out why ``pretty(self.result)`` prints twice
        return f"{pretty(self.template)}\n{pretty(self.result.matrix)}"

    def __repr__(self) -> str:
        return f"<multiplied.{self.__class__.__name__} object at {hex(id(self))}>"

    def __len__(self) -> int:
        return len(self.template)


# TODO: add examples
def resolve_pattern(matrix: Matrix) -> Pattern:
    """For a given matrix, progressively allocate CSAs then adders to pattern

    Parameters
    ----------
    matrix : Matrix
        The matrix to resolve the pattern for.

    Returns
    -------
    Pattern
        The resolved pattern.

    """

    from multiplied.core.utils.char import chargen

    char = chargen()
    if (empty_rows_ := empty_rows(matrix)) == matrix.bits:
        return Pattern(["_"] * matrix.bits)

    # TODO use io.StringIO()
    scope = matrix.bits - empty_rows_
    new_pattern = []
    while 0 < scope:
        ch = next(char)
        n = len(new_pattern)

        if 3 <= scope:
            new_pattern += [ch, ch, ch]
        elif 2 == scope:
            new_pattern += [ch, ch]
        elif 1 == scope:
            new_pattern += [ch]

        scope -= len(new_pattern) - n
    new_pattern += ["_"] * empty_rows_
    return Pattern(new_pattern)


def build_noop_template(self, pattern: Pattern, *, dadda=False) -> None:
    """Create template for zeroed matrix using pattern"""


"""
Complex templates implement decoders and bit-mapping.

Decoders reduce 4 or more bits at a time.

Bit mapping defines where bits are placed in each stage,
enabling complex implementations and possible optimisations.
"""
