"""
Bigyo renderer file
"""

from abc import ABC, abstractmethod
from typing import Optional
from wcwidth import wcswidth, wcwidth


class BigyoRenderer(ABC):
    """
    Abstract Base Bigyo rendering strategy.

    Bigyo rendering strategy defines how :class:`Bigyo` generates side by side comparison lines.

    :param sep: Separator for separate two compared lines, defaults to "|"
    :param mark_unchanged: Flag to decide :class:`Bigyo` whether it passes line as-is
                          or mark unchanged line with "  " indicator, defaultes to False
    """
    def __init__(self, sep: str="|", mark_unchanged: bool=False):
        self.maxlen = -1
        self.sep = sep
        self.mark_unchanged = mark_unchanged

    @staticmethod
    def _string_width(string: str) -> int:
        """
        Helper function to calculate string width displayed on console.

        :param string: String to calculate width
        :return: Width of the string
        :raises ValueError: When string includes control character
        """
        string_width = wcswidth(string)
        if string_width == -1:
            raise ValueError("Control character was included in string"\
                             "which has unknown effect while printing.")
        return string_width

    def _join_with_spaces(self, left: str, right: str) -> str:
        """
        Helper function to join two string with separater `self.sep`.

        Spacing is added to make it look nicer.

        :param left: Left string to be printed
        :param right: Right string to be printed
        :return: joined string with appropriate spacing and separater
        :raises ValueError: When string includes control character
        """
        left_width = BigyoRenderer._string_width(left)
        if wcswidth(right) == -1:
            raise ValueError("Control character was included in string"\
                             "which has unknown effect while printing.")
        return f"{left}{' '*(self.maxlen - left_width)}{self.sep}{right}" + "\n"

    @staticmethod
    def _replace_unicode_match(string: str, replace: str) -> str:
        """
        Helper function to make replace string match the visual length of the string.

        :param string: Original string to match
        :param replace: Replace string to modify
        :return: Replace string which look *nicer*
        """
        ret = ""
        for next_char, next_replace in zip(string, replace):
            if wcwidth(next_char) == 1:
                ret += next_replace
            elif wcwidth(next_char) == 2:
                ret += next_replace * 2
        return ret


    def render(self, *, left: str="", right: str="",\
               left_replace: Optional[str]=None, right_replace: Optional[str]=None) -> str:
        """
        Function to actually build comparison lines.
        DO NOT inherit it; inherit `_render` instead, which actually renders.

        :param left: Left string compared
        :param right: Right string compared
        :param left_replace: Diff line generated by :class:`Bigyo`
                            for `left`, defaults to None
        :param right_replace: Diff line generated by :class:`Bigyo`
                            for `right`, defaults to None
        :return: Compared line
        :raises ValueError: When self.maxlen is not set
        """
        if self.maxlen == -1:
            raise ValueError("Maxlen must be set.")
        return self._render(left=left, right=right, left_replace=left_replace, right_replace=right_replace)

    @abstractmethod
    def _render(self, *, left: str="", right: str="",\
               left_replace: Optional[str]=None, right_replace: Optional[str]=None) -> str:
        pass

class SimpleBigyoRenderer(BigyoRenderer):
    """
    Simple Bigyo rendering stratgy.

    Will produce side-by-side comparison, with difference is displayed as separated line.

    :param sep: Separator for separate two compared lines, defaults to "|"
    :param mark_unchanged: Flag to decide :class:`Bigyo` whether it passes line as-is
                          or mark unchanged line with "  " indicator, defaultes to False
    """
    def _render(self, *, left: str="", right: str="",\
               left_replace: Optional[str]=None, right_replace: Optional[str]=None) -> str:
        diff_line = self._join_with_spaces(left, right)
        if any([left_replace, right_replace]):
            diff_line += self._join_with_spaces(
                "" if left_replace is None else BigyoRenderer._replace_unicode_match(left, left_replace),
                "" if right_replace is None else BigyoRenderer._replace_unicode_match(right, right_replace),
                )
        return diff_line


class OnelineBigyoRenderer(BigyoRenderer):
    """
    One-line Bigyo rendering stratgy.

    Will produce side-by-side comparison, with difference is displayed in-line with marks.

    Mark is changable by __init__ method.

    Default mark is ("<", ">") for added difference, (">", "<") for removed difference.

    So <added difference will be shown like this>, and >removed difference will be shown like this<.

    As mark is added, string width can be enlarged unpredictably. Margin is added to solve this problem.

    :param sep: Separator for separate two compared lines, defaults to "|"
    :param mark_unchanged: Flag to decide :class:`Bigyo` whether it passes line as-is
                          or mark unchanged line with "  " indicator, defaultes to True
    :param add_mark: Characters to mark range of added difference, defaultes to ("<", ">")
    :param delete_mark: Characters to mark range of removed difference, defaultes to (">", "<")
    :param margin: Additional space to deal with enlarged width problem, defaults to 2
    """
    def __init__(self, sep="|", mark_unchanged=True,\
                 add_mark: tuple[str, str]=("<", ">"), delete_mark: tuple[str, str]=(">", "<"), margin: int=2):
        super().__init__(sep, mark_unchanged)
        self.add_mark = add_mark
        self.delete_mark = delete_mark
        self.margin = margin

    def _render(self, *, left: str = "", right: str = "",\
               left_replace: Optional[str] = None, right_replace: Optional[str] = None) -> str:
        delete = 0
        add = 1
        def combine_str(string, operation, place:list[bool] = None):
            if place is None:
                place = [True] * len(string)
            assert len(place) == len(string)

            combined: str = ""
            in_editing: bool = False

            opener = self.delete_mark[0] if operation == delete else self.add_mark[0]
            closer = self.delete_mark[1] if operation == delete else self.add_mark[1]
            for next_char, is_combined in zip(string, place):
                if is_combined != in_editing:
                    if not in_editing:
                        combined += opener
                    else:
                        combined += closer
                    in_editing = not in_editing
                combined += next_char
            if in_editing:
                combined += closer
            return combined

        self.maxlen += self.margin
        processed: list[str] = ["", ""]
        for i, (string, cue) in enumerate([(left, left_replace), (right, right_replace)]):
            if string == "":
                processed[i] = ""
                continue
            line_cue, string = string[:2], string[2:]
            cue_place = [False] * len(string)
            if cue is None:
                cue_place = [True] * len(string)
            else:
                cue = cue[2:]
                for j, next_char in enumerate(cue):
                    cue_place[j] = (next_char != " ")

            if line_cue == "  ":
                processed[i] = string
            elif line_cue == "- ":
                processed[i] = combine_str(string, delete, cue_place)
            elif line_cue == "+ ":
                processed[i] = combine_str(string, add, cue_place)
        res = self._join_with_spaces(*processed)
        self.maxlen -= self.margin
        return res


class VerticalBigyoRenderer(BigyoRenderer):
    """
    Vertical Renderer for narrow screen.
    Separator will be used for separate lines.

    :param sep: Separator for separate two compared lines, defaults to "-"
    :param mark_unchanged: Flag to decide :class:`Bigyo` whether it passes line as-is
                          or mark unchanged line with "  " indicator, defaultes to False
    """

    def __init__(self, sep: str="-", mark_unchanged: bool=False):
        super().__init__(sep, mark_unchanged)

    def _render(self, *, left: str="", right: str="",\
            left_replace: Optional[str]=None, right_replace: Optional[str]=None) -> str:
        processed = ""
        processed += "< " + left + "\n"
        if left_replace:
            processed += "< " + BigyoRenderer._replace_unicode_match(left, left_replace) + "\n"
        processed += "> " + right + "\n"
        if right_replace:
            processed += "> " + BigyoRenderer._replace_unicode_match(right, right_replace) + "\n"
        processed += self.sep * (self.maxlen + 2)
        processed += "\n"
        return processed
