"""RadioKnob widget — retro radio-style rotary knob.

A self-contained PySide6 widget (arc range, tick marks, glowing
indicator dot). Has no internal package dependencies — only PySide6
and ``math`` from the stdlib — so it can be imported standalone.

Extracted from ``open-transcode.v3.py`` (QA item v4-03 — package split).
This is a pure code-organization refactor; behavior is identical to v3.
"""

import math

from PySide6.QtCore import Qt, Signal, QPointF, QRectF
from PySide6.QtGui import (
    QFont, QColor, QPainter, QPen, QBrush,
    QRadialGradient, QFontMetrics,
)
from PySide6.QtWidgets import QWidget

# ──────────────────────────────────────────────
#  RADIO KNOB WIDGET (oldschool rotary control)
# ──────────────────────────────────────────────

class RadioKnob(QWidget):
    """
    A retro radio-style rotary knob widget.
    Supports arc range, tick marks, and a glowing indicator dot.

    Rotation: 7 o'clock (min) to 5 o'clock (max) = 300 degrees.
    """
    valueChanged = Signal(float)

    def __init__(
        self,
        parent=None,
        min_val: float = 0.0,
        max_val: float = 100.0,
        default_val: float = 50.0,
        label: str = "",
        unit: str = "",
        color: tuple = (42, 130, 218),
        num_ticks: int = 17,
        tick_labels: list[str] | None = None,
        snap_ticks: bool = False,
        compact: bool = False,
    ):
        super().__init__(parent)
        self.min_val = min_val
        self.max_val = max_val
        self._value = default_val
        self.label = label
        self.unit = unit
        self.color = QColor(*color)
        self.num_ticks = num_ticks
        self.tick_labels = tick_labels
        self.snap_ticks = snap_ticks
        self._dragging = False
        self.compact = compact

        # Arc geometry: 300-degree sweep, centered at 12 o'clock
        self._arc_start = 210.0   # degrees (7 o'clock)
        self._arc_span = -300.0   # negative = clockwise

        # Scaling factor for compact mode (~70% of full size)
        s = 0.70 if compact else 1.0
        self._s = s
        self.setFixedSize(int(180 * s), int(210 * s))
        self.setCursor(Qt.CursorShape.PointingHandCursor)

    # --- Public API ---

    def value(self) -> float:
        return self._value

    def setValue(self, v: float):
        v = max(self.min_val, min(self.max_val, v))
        if self.snap_ticks:
            v = self._snap(v)
        if v != self._value:
            self._value = v
            self.update()
            self.valueChanged.emit(v)

    def intValue(self) -> int:
        return int(round(self._value))

    def _snap(self, v: float) -> float:
        """Snap to nearest tick."""
        step = (self.max_val - self.min_val) / max(1, self.num_ticks - 1)
        return round((v - self.min_val) / step) * step + self.min_val

    def _val_to_angle(self, v: float) -> float:
        """Map value to angle in degrees (matching the conical gradient)."""
        ratio = (v - self.min_val) / (self.max_val - self.min_val) if self.max_val != self.min_val else 0
        return self._arc_start + ratio * self._arc_span  # goes from 210 -> -90

    def _angle_to_val(self, angle_deg: float) -> float:
        """Map angle back to value."""
        # Normalize angle relative to arc start
        ratio = (angle_deg - self._arc_start) / self._arc_span
        ratio = max(0.0, min(1.0, ratio))
        v = self.min_val + ratio * (self.max_val - self.min_val)
        if self.snap_ticks:
            v = self._snap(v)
        return v

    # --- Painting ---

    def paintEvent(self, event):
        p = QPainter(self)
        p.setRenderHint(QPainter.RenderHint.Antialiasing)
        w, h = self.width(), self.height()
        s = self._s  # scale factor (0.7 for compact, 1.0 for full)

        cx = w / 2
        cy = h / 2 - 4 * s
        outer_r = 70 * s
        knob_r = 40 * s
        arc_w = max(1, int(8 * s))
        tick_w = max(1, 1.5 * s)
        bezel_pad = 6 * s

        # --- Outer bezel ring ---
        bezel_grad = QRadialGradient(cx, cy, outer_r + bezel_pad)
        bezel_grad.setColorAt(0.85, QColor(48, 48, 52))
        bezel_grad.setColorAt(1.0, QColor(26, 26, 30))
        p.setBrush(QBrush(bezel_grad))
        p.setPen(Qt.PenStyle.NoPen)
        p.drawEllipse(QPointF(cx, cy), outer_r + bezel_pad, outer_r + bezel_pad)

        # --- Inactive arc (dark track) ---
        p.setPen(QPen(QColor(50, 50, 56), arc_w, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
        p.drawArc(QRectF(cx - outer_r, cy - outer_r, outer_r * 2, outer_r * 2),
                  int(self._arc_start * 16), int(self._arc_span * 16))

        # --- Active arc (colored fill up to current value) ---
        val_angle = self._val_to_angle(self._value)
        active_span = val_angle - self._arc_start
        if abs(active_span) > 0.5:
            arc_color = QColor(self.color)
            p.setPen(QPen(arc_color, arc_w, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
            p.drawArc(QRectF(cx - outer_r, cy - outer_r, outer_r * 2, outer_r * 2),
                      int(self._arc_start * 16), int(active_span * 16))

        # --- Tick marks ---
        for i in range(self.num_ticks):
            t = i / (self.num_ticks - 1) if self.num_ticks > 1 else 0
            tick_angle = self._val_to_angle(self.min_val + t * (self.max_val - self.min_val))
            tick_rad = tick_angle * math.pi / 180.0
            ox = cx + (outer_r + 12 * s) * (-1) * math.sin(tick_rad)
            oy = cy + (outer_r + 12 * s) * (-1) * (-math.cos(tick_rad))
            ix_ = cx + (outer_r + 3 * s) * (-1) * math.sin(tick_rad)
            iy_ = cy + (outer_r + 3 * s) * (-1) * (-math.cos(tick_rad))
            p.setPen(QPen(QColor(130, 130, 130), tick_w))
            p.drawLine(QPointF(ix_, iy_), QPointF(ox, oy))

        # Tick labels (if provided)
        if self.tick_labels:
            p.setFont(QFont("Sans", max(5, int(7 * s))))
            p.setPen(QColor(160, 160, 160))
            step = max(1, self.num_ticks // len(self.tick_labels))
            label_idx = 0
            for i in range(0, self.num_ticks, step):
                if label_idx >= len(self.tick_labels):
                    break
                t = i / (self.num_ticks - 1) if self.num_ticks > 1 else 0
                tick_angle = self._val_to_angle(self.min_val + t * (self.max_val - self.min_val))
                tick_rad = tick_angle * math.pi / 180.0
                lx = cx + (outer_r + 24 * s) * (-1) * math.sin(tick_rad)
                ly = cy + (outer_r + 24 * s) * (-1) * (-math.cos(tick_rad))
                txt = self.tick_labels[label_idx]
                fm = QFontMetrics(p.font())
                tw = fm.horizontalAdvance(txt)
                p.drawText(QPointF(lx - tw / 2, ly + 2 * s), txt)
                label_idx += 1

        # --- Knob body (dark brushed aluminum) ---
        knob_grad = QRadialGradient(cx - 6 * s, cy - 6 * s, knob_r * 1.3)
        knob_grad.setColorAt(0.0, QColor(72, 72, 78))
        knob_grad.setColorAt(0.5, QColor(50, 50, 55))
        knob_grad.setColorAt(1.0, QColor(34, 34, 38))
        p.setBrush(QBrush(knob_grad))
        p.setPen(QPen(QColor(26, 26, 30), max(1, 1.5 * s)))
        p.drawEllipse(QPointF(cx, cy), knob_r, knob_r)

        # --- Inner shadow ring ---
        inner_shadow = QRadialGradient(cx, cy, knob_r - 2)
        inner_shadow.setColorAt(0.85, QColor(0, 0, 0, 0))
        inner_shadow.setColorAt(1.0, QColor(0, 0, 0, 60))
        p.setBrush(QBrush(inner_shadow))
        p.setPen(Qt.PenStyle.NoPen)
        p.drawEllipse(QPointF(cx, cy), knob_r - 1, knob_r - 1)

        # --- Indicator line (pointer) ---
        ptr_angle = self._val_to_angle(self._value)
        ptr_rad = ptr_angle * 3.14159265 / 180.0
        ptr_len = knob_r - 8 * s
        px = cx + ptr_len * (-1) * math.sin(ptr_rad)
        py = cy + ptr_len * (-1) * (-math.cos(ptr_rad))
        p.setPen(QPen(QColor(255, 255, 255, 220), max(1, 2.5 * s),
                     Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
        p.drawLine(QPointF(cx, cy), QPointF(px, py))

        # --- Center cap dot ---
        cap_r = max(2, 5 * s)
        cap_grad = QRadialGradient(cx, cy, cap_r)
        cap_grad.setColorAt(0.0, QColor(60, 60, 65))
        cap_grad.setColorAt(1.0, QColor(30, 30, 34))
        p.setBrush(QBrush(cap_grad))
        p.setPen(Qt.PenStyle.NoPen)
        p.drawEllipse(QPointF(cx, cy), cap_r, cap_r)

        # --- Glow dot at arc tip ---
        glow_r = max(3, 10 * s)
        glow_x = cx + outer_r * (-1) * math.sin(ptr_rad)
        glow_y = cy + outer_r * (-1) * (-math.cos(ptr_rad))
        glow = QRadialGradient(glow_x, glow_y, glow_r * 1.2)
        glow.setColorAt(0.0, QColor(self.color.red(), self.color.green(), self.color.blue(), 200))
        glow.setColorAt(1.0, QColor(self.color.red(), self.color.green(), self.color.blue(), 0))
        p.setBrush(QBrush(glow))
        p.setPen(Qt.PenStyle.NoPen)
        p.drawEllipse(QPointF(glow_x, glow_y), glow_r, glow_r)

        p.end()

        # --- Label + value text below knob ---
        p2 = QPainter(self)
        p2.setRenderHint(QPainter.RenderHint.Antialiasing)

        # Value line (e.g. "32.0 CRF")
        val_font_sz = max(6, int(13 * s))
        p2.setFont(QFont("Consolas", val_font_sz, QFont.Weight.Bold))
        val_color = QColor(self.color.red(), self.color.green(), self.color.blue())
        p2.setPen(val_color)
        val_text = f"{self._value:.0f} {self.unit}" if self.unit else f"{self._value:.0f}"
        p2.drawText(QRectF(0, h - 38 * s, w, 20 * s), Qt.AlignmentFlag.AlignCenter, val_text)

        # Label line (e.g. "Quality")
        lbl_font_sz = max(5, int(9 * s))
        p2.setFont(QFont("Consolas", lbl_font_sz, QFont.Weight.Bold))
        p2.setPen(QColor(160, 160, 160))
        p2.drawText(QRectF(0, h - 18 * s, w, 16 * s), Qt.AlignmentFlag.AlignCenter, self.label)
        p2.end()

    # --- Input handling ---

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._dragging = True
            self._update_from_mouse(event.position())

    def mouseMoveEvent(self, event):
        if self._dragging:
            self._update_from_mouse(event.position())

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self._dragging = False

    def wheelEvent(self, event):
        delta = event.angleDelta().y()
        step = (self.max_val - self.min_val) / max(1, self.num_ticks - 1)
        if delta > 0:
            self.setValue(self._value + step)
        elif delta < 0:
            self.setValue(self._value - step)

    def _update_from_mouse(self, pos: QPointF):
        cx = self.width() / 2
        cy = self.height() / 2 - 4 * self._s
        dx = pos.x() - cx
        dy = pos.y() - cy
        angle = math.degrees(math.atan2(dx, -dy))  # 0=north, CW positive
        if angle < 0:
            angle += 360
        # Clamp to arc range: 210..510 (which is 210..360 and 0..150)
        # Our arc: 210 degrees to -90 (=270) degrees clockwise
        if angle < 210 and angle > 150:
            # Dead zone at bottom (between 150 and 210)
            # Push to nearest end
            angle = 210 if abs(angle - 210) < abs(angle - 510) else 510
        if angle > 360:
            angle -= 360  # normalize back to 0..360
        self.setValue(self._angle_to_val(angle))

