"""The building blocks of the TIM language,

Use `pytermgui.markup.parsing.tokenize_markup` and `pytermgui.markup.parsing.tokenize_ansi` to
generate.
"""

from __future__ import annotations

from dataclasses import dataclass
from functools import cached_property
from typing import TYPE_CHECKING, Any, Generator, Iterator

from typing_extensions import TypeGuard

from ..colors import Color

if TYPE_CHECKING:
    from ..fancy_repr import FancyYield

__all__ = [
    "Token",
    "PlainToken",
    "PseudoToken",
    "StyleToken",
    "ColorToken",
    "AliasToken",
    "ClearToken",
    "MacroToken",
    "HLinkToken",
    "CursorToken",
]


class Token:
    """A piece of markup information.

    All tokens must have at least a `value` field, and have `markup` and `prettified_markup`
    properties derived from it in some manner.

    They are meant to be immutable (frozen), and generated by some tokenization. They are also
    static representations of the data in its pre-parsed form.
    """

    value: str

    @cached_property
    def markup(self) -> str:
        """Returns markup representing this token."""

        return self.value

    @cached_property
    def prettified_markup(self) -> str:
        """Returns syntax-highlighted markup representing this token."""

        return f"[{self.markup}]{self.markup}[/{self.markup}]"

    def __eq__(self, other: object) -> bool:
        return isinstance(other, type(self)) and other.value == self.value

    def __repr__(self) -> str:
        return f"<{type(self).__name__} markup: '{self.markup}'>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield f"<{type(self).__name__} markup: "
        yield {
            "text": self.prettified_markup,
            "highlight": False,
        }
        yield ">"

    def is_plain(self) -> TypeGuard["PlainToken"]:
        """Returns True if this token is an instance of PlainToken."""

        return isinstance(self, PlainToken)

    def is_pseudo(self) -> TypeGuard["PseudoToken"]:
        """Returns True if this token is an instance of PseudoToken."""

        return isinstance(self, PseudoToken)

    def is_color(self) -> TypeGuard["ColorToken"]:
        """Returns True if this token is an instance of ColorToken."""

        return isinstance(self, ColorToken)

    def is_style(self) -> TypeGuard["StyleToken"]:
        """Returns True if this token is an instance of StyleToken."""

        return isinstance(self, StyleToken)

    def is_alias(self) -> TypeGuard["AliasToken"]:
        """Returns True if this token is an instance of AliasToken."""

        return isinstance(self, AliasToken)

    def is_macro(self) -> TypeGuard["MacroToken"]:
        """Returns True if this token is an instance of MacroToken."""

        return isinstance(self, MacroToken)

    def is_clear(self) -> TypeGuard["ClearToken"]:
        """Returns True if this token is an instance of ClearToken."""

        return isinstance(self, ClearToken)

    def is_hyperlink(self) -> TypeGuard["HLinkToken"]:
        """Returns True if this token is an instance of HLinkToken."""

        return isinstance(self, HLinkToken)

    def is_cursor(self) -> TypeGuard["CursorToken"]:
        """Returns True if this token is an instance of CursorToken."""

        return isinstance(self, CursorToken)


@dataclass(frozen=True, repr=False)
class PlainToken(Token):
    """A plain piece of text.

    These are the parts of data in-between markup tag groups.
    """

    __slots__ = ("value",)

    value: str

    def __repr__(self) -> str:
        return f"<{type(self).__name__} markup: {self.markup!r}>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield f"<{type(self).__name__} markup: {self.markup!r}>"


@dataclass(frozen=True, repr=False)
class PseudoToken(Token):
    """A token that can modify it's context, but doesn't hold information of its own."""

    value: str

    @cached_property
    def prettified_markup(self) -> str:
        return f"[245 italic]{self.markup}[/]"


@dataclass(frozen=True, repr=False)
class ColorToken(Token):
    """A color identifier.

    It stores the markup that created it, as well as the `pytermgui.colors.Color` object
    that it represents.
    """

    __slots__ = ("value",)

    value: str
    color: Color

    @cached_property
    def markup(self) -> str:
        return self.color.markup

    @cached_property
    def prettified_markup(self) -> str:
        clearer = "bg" if self.color.background else "fg"

        return f"[{self.markup}]{self.markup}[/{clearer}]"


@dataclass(frozen=True, repr=False)
class StyleToken(Token):
    """A terminal-style identifier.

    Most terminals support a set of 9 styles:

    - bold
    - dim
    - italic
    - underline
    - blink
    - blink2
    - inverse
    - invisible
    - strikethrough

    This token will store the style it represents by its name in the `value` field. Note
    that other, less widely supported styles *may* be available; for an up-to-date list,
    run `ptg -i pytermgui.markup.style_maps.STYLES`.
    ```

    """

    __slots__ = ("value",)

    value: str


@dataclass(frozen=True, repr=False)
class ClearToken(Token):
    """A tag-clearer.

    These tokens are prefixed by `/`, and followed by the name of the tag they target.

    To reset color information in the current text, use the `/fg` and `/bg` special
    tags. We cannot unset a specific color due to how the terminal works; all these do
    is "reset" the current stroke color to the default of the terminal.

    Additionally, there are some other special identifiers:

    - `/`:  Clears all tags, including styles, colors, macros, links and more.
    - `/!`: Clears all currently applied macros.
    - `/~`: Clears all currently applied links.
    """

    __slots__ = ("value",)

    value: str

    @cached_property
    def prettified_markup(self) -> str:
        target = self.markup[1:]

        return f"[210 strikethrough]/[/fg]{target}[/]"

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Token):
            return False

        return super().__eq__(other) or all(
            obj.markup in ["/dim", "/bold"] for obj in [self, other]
        )

    def targets(  # pylint: disable=too-many-return-statements
        self, token: Token
    ) -> bool:
        """Returns True if this token targets the one given as an argument."""

        if token.is_clear() or token.is_cursor():
            return False

        if self.value in ("/", f"/{token.value}"):
            return True

        if token.is_hyperlink() and self.value == "/~":
            return True

        if token.is_macro() and self.value == "/!":
            return True

        if not Token.is_color(token):
            return False

        if self.value == "/fg" and not token.color.background:
            return True

        return self.value == "/bg" and token.color.background


@dataclass(frozen=True, repr=False)
class AliasToken(Token):
    """A way to reference a set of tags from one central name."""

    __slots__ = ("value",)

    value: str


@dataclass(frozen=True, repr=False)
class MacroToken(Token):
    """A binding of a Python function to a markup name.

    See the docs on information about syntax & semantics.
    """

    __slots__ = ("value", "arguments")

    value: str
    arguments: tuple[str, ...]

    def __iter__(self) -> Iterator[Any]:
        return iter((self.value, self.arguments))

    @cached_property
    def prettified_markup(self) -> str:
        target = self.markup[1:]

        return f"[210 bold]![/]{target}"

    @cached_property
    def markup(self) -> str:
        return f"{self.value}" + (
            f"({':'.join(self.arguments)})" if len(self.arguments) > 0 else ""
        )


@dataclass(frozen=True, repr=False)
class HLinkToken(Token):
    """A terminal hyperlink.

    See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda.
    """

    __slots__ = ("value",)

    value: str

    @cached_property
    def markup(self) -> str:
        return f"~{self.value}"

    @cached_property
    def prettified_markup(self) -> str:
        return f"[{self.markup}]~[blue underline]{self.value}[/fg /underline /~]"


@dataclass(frozen=True, repr=False)
class CursorToken(Token):
    """A cursor location.

    These can be used to move the terminal's cursor.
    """

    __slots__ = ("value", "y", "x")

    value: str
    y: int | None
    x: int | None

    def __iter__(self) -> Iterator[int | None]:
        return iter((self.y, self.x))

    def __repr__(self) -> str:
        return f"<{type(self).__name__} position: {(';'.join(map(str, self)))}>"

    def __fancy_repr__(self) -> Generator[FancyYield, None, None]:
        yield self.__repr__()

    @cached_property
    def markup(self) -> str:
        return f"({self.value})"
