"""Render ANSI/SGR-coloured terminal text (e.g. wttr.in) into a Gtk.TextView.

wttr.in returns real terminal escape sequences when curled. We parse the SGR
subset it uses (basic 8/16 colours, xterm-256, truecolor, bold, reset) and emit
Gtk.TextTags so the CLI art keeps its colours and block-glyph alignment. This is
deliberately reusable by any future "show me some CLI art" widget.
"""

from __future__ import annotations

import re

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gdk, Gtk  # noqa: E402

_SGR = re.compile(r"\x1b\[([0-9;]*)m")

# xterm 16-colour base palette (approx, tuned to the CyberQueer dark background).
_BASE16 = [
    (0x00, 0x00, 0x00), (0xCC, 0x24, 0x24), (0x33, 0xCC, 0x33), (0xCC, 0xCC, 0x33),
    (0x33, 0x66, 0xCC), (0xCC, 0x33, 0xCC), (0x33, 0xCC, 0xCC), (0xD6, 0xAB, 0xAB),
    (0x66, 0x66, 0x66), (0xF5, 0x05, 0x05), (0x55, 0xFF, 0x55), (0xFF, 0xFF, 0x55),
    (0x55, 0x88, 0xFF), (0xE4, 0x00, 0x46), (0x55, 0xFF, 0xFF), (0xFF, 0xFF, 0xFF),
]


def _xterm256(n: int) -> tuple[int, int, int]:
    if n < 16:
        return _BASE16[n]
    if n < 232:  # 6x6x6 colour cube
        n -= 16
        r, g, b = n // 36, (n // 6) % 6, n % 6
        conv = lambda c: 55 + c * 40 if c else 0
        return conv(r), conv(g), conv(b)
    v = 8 + (n - 232) * 10  # grayscale ramp
    return v, v, v


def _rgba(rgb: tuple[int, int, int]) -> Gdk.RGBA:
    c = Gdk.RGBA()
    c.red, c.green, c.blue, c.alpha = rgb[0] / 255, rgb[1] / 255, rgb[2] / 255, 1.0
    return c


class AnsiRenderer:
    """Owns a TextView and repaints it from ANSI text. Tags are cached by state."""

    def __init__(self) -> None:
        self.view = Gtk.TextView(
            editable=False, cursor_visible=False, monospace=True,
            wrap_mode=Gtk.WrapMode.NONE,
        )
        self.view.add_css_class("ansi-view")
        self.buffer = self.view.get_buffer()
        self._tag_cache: dict[tuple, Gtk.TextTag] = {}

    def _tag(self, fg, bg, bold) -> Gtk.TextTag | None:
        if fg is None and bg is None and not bold:
            return None
        key = (fg, bg, bold)
        tag = self._tag_cache.get(key)
        if tag is None:
            tag = self.buffer.create_tag()
            if fg is not None:
                tag.set_property("foreground-rgba", _rgba(fg))
            if bg is not None:
                tag.set_property("background-rgba", _rgba(bg))
            if bold:
                tag.set_property("weight", 700)
            self._tag_cache[key] = tag
        return tag

    def set_text(self, text: str) -> None:
        self.buffer.set_text("", 0)
        fg = bg = None
        bold = False
        pos = 0
        for m in _SGR.finditer(text):
            chunk = text[pos:m.start()]
            if chunk:
                self._insert(chunk, fg, bg, bold)
            fg, bg, bold = self._apply(m.group(1), fg, bg, bold)
            pos = m.end()
        tail = text[pos:]
        if tail:
            self._insert(tail, fg, bg, bold)

    def _insert(self, chunk, fg, bg, bold) -> None:
        end = self.buffer.get_end_iter()
        tag = self._tag(fg, bg, bold)
        if tag is None:
            self.buffer.insert(end, chunk)
        else:
            self.buffer.insert_with_tags(end, chunk, tag)

    @staticmethod
    def _apply(params: str, fg, bg, bold):
        codes = [int(x) if x else 0 for x in params.split(";")] if params else [0]
        i = 0
        while i < len(codes):
            c = codes[i]
            if c == 0:
                fg = bg = None
                bold = False
            elif c == 1:
                bold = True
            elif c == 22:
                bold = False
            elif c == 39:
                fg = None
            elif c == 49:
                bg = None
            elif 30 <= c <= 37:
                fg = _BASE16[c - 30]
            elif 90 <= c <= 97:
                fg = _BASE16[c - 90 + 8]
            elif 40 <= c <= 47:
                bg = _BASE16[c - 40]
            elif 100 <= c <= 107:
                bg = _BASE16[c - 100 + 8]
            elif c in (38, 48):
                target = "fg" if c == 38 else "bg"
                if i + 1 < len(codes) and codes[i + 1] == 5:
                    val = _xterm256(codes[i + 2]) if i + 2 < len(codes) else None
                    i += 2
                elif i + 1 < len(codes) and codes[i + 1] == 2:
                    val = tuple(codes[i + 2:i + 5]) if i + 4 < len(codes) else None
                    i += 4
                else:
                    val = None
                if target == "fg":
                    fg = val
                else:
                    bg = val
            i += 1
        return fg, bg, bold
