"""Custom widgets for the OBDash GUI: a multi-axis plot (one colored axis per
metric), a single-axis plot, and a car-style gauge / gauge grid.

All three plot/gauge containers share a small interface so the main window can
swap between them:  add_curve(key,name,unit,color) / set_data(key,xs,ys) /
remove_curve(key) / clear().
"""
import math

from PySide6 import QtCore, QtGui, QtWidgets
import pyqtgraph as pg


class SinglePlot(QtWidgets.QWidget):
    """One shared Y axis. Used for the Normalize (% of range) mode."""

    def __init__(self):
        super().__init__()
        lay = QtWidgets.QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        self.pw = pg.PlotWidget()
        self.pw.addLegend(offset=(10, 10))
        self.pw.showGrid(x=True, y=True, alpha=0.25)
        self.pw.setLabel("bottom", "time", units="s")
        lay.addWidget(self.pw)
        self._curves = {}

    def set_y_label(self, text):
        self.pw.setLabel("left", text)

    def add_curve(self, key, name, unit, color):
        self._curves[key] = self.pw.plot([], [], name=name, pen=pg.mkPen(color, width=2))

    def set_data(self, key, xs, ys):
        c = self._curves.get(key)
        if c is not None:
            c.setData(xs, ys)

    def remove_curve(self, key):
        c = self._curves.pop(key, None)
        if c is not None:
            self.pw.removeItem(c)
            leg = self.pw.plotItem.legend
            if leg:
                try:
                    leg.removeItem(c)
                except Exception:
                    pass

    def clear(self):
        for key in list(self._curves):
            self.remove_curve(key)

    def set_background(self, bg):
        self.pw.setBackground(bg)


class MultiAxisPlot(QtWidgets.QWidget):
    """Overlay with one Y axis PER METRIC, each axis colored to match its line,
    so mixed-scale signals are all readable at their true values.

    The 'primary' metric owns the LEFT axis; the rest stack on the right.
    Click a line to promote it to the left axis. Beyond MAX_RIGHT extra metrics,
    overflow curves share the primary axis (rare; plot fewer to compare)."""

    MAX_RIGHT = 5

    def __init__(self):
        super().__init__()
        lay = QtWidgets.QVBoxLayout(self)
        lay.setContentsMargins(0, 0, 0, 0)
        self.glw = pg.GraphicsLayoutWidget()
        lay.addWidget(self.glw)
        self.p = self.glw.addPlot()
        self.p.showGrid(x=True, y=True, alpha=0.2)
        self.p.setLabel("bottom", "time", units="s")
        self.legend = self.p.addLegend(offset=(10, 10))
        self.base_vb = self.p.vb
        self._curves = {}       # key -> {color,name,unit,curve,vb,ax}
        self._order = []        # insertion order of keys
        self._primary = None
        self.base_vb.sigResized.connect(self._sync)

    def add_curve(self, key, name, unit, color):
        if key in self._curves:
            return
        curve = pg.PlotCurveItem(pen=pg.mkPen(color, width=2), name=name)
        curve.setClickable(True, width=8)
        curve.sigClicked.connect(lambda *_a, k=key: self.set_primary(k))
        self._curves[key] = {"color": color, "name": name, "unit": unit,
                             "curve": curve, "vb": None, "ax": None}
        self._order.append(key)
        if self._primary is None:
            self._primary = key
        self._rebuild()

    def set_primary(self, key):
        """Move this metric's axis to the LEFT (make it the primary axis)."""
        if key in self._curves and key != self._primary:
            self._primary = key
            self._rebuild()

    def set_data(self, key, xs, ys):
        c = self._curves.get(key)
        if c is not None:
            c["curve"].setData(xs, ys)

    def remove_curve(self, key):
        c = self._curves.pop(key, None)
        if not c:
            return
        self._order.remove(key)
        self._detach(c)
        if self._primary == key:
            self._primary = self._order[0] if self._order else None
        self._rebuild()

    def clear(self):
        for c in self._curves.values():
            self._detach(c)
        self._curves = {}
        self._order = []
        self._primary = None
        self.legend.clear()

    def set_y_label(self, _text):
        pass                                         # labels itself per metric

    def set_background(self, bg):
        self.glw.setBackground(bg)

    # -- internals --
    def _detach(self, c):
        """Remove a curve from its current ViewBox and tear down its extra axis
        (the base left axis is kept). Idempotent -- safe to call repeatedly."""
        cur = c["curve"]
        vb = c.get("vb")
        if vb is not None:
            try:
                vb.removeItem(cur)
            except Exception:
                pass
        ax = c.get("ax")
        if ax is not None and ax is not self.p.getAxis("left"):
            try:
                self.p.layout.removeItem(ax)
            except Exception:
                pass
            sc = ax.scene()
            if sc is not None:
                sc.removeItem(ax)
        if vb is not None and vb is not self.base_vb:
            sc = vb.scene()
            if sc is not None:
                sc.removeItem(vb)
        c["vb"] = None
        c["ax"] = None

    def _rebuild(self):
        for c in self._curves.values():
            self._detach(c)
        self.legend.clear()
        if self._primary is None:
            return
        order = [self._primary] + [k for k in self._order if k != self._primary]
        col = 3
        left = self.p.getAxis("left")
        for idx, key in enumerate(order):
            c = self._curves[key]
            cur = c["curve"]
            pen = pg.mkPen(c["color"])
            if idx == 0:                              # primary -> left axis
                self.base_vb.addItem(cur)
                left.setLabel(c["name"])             # name already carries the unit
                left.setPen(pen); left.setTextPen(pen)
                c["vb"] = self.base_vb; c["ax"] = left
            elif idx <= self.MAX_RIGHT:               # right axis, colored to line
                vb = pg.ViewBox()
                ax = pg.AxisItem("right")
                self.p.layout.addItem(ax, 2, col); col += 1
                self.p.scene().addItem(vb)
                ax.linkToView(vb); vb.setXLink(self.p)
                ax.setLabel(c["name"])
                ax.setPen(pen); ax.setTextPen(pen)
                vb.addItem(cur)
                c["vb"] = vb; c["ax"] = ax
            else:                                     # overflow -> primary axis
                self.base_vb.addItem(cur)
                c["vb"] = self.base_vb; c["ax"] = None
            self.legend.addItem(cur, c["name"])
        self._sync()

    def _sync(self):
        rect = self.base_vb.sceneBoundingRect()
        for c in self._curves.values():
            vb = c.get("vb")
            if vb is not None and vb is not self.base_vb:
                vb.setGeometry(rect)
                vb.linkedViewChanged(self.base_vb, vb.XAxis)


class ArcGauge(QtWidgets.QWidget):
    """A 270-degree arc gauge for a single signal: filled arc to the current
    value, peak tick, big numeric readout, name + unit."""

    _AMBER = "#e6a23c"
    _RED = "#e6194B"
    _NEEDLE = {"ok": "#e8e8e8", "warn": _AMBER, "crit": _RED}

    def __init__(self, name, unit, vmin, vmax, accent="#3cb44b", zones=None):
        super().__init__()
        self.name = name
        self.unit = unit
        self.vmin = float(vmin)
        self.vmax = float(vmax) if vmax != vmin else float(vmin) + 1.0
        self.accent = QtGui.QColor(accent)
        z = zones or {}
        self.warn_hi = z.get("warn_hi"); self.redline_hi = z.get("redline_hi")
        self.warn_lo = z.get("warn_lo"); self.redline_lo = z.get("redline_lo")
        self.value = None
        self.peak = None
        self.setMinimumSize(172, 176)

    def set_value(self, v, peak=None):
        self.value = v
        self.peak = peak
        self.update()

    def _frac(self, v):
        return max(0.0, min(1.0, (v - self.vmin) / (self.vmax - self.vmin)))

    def _zone(self, v):
        if v is None:
            return "ok"
        if self.redline_hi is not None and v >= self.redline_hi:
            return "crit"
        if self.redline_lo is not None and v <= self.redline_lo:
            return "crit"
        if self.warn_hi is not None and v >= self.warn_hi:
            return "warn"
        if self.warn_lo is not None and v <= self.warn_lo:
            return "warn"
        return "ok"

    # gauge geometry: a 270-degree dial, gap at the bottom (like a tach)
    _START = 225.0       # value=min angle (math degrees, 0=3 o'clock, CCW+)
    _SWEEP = 270.0       # clockwise as value rises

    def _ang(self, frac):
        return self._START - self._SWEEP * max(0.0, min(1.0, frac))

    def paintEvent(self, _ev):
        p = QtGui.QPainter(self)
        p.setRenderHint(QtGui.QPainter.Antialiasing)
        w, h = self.width(), self.height()
        p.fillRect(self.rect(), QtGui.QColor("#141414"))

        side = min(w, h - 14)
        R = side / 2 - 6
        cx, cy = w / 2.0, R + 8

        def pt(ang_deg, r):
            a = math.radians(ang_deg)
            return QtCore.QPointF(cx + r * math.cos(a), cy - r * math.sin(a))

        # dial face + bezel
        p.setPen(QtGui.QPen(QtGui.QColor("#2a2a2a"), 3))
        p.setBrush(QtGui.QColor("#0c0c0c"))
        p.drawEllipse(QtCore.QPointF(cx, cy), R, R)

        # warning / redline zone bands on the outer ring (like a tach redline)
        ringrect = QtCore.QRectF(cx - R + 3, cy - R + 3, 2 * (R - 3), 2 * (R - 3))

        def band(fa, fb, color):
            sa = self._ang(fa); span = self._ang(fb) - self._ang(fa)
            pen = QtGui.QPen(QtGui.QColor(color), 4)
            pen.setCapStyle(QtCore.Qt.FlatCap)
            p.setPen(pen)
            p.drawArc(ringrect, int(sa * 16), int(span * 16))

        if self.redline_hi is not None:
            band(self._frac(self.redline_hi), 1.0, self._RED)
        if self.warn_hi is not None:
            top = self._frac(self.redline_hi) if self.redline_hi is not None else 1.0
            band(self._frac(self.warn_hi), top, self._AMBER)
        if self.redline_lo is not None:
            band(0.0, self._frac(self.redline_lo), self._RED)
        if self.warn_lo is not None:
            bot = self._frac(self.redline_lo) if self.redline_lo is not None else 0.0
            band(bot, self._frac(self.warn_lo), self._AMBER)

        # tick marks + numeric scale
        majors = 6
        fnt = p.font(); fnt.setPointSize(7); p.setFont(fnt)
        for i in range(majors * 5 + 1):
            frac = i / (majors * 5)
            a = self._ang(frac)
            major = (i % 5 == 0)
            p.setPen(QtGui.QPen(QtGui.QColor("#ddd" if major else "#555"),
                                2 if major else 1))
            p.drawLine(pt(a, R - 3), pt(a, R - (13 if major else 7)))
            if major:
                val = self.vmin + frac * (self.vmax - self.vmin)
                span = abs(self.vmax - self.vmin)
                lbl = f"{val:.0f}" if span >= 50 else f"{val:.1f}" if span >= 5 else f"{val:g}"
                lp = pt(a, R - 25)
                p.setPen(QtGui.QColor("#9a9a9a"))
                p.drawText(QtCore.QRectF(lp.x() - 18, lp.y() - 7, 36, 14),
                           QtCore.Qt.AlignCenter, lbl)

        # peak marker (thin amber tick)
        if self.peak is not None and self.value is not None and self.peak != self.value:
            a = self._ang(self._frac(self.peak))
            p.setPen(QtGui.QPen(QtGui.QColor("#e6c84b"), 2))
            p.drawLine(pt(a, R - 3), pt(a, R - 14))

        zcol = QtGui.QColor(self._NEEDLE[self._zone(self.value)])
        # needle (colored by zone: white=ok, amber=warn, red=redline)
        if self.value is not None:
            a = self._ang(self._frac(self.value))
            tip = pt(a, R - 16)
            b1 = pt(a + 90, 4.5)
            b2 = pt(a - 90, 4.5)
            tail = pt(a + 180, 10)
            path = QtGui.QPainterPath()
            path.moveTo(b1); path.lineTo(tip); path.lineTo(b2); path.lineTo(tail)
            path.closeSubpath()
            p.setPen(QtCore.Qt.NoPen)
            p.setBrush(zcol)
            p.drawPath(path)
        # hub
        p.setBrush(QtGui.QColor("#999")); p.setPen(QtCore.Qt.NoPen)
        p.drawEllipse(QtCore.QPointF(cx, cy), 5, 5)

        # digital readout (lower face) -- colored by zone
        p.setPen(zcol)
        fnt.setPointSize(13); fnt.setBold(True); p.setFont(fnt)
        val = "--" if self.value is None else f"{self.value:g}"
        p.drawText(QtCore.QRectF(cx - 55, cy + R * 0.30, 110, 22),
                   QtCore.Qt.AlignCenter, val)
        # name + unit
        fnt.setPointSize(8); fnt.setBold(False); p.setFont(fnt)
        p.setPen(QtGui.QColor("#9a9a9a"))
        p.drawText(QtCore.QRectF(0, h - 15, w, 14), QtCore.Qt.AlignCenter,
                   f"{self.name}  ({self.unit})" if self.unit else self.name)
        p.end()


class GaugeGrid(QtWidgets.QScrollArea):
    """Scrollable grid of ArcGauges, one per signal."""

    def __init__(self):
        super().__init__()
        self.setWidgetResizable(True)
        self.setStyleSheet("QScrollArea, QWidget { background: #111; }")
        self._inner = QtWidgets.QWidget()
        self._grid = QtWidgets.QGridLayout(self._inner)
        self._grid.setSpacing(8)
        self.setWidget(self._inner)
        self._gauges = {}
        self._cols = 4

    def rebuild(self, specs):
        """specs: list of (key, name, unit, vmin, vmax, accent, zones)."""
        while self._grid.count():
            item = self._grid.takeAt(0)
            wdg = item.widget()
            if wdg:
                wdg.deleteLater()
        self._gauges = {}
        for i, spec in enumerate(specs):
            key, name, unit, vmin, vmax, accent = spec[:6]
            zones = spec[6] if len(spec) > 6 else None
            g = ArcGauge(name, unit, vmin, vmax, accent, zones)
            self._gauges[key] = g
            self._grid.addWidget(g, i // self._cols, i % self._cols)

    def keys(self):
        return set(self._gauges)

    def set_value(self, key, v, peak=None):
        g = self._gauges.get(key)
        if g is not None:
            g.set_value(v, peak)
