import re
from typing import Any, Dict, Optional, Tuple

from PySide6.QtWidgets import QFrame
from PySide6.QtCore import QSize, Qt, QRectF, QPointF
from PySide6.QtGui import (
    QPainter, QColor, QPen, QPixmap,
    QBrush, QLinearGradient, QPainterPath
)




# =============================================================================
# Helper: StyleDecorator (border + radius + background + cache pixmap)
# =============================================================================

class StyleDecorator:
    """
    Helper xử lý style dạng sx cho QWidget:
      - Parse sx 1 lần (tránh parse/alloc trong paintEvent)
      - Render ra pixmap cache theo (size, dpr, styleKey)
      - Paint: chỉ drawPixmap (nhẹ)

    Hỗ trợ:
      - border (4 cạnh khác nhau hoặc đồng nhất)
      - border-radius (uniform hoặc theo từng góc)
      - background: color / linear-gradient(...)
    """

    def __init__(self, owner: QFrame, sx: Optional[Dict[str, Any]] = None):
        self._owner = owner
        self._sx: Dict[str, Any] = sx or {}

        self._border_spec: Dict[str, Dict[str, Any]] = {}
        self._radius_spec: Dict[str, float] = {}
        self._bg_spec: Dict[str, Any] = {}

        self._cache_size: Optional[Tuple[int, int]] = None
        self._cache_dpr: Optional[float] = None
        self._cache_key: Optional[str] = None
        self._pixmap: Optional[QPixmap] = None

        self.set_sx(self._sx)

    # ------------------------- Public API -------------------------

    def set_sx(self, sx: Dict[str, Any]):
        self._sx = sx or {}
        self._border_spec = self._parse_border_sx(self._sx)
        self._radius_spec = self._parse_radius_sx(self._sx)
        self._bg_spec = self._parse_background_sx(self._sx)
        self.invalidate_cache()

    def invalidate_cache(self):
        self._cache_size = None
        self._cache_dpr = None
        self._cache_key = None
        self._pixmap = None

    def paint(self, painter: QPainter, rect_size: QSize, dpr: float):
        pm = self._get_pixmap(rect_size, dpr)
        if pm is None or pm.isNull():
            return
        painter.drawPixmap(0, 0, pm)

    # ------------------------- Enum helpers (PySide6-safe) -------------------------

    def _enum_to_int(self, v) -> int:
        try:
            if isinstance(v, int):
                return v
            if hasattr(v, "value"):
                return int(v.value)
            return int(v)
        except Exception:
            return 0

    # ------------------------- Parsing utils -------------------------

    def _parse_px(self, s: str) -> float:
        if not isinstance(s, str):
            return 0.0
        m = re.search(r"(-?\d+(?:\.\d+)?)\s*px", s.strip(), re.I)
        if not m:
            m = re.search(r"(-?\d+(?:\.\d+)?)", s.strip())
        if not m:
            return 0.0
        try:
            return max(0.0, float(m.group(1)))
        except Exception:
            return 0.0

    def _parse_color(self, v: Any) -> QColor:
        if v is None:
            return QColor(0, 0, 0, 0)
        if isinstance(v, QColor):
            return v
        if isinstance(v, (tuple, list)) and len(v) in (3, 4):
            r, g, b = int(v[0]), int(v[1]), int(v[2])
            a = int(v[3]) if len(v) == 4 else 255
            return QColor(r, g, b, max(0, min(255, a)))

        if not isinstance(v, str):
            return QColor(0, 0, 0, 0)

        s = v.strip()

        m = re.search(
            r"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([0-9.]+)\s*)?\)",
            s, re.I
        )
        if m:
            r = int(m.group(1))
            g = int(m.group(2))
            b = int(m.group(3))
            a_raw = m.group(4)
            if a_raw is None:
                a = 255
            else:
                a_f = float(a_raw)
                a = int(round(a_f * 255)) if a_f <= 1.0 else int(round(a_f))
                a = max(0, min(255, a))
            return QColor(r, g, b, a)

        if s.startswith("#"):
            c = QColor(s)
            if c.isValid():
                return c

        c = QColor(s)
        if c.isValid():
            return c

        return QColor(0, 0, 0, 0)

    def _parse_pen_style_int(self, s: str) -> int:
        ss = (s or "").lower()
        if "dashed" in ss:
            return self._enum_to_int(Qt.PenStyle.DashLine)
        if "dotted" in ss:
            return self._enum_to_int(Qt.PenStyle.DotLine)
        return self._enum_to_int(Qt.PenStyle.SolidLine)

    # ------------------------- Border parsing -------------------------

    def _parse_border_value(self, v: Any) -> Dict[str, Any]:
        """
        Output: {"w": int, "color": QColor, "style": int(enum_value)}
        """
        solid_i = self._enum_to_int(Qt.PenStyle.SolidLine)

        if v is None:
            return {"w": 0, "color": QColor(0, 0, 0, 0), "style": solid_i}

        if isinstance(v, (int, float)):
            w = max(0, int(round(v)))
            return {"w": w, "color": QColor(0, 0, 0, 255), "style": solid_i}

        if not isinstance(v, str):
            return {"w": 0, "color": QColor(0, 0, 0, 0), "style": solid_i}

        s = v.strip()
        w = int(round(self._parse_px(s)))
        style_i = self._parse_pen_style_int(s)

        m_rgba = re.search(r"rgba?\([^)]+\)", s, re.I)
        if m_rgba:
            color = self._parse_color(m_rgba.group(0))
        else:
            m_hex = re.search(r"(#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8}))", s)
            if m_hex:
                color = self._parse_color(m_hex.group(1))
            else:
                color = QColor(0, 0, 0, 255)

        return {"w": w, "color": color, "style": style_i}

    def _parse_border_sx(self, sx: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
        base = self._parse_border_value(sx.get("border") or sx.get("borderWidth") or sx.get("border-width"))

        def pick(*keys):
            for k in keys:
                if k in sx and sx[k] is not None:
                    return sx[k]
            return None

        left_v = pick("borderLeft", "border-left", "borderLeftWidth", "border-left-width")
        right_v = pick("borderRight", "border-right", "borderRightWidth", "border-right-width")
        top_v = pick("borderTop", "border-top", "borderTopWidth", "border-top-width")
        bottom_v = pick("borderBottom", "border-bottom", "borderBottomWidth", "border-bottom-width")

        left = self._parse_border_value(left_v) if left_v is not None else base
        right = self._parse_border_value(right_v) if right_v is not None else base
        top = self._parse_border_value(top_v) if top_v is not None else base
        bottom = self._parse_border_value(bottom_v) if bottom_v is not None else base

        return {"left": left, "right": right, "top": top, "bottom": bottom}

    # ------------------------- Radius parsing -------------------------

    def _parse_radius_sx(self, sx: Dict[str, Any]) -> Dict[str, float]:
        base = sx.get("borderRadius", sx.get("border-radius", 0))
        base_r = float(base) if isinstance(base, (int, float)) else float(self._parse_px(str(base)))
        base_r = max(0.0, base_r)

        def pick_radius(*keys) -> float:
            for k in keys:
                if k in sx and sx[k] is not None:
                    v = sx[k]
                    if isinstance(v, (int, float)):
                        return max(0.0, float(v))
                    return max(0.0, float(self._parse_px(str(v))))
            return base_r

        tl = pick_radius("borderTopLeftRadius", "border-top-left-radius")
        tr = pick_radius("borderTopRightRadius", "border-top-right-radius")
        br = pick_radius("borderBottomRightRadius", "border-bottom-right-radius")
        bl = pick_radius("borderBottomLeftRadius", "border-bottom-left-radius")

        return {"tl": tl, "tr": tr, "br": br, "bl": bl}

    # ------------------------- Background parsing -------------------------

    def _parse_linear_gradient(self, s: str) -> Optional[Dict[str, Any]]:
        s = s.strip()
        if not s.lower().startswith("linear-gradient(") or not s.endswith(")"):
            return None

        inner = s[len("linear-gradient("):-1].strip()
        parts = [p.strip() for p in inner.split(",") if p.strip()]
        if len(parts) < 2:
            return None

        direction = "to bottom"
        colors = parts

        first = parts[0].lower()
        if first.startswith("to ") or first.endswith("deg"):
            direction = parts[0]
            colors = parts[1:]

        if len(colors) < 2:
            return None

        c1 = self._parse_color(colors[0])
        c2 = self._parse_color(colors[1])

        return {"type": "linear-gradient", "dir": direction, "c1": c1, "c2": c2}

    def _parse_background_sx(self, sx: Dict[str, Any]) -> Dict[str, Any]:
        v = sx.get("background", None)
        if v is None:
            v = sx.get("backgroundColor", sx.get("background-color", None))

        if v is None:
            return {"type": "none"}

        if isinstance(v, str):
            grad = self._parse_linear_gradient(v)
            if grad:
                return grad

        return {"type": "color", "color": self._parse_color(v)}

    # ------------------------- Cache key -------------------------

    def _style_key(self) -> str:
        b = self._border_spec
        r = self._radius_spec
        bg = self._bg_spec

        def border_one(side: str) -> str:
            s = b.get(side, {})
            c: QColor = s.get("color", QColor(0, 0, 0, 0))
            style_i = self._enum_to_int(s.get("style", self._enum_to_int(Qt.PenStyle.SolidLine)))
            return f"{side}:{int(s.get('w', 0) or 0)}:{style_i}:{c.rgba()}"

        b_key = "|".join([border_one("left"), border_one("top"), border_one("right"), border_one("bottom")])
        r_key = f"r:{r.get('tl',0):.2f},{r.get('tr',0):.2f},{r.get('br',0):.2f},{r.get('bl',0):.2f}"

        if bg.get("type") == "none":
            bg_key = "bg:none"
        elif bg.get("type") == "color":
            c: QColor = bg.get("color", QColor(0, 0, 0, 0))
            bg_key = f"bg:color:{c.rgba()}"
        else:
            c1: QColor = bg.get("c1", QColor(0, 0, 0, 0))
            c2: QColor = bg.get("c2", QColor(0, 0, 0, 0))
            bg_key = f"bg:grad:{bg.get('dir','')}:{c1.rgba()}:{c2.rgba()}"

        return f"{b_key}||{r_key}||{bg_key}"

    # ------------------------- Pixmap render -------------------------

    def _get_pixmap(self, size: QSize, dpr: float) -> Optional[QPixmap]:
        w = max(0, int(size.width()))
        h = max(0, int(size.height()))
        if w <= 0 or h <= 0:
            return None

        key = self._style_key()

        if (
            self._pixmap is not None
            and self._cache_size == (w, h)
            and self._cache_dpr == dpr
            and self._cache_key == key
        ):
            return self._pixmap

        pm = QPixmap(int(w * dpr), int(h * dpr))
        pm.setDevicePixelRatio(dpr)
        pm.fill(Qt.GlobalColor.transparent)

        p = QPainter(pm)
        p.setRenderHint(QPainter.RenderHint.Antialiasing, True)

        self._render(p, float(w), float(h))

        p.end()

        self._pixmap = pm
        self._cache_size = (w, h)
        self._cache_dpr = dpr
        self._cache_key = key
        return pm

    def _render(self, p: QPainter, w: float, h: float):
        self._render_background(p, w, h)
        self._render_border(p, w, h)

    def _rounded_path(self, rect: QRectF, r: Dict[str, float]) -> QPainterPath:
        tl = float(r.get("tl", 0.0))
        tr = float(r.get("tr", 0.0))
        br = float(r.get("br", 0.0))
        bl = float(r.get("bl", 0.0))

        max_rx = rect.width() / 2.0
        max_ry = rect.height() / 2.0
        tl = min(tl, max_rx, max_ry)
        tr = min(tr, max_rx, max_ry)
        br = min(br, max_rx, max_ry)
        bl = min(bl, max_rx, max_ry)

        x, y, rw, rh = rect.x(), rect.y(), rect.width(), rect.height()
        path = QPainterPath()

        path.moveTo(x + tl, y)
        path.lineTo(x + rw - tr, y)
        if tr > 0:
            path.quadTo(x + rw, y, x + rw, y + tr)
        else:
            path.lineTo(x + rw, y)

        path.lineTo(x + rw, y + rh - br)
        if br > 0:
            path.quadTo(x + rw, y + rh, x + rw - br, y + rh)
        else:
            path.lineTo(x + rw, y + rh)

        path.lineTo(x + bl, y + rh)
        if bl > 0:
            path.quadTo(x, y + rh, x, y + rh - bl)
        else:
            path.lineTo(x, y + rh)

        path.lineTo(x, y + tl)
        if tl > 0:
            path.quadTo(x, y, x + tl, y)
        else:
            path.lineTo(x, y)

        path.closeSubpath()
        return path

    def _render_background(self, p: QPainter, w: float, h: float):
        bg = self._bg_spec
        if bg.get("type") == "none":
            return

        rect = QRectF(0.0, 0.0, w, h)
        r = self._radius_spec
        has_radius = any((r.get("tl", 0), r.get("tr", 0), r.get("br", 0), r.get("bl", 0)))

        if has_radius:
            path = self._rounded_path(rect, r)
            p.save()
            p.setClipPath(path)

        if bg.get("type") == "color":
            c: QColor = bg.get("color", QColor(0, 0, 0, 0))
            p.fillRect(rect, QBrush(c))
        elif bg.get("type") == "linear-gradient":
            grad = self._build_linear_gradient(w, h, bg.get("dir", "to bottom"), bg.get("c1"), bg.get("c2"))
            p.fillRect(rect, QBrush(grad))

        if has_radius:
            p.restore()

    def _build_linear_gradient(self, w: float, h: float, direction: str, c1: QColor, c2: QColor) -> QLinearGradient:
        direction = (direction or "").strip().lower()
        c1 = c1 if isinstance(c1, QColor) else QColor(0, 0, 0, 0)
        c2 = c2 if isinstance(c2, QColor) else QColor(0, 0, 0, 0)

        x1, y1, x2, y2 = 0.0, 0.0, 0.0, h

        if direction.startswith("to "):
            if "right" in direction:
                x1, y1, x2, y2 = 0.0, 0.0, w, 0.0
            elif "left" in direction:
                x1, y1, x2, y2 = w, 0.0, 0.0, 0.0
            elif "top" in direction:
                x1, y1, x2, y2 = 0.0, h, 0.0, 0.0
            else:
                x1, y1, x2, y2 = 0.0, 0.0, 0.0, h
        elif direction.endswith("deg"):
            try:
                import math
                deg = float(direction.replace("deg", "").strip())
                rad = math.radians(deg)
                vx = math.sin(rad)
                vy = -math.cos(rad)
                cx, cy = w / 2.0, h / 2.0
                half = max(w, h) / 2.0
                x1, y1 = cx - vx * half, cy - vy * half
                x2, y2 = cx + vx * half, cy + vy * half
            except Exception:
                x1, y1, x2, y2 = 0.0, 0.0, 0.0, h

        g = QLinearGradient(QPointF(x1, y1), QPointF(x2, y2))
        g.setColorAt(0.0, c1)
        g.setColorAt(1.0, c2)
        return g

    def _border_is_uniform(self) -> bool:
        s = self._border_spec
        a = s["left"]
        for k in ("top", "right", "bottom"):
            b = s[k]
            if int(a.get("w", 0) or 0) != int(b.get("w", 0) or 0):
                return False
            if int(a.get("style", 0) or 0) != int(b.get("style", 0) or 0):
                return False
            ca: QColor = a.get("color", QColor(0, 0, 0, 0))
            cb: QColor = b.get("color", QColor(0, 0, 0, 0))
            if ca.rgba() != cb.rgba():
                return False
        return int(a.get("w", 0) or 0) > 0

    def _render_border(self, p: QPainter, w: float, h: float):
        s = self._border_spec
        lw = int(s["left"].get("w", 0) or 0)
        rw = int(s["right"].get("w", 0) or 0)
        tw = int(s["top"].get("w", 0) or 0)
        bw = int(s["bottom"].get("w", 0) or 0)

        if (lw + rw + tw + bw) <= 0:
            return

        if self._border_is_uniform():
            self._render_uniform_border(p, w, h)
            return

        x_left = lw / 2.0 if lw > 0 else 0.0
        x_right = (w - rw / 2.0) if rw > 0 else w
        y_top = tw / 2.0 if tw > 0 else 0.0
        y_bottom = (h - bw / 2.0) if bw > 0 else h

        def set_pen(side_spec: Dict[str, Any]):
            bw_ = int(side_spec.get("w", 0) or 0)
            col: QColor = side_spec.get("color", QColor(0, 0, 0, 255))
            style_i = int(side_spec.get("style", self._enum_to_int(Qt.PenStyle.SolidLine)) or 0)
            pen = QPen(col, bw_, Qt.PenStyle(style_i))
            pen.setCapStyle(Qt.PenCapStyle.SquareCap)
            p.setPen(pen)

        if tw > 0:
            set_pen(s["top"])
            p.drawLine(QPointF(x_left, y_top), QPointF(x_right, y_top))
        if bw > 0:
            set_pen(s["bottom"])
            p.drawLine(QPointF(x_left, y_bottom), QPointF(x_right, y_bottom))
        if lw > 0:
            set_pen(s["left"])
            p.drawLine(QPointF(x_left, y_top), QPointF(x_left, y_bottom))
        if rw > 0:
            set_pen(s["right"])
            p.drawLine(QPointF(x_right, y_top), QPointF(x_right, y_bottom))

    def _render_uniform_border(self, p: QPainter, w: float, h: float):
        s = self._border_spec["left"]
        bw = int(s.get("w", 0) or 0)
        if bw <= 0:
            return

        col: QColor = s.get("color", QColor(0, 0, 0, 255))
        style_i = int(s.get("style", self._enum_to_int(Qt.PenStyle.SolidLine)) or 0)
        pen = QPen(col, bw, Qt.PenStyle(style_i))
        pen.setCapStyle(Qt.PenCapStyle.SquareCap)
        p.setPen(pen)
        p.setBrush(Qt.BrushStyle.NoBrush)

        inset = bw / 2.0
        rect = QRectF(inset, inset, max(0.0, w - 2 * inset), max(0.0, h - 2 * inset))

        r = self._radius_spec
        has_radius = any((r.get("tl", 0), r.get("tr", 0), r.get("br", 0), r.get("bl", 0)))
        if not has_radius:
            p.drawRect(rect)
            return

        rr = {
            "tl": max(0.0, r.get("tl", 0.0) - inset),
            "tr": max(0.0, r.get("tr", 0.0) - inset),
            "br": max(0.0, r.get("br", 0.0) - inset),
            "bl": max(0.0, r.get("bl", 0.0) - inset),
        }
        path = self._rounded_path(rect, rr)
        p.drawPath(path)

