"""QQuickPaintedItem trend chart — running-average temperature over time.

Renders a single polyline series (per-frame average temperatures) over a fixed
0-200°C Y axis and an elapsed-seconds X axis, using a bounded 60-second
rolling window. Mirrors the @QmlElement registration pattern used by
`WaferMapItem` so it can be embedded directly in QML via `import ISC.Wafer`
and used as `TrendChartItem { }`.

The series is driven by `streamController.trendDelta` (one new
`[elapsed_s, value]` point per live frame) and cleared on
`streamController.trendReset` (new stream started). See
docs/adr/0001-trend-chart-axes-standardization.md.
"""
from __future__ import annotations

import json
import logging
from typing import Optional

from PySide6.QtCore import Property, QPoint, QRectF, Qt, Signal, Slot
from PySide6.QtGui import QColor, QFont, QPainter, QPen, QPolygon
from PySide6.QtQml import QmlElement
from PySide6.QtQuick import QQuickPaintedItem

from pygui.backend.visualization.chart_geometry import elapsed_to_pixel, value_to_pixel

log = logging.getLogger(__name__)

QML_IMPORT_NAME = "ISC.Wafer"
QML_IMPORT_MAJOR_VERSION = 1

WINDOW_SECONDS = 60.0
Y_MIN = 0.0
Y_MAX = 200.0
Y_TICKS = (0, 40, 80, 120, 160, 200)


@QmlElement
class TrendChartItem(QQuickPaintedItem):
    """Painted trend chart; driven by incremental (elapsed_s, value) points."""

    hasDataChanged = Signal()
    lineColorChanged = Signal()
    gridColorChanged = Signal()
    textColorChanged = Signal()
    paddingChanged = Signal()

    def __init__(self, parent: Optional[QQuickPaintedItem] = None) -> None:
        super().__init__(parent)
        self.setRenderTarget(QQuickPaintedItem.RenderTarget.FramebufferObject)
        self.setAntialiasing(True)
        self.setFillColor(QColor("transparent"))

        self._points: list[tuple[float, float]] = []
        self._padding: int = 8
        # Separate from `_padding` (breathing room around the plot area): these
        # reserve space for the axis labels themselves, which are wider than
        # `_padding` alone — that mismatch is what clipped y-axis text before.
        self._margin_left: int = 48
        self._margin_bottom: int = 30
        # Top tick label is drawn centered on the plot's top edge (y_px - 7),
        # so the plot needs headroom or the label kisses the card border.
        self._margin_top: int = 10
        self._line_color = QColor("#5B9DF5")
        self._grid_color = QColor("#2A3441")
        self._text_color = QColor("#CBD5E1")

    # ── Qt properties ─────────────────────────────────────────────────────

    @Property(bool, notify=hasDataChanged)
    def hasData(self) -> bool:
        """True when at least one point is buffered.

        QML can bind to this safely (a plain list length is not bindable).
        """
        return len(self._points) > 0

    @Property(QColor, notify=lineColorChanged)
    def lineColor(self) -> QColor:
        return self._line_color

    @lineColor.setter  # type: ignore[no-redef]
    def lineColor(self, c: QColor) -> None:
        if c == self._line_color:
            return
        self._line_color = QColor(c)
        self.lineColorChanged.emit()
        self.update()

    @Property(QColor, notify=gridColorChanged)
    def gridColor(self) -> QColor:
        return self._grid_color

    @gridColor.setter  # type: ignore[no-redef]
    def gridColor(self, c: QColor) -> None:
        if c == self._grid_color:
            return
        self._grid_color = QColor(c)
        self.gridColorChanged.emit()
        self.update()

    @Property(QColor, notify=textColorChanged)
    def textColor(self) -> QColor:
        return self._text_color

    @textColor.setter  # type: ignore[no-redef]
    def textColor(self, c: QColor) -> None:
        if c == self._text_color:
            return
        self._text_color = QColor(c)
        self.textColorChanged.emit()
        self.update()

    @Property(int, notify=paddingChanged)
    def padding(self) -> int:
        return self._padding

    @padding.setter  # type: ignore[no-redef]
    def padding(self, val: int) -> None:
        v = max(0, int(val))
        if v == self._padding:
            return
        self._padding = v
        self.paddingChanged.emit()
        self.update()

    # ── slots for QML: streamController.trendDelta / trendReset ────────────

    @Slot(str)
    def appendDelta(self, delta_json: str) -> None:
        """Append new (elapsed_s, value) points and prune the 60s window.

        Mirrors `streamController.trendDelta`, which emits one new pair per
        live frame as `[[elapsed_s, value], ...]`.
        """
        try:
            parsed = json.loads(delta_json) if delta_json else []
        except (json.JSONDecodeError, TypeError) as exc:
            log.error("TrendChartItem: failed to parse trend delta JSON: %s", exc)
            return

        had_data = len(self._points) > 0
        added = False
        for pair in parsed:
            try:
                elapsed, value = float(pair[0]), float(pair[1])
            except (TypeError, ValueError, IndexError):
                continue
            self._points.append((elapsed, value))
            added = True

        if not added:
            return

        latest_elapsed = self._points[-1][0]
        cutoff = latest_elapsed - WINDOW_SECONDS
        self._points = [p for p in self._points if p[0] >= cutoff]

        if not had_data:
            self.hasDataChanged.emit()
        self.update()

    @Slot()
    def clearTrend(self) -> None:
        """Clear all buffered points (new stream started)."""
        if not self._points:
            return
        self._points = []
        self.hasDataChanged.emit()
        self.update()

    # ── paint ─────────────────────────────────────────────────────────────

    def paint(self, painter: QPainter) -> None:  # type: ignore[override]
        painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
        painter.setRenderHint(QPainter.RenderHint.TextAntialiasing, True)

        w = self.width()
        h = self.height()
        if w <= 0 or h <= 0:
            return

        left = self._padding + self._margin_left
        top = self._padding + self._margin_top
        bottom_inset = self._padding + self._margin_bottom
        plot_rect = QRectF(left, top,
                           max(1, w - left - self._padding),
                           max(1, h - top - bottom_inset))

        self._draw_grid(painter, plot_rect)

        if len(self._points) < 2:
            return

        window_end = self._points[-1][0]
        window_start = max(0.0, window_end - WINDOW_SECONDS)
        self._draw_axes_labels(painter, plot_rect, window_start, window_end)
        self._draw_line(painter, plot_rect, window_start, window_end)

    # ── internals ─────────────────────────────────────────────────────────

    def _x_to_px(self, elapsed: float, window_start: float, window_end: float, plot_rect: QRectF) -> float:
        return elapsed_to_pixel(elapsed, window_start, window_end, plot_rect.left(), plot_rect.width())

    def _y_to_px(self, value: float, plot_rect: QRectF) -> float:
        px = value_to_pixel(value, Y_MIN, Y_MAX, plot_rect.top(), plot_rect.height())
        # Clamp: a sensor reading outside 0-200°C shouldn't paint outside the widget.
        return max(plot_rect.top(), min(plot_rect.bottom(), px))

    def _draw_grid(self, painter: QPainter, plot_rect: QRectF) -> None:
        pen = QPen(self._grid_color)
        pen.setWidthF(1.0)
        pen.setCosmetic(True)
        painter.setPen(pen)
        rows = len(Y_TICKS) - 1
        for i in range(rows + 1):
            y = plot_rect.top() + (i / rows) * plot_rect.height()
            painter.drawLine(int(plot_rect.left()), int(y),
                             int(plot_rect.right()), int(y))

    def _draw_axes_labels(
        self,
        painter: QPainter,
        plot_rect: QRectF,
        window_start: float,
        window_end: float,
    ) -> None:
        painter.setPen(self._text_color)
        font = QFont(painter.font())
        font.setPointSizeF(max(7.0, font.pointSizeF() * 0.85))
        painter.setFont(font)

        title_w = 14  # leftmost sliver reserved for the rotated "Temp" title
        label_w = self._margin_left - 4 - title_w  # 4px gutter before the plot area
        for tick in reversed(Y_TICKS):
            y_px = self._y_to_px(tick, plot_rect)
            painter.drawText(QRectF(title_w, y_px - 7, label_w, 14),
                             Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter,
                             str(tick))

        # X-axis: elapsed seconds within the current rolling window.
        cols = 4
        y_px = plot_rect.bottom() + 4
        for i in range(cols + 1):
            t_val = window_start + (i / cols) * (window_end - window_start)
            x_px = self._x_to_px(t_val, window_start, window_end, plot_rect)
            label = f"{t_val:.0f}"
            if i == 0:
                rect = QRectF(x_px, y_px, 40, 14)
                align = Qt.AlignmentFlag.AlignLeft
            elif i == cols:
                # Right-anchor inside the plot so the label never clips off-widget
                rect = QRectF(x_px - 40, y_px, 40, 14)
                align = Qt.AlignmentFlag.AlignRight
            else:
                rect = QRectF(x_px - 20, y_px, 40, 14)
                align = Qt.AlignmentFlag.AlignHCenter
            painter.drawText(rect, align | Qt.AlignmentFlag.AlignTop, label)

        self._draw_axis_titles(painter, plot_rect, title_w, y_px + 18)

    def _draw_axis_titles(
        self,
        painter: QPainter,
        plot_rect: QRectF,
        title_w: float,
        x_label_bottom: float,
    ) -> None:
        # Y axis: "Temp", rotated upright, centered along the plot's height.
        painter.save()
        painter.translate(title_w / 2, plot_rect.center().y())
        painter.rotate(-90)
        painter.drawText(QRectF(-plot_rect.height() / 2, -7, plot_rect.height(), 14),
                         Qt.AlignmentFlag.AlignCenter, "Temp (°C)")
        painter.restore()

        # X axis: "Time", centered under the elapsed-seconds tick row.
        painter.drawText(QRectF(plot_rect.left(), x_label_bottom, plot_rect.width(), 14),
                         Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop, "Time")

    def _draw_line(
        self,
        painter: QPainter,
        plot_rect: QRectF,
        window_start: float,
        window_end: float,
    ) -> None:
        pen = QPen(self._line_color)
        pen.setWidthF(2.0)
        pen.setCosmetic(True)
        pen.setCapStyle(Qt.PenCapStyle.RoundCap)
        pen.setJoinStyle(Qt.PenJoinStyle.RoundJoin)
        painter.setPen(pen)

        poly = QPolygon()
        for elapsed, value in self._points:
            px = self._x_to_px(elapsed, window_start, window_end, plot_rect)
            py = self._y_to_px(value, plot_rect)
            poly.append(QPoint(int(px), int(py)))
        painter.drawPolyline(poly)

        # Trailing dot at the last data point
        last_elapsed, last_value = self._points[-1]
        last_x = int(self._x_to_px(last_elapsed, window_start, window_end, plot_rect))
        last_y = int(self._y_to_px(last_value, plot_rect))
        painter.setBrush(self._line_color)
        painter.setPen(Qt.PenStyle.NoPen)
        radius = 3
        painter.drawEllipse(last_x - radius, last_y - radius,
                            radius * 2, radius * 2)
