# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
enc_tidegraph.py
----------------
A lightweight, self-drawn time graph for the Chart Viewer's side panel. It is the
time control for the time-varying S-100 products: while an S-111 and/or S-104
product is loaded it REPLACES the pick report, and its **vertical cursor doubles
as the time slider** — click or drag anywhere on it to scrub the moment, which
drives the S-111 arrow field and the S-104 tidal adjustment.

  • X = time, spanning the first → last record of the loaded series.
  • the vertical cursor is the selected moment; drag it to change time.
  • Y + the water-level curve appear ONLY when an S-104 product is loaded AND a
    point has been picked on the chart (then Y = water level above datum, m).
  • colours follow the viewer's Day / Dusk / Night palette.

Pure QPainter (no QtCharts / external deps) so it renders identically everywhere
and themes cleanly for night vision.
"""

from datetime import timedelta

try:
    from qgis.PyQt.QtCore import Qt, QRectF, QPointF, pyqtSignal
    from qgis.PyQt.QtGui import QColor, QPen, QBrush, QFont, QPainter, QPolygonF
    from qgis.PyQt.QtWidgets import QWidget
except (ImportError, ValueError):                       # headless import guard
    QWidget = object
    pyqtSignal = None

# Palette-keyed colours (r,g,b[,a]). Night is red-tinted for dark adaptation.
_C = {
    "Day": {"bg": (245, 247, 250), "ink": (40, 44, 52), "grid": (0, 0, 0, 26),
            "curve": (20, 90, 200), "fill": (20, 90, 200, 38),
            "datum": (0, 140, 90), "cursor": (210, 0, 0),
            "box_bg": (255, 255, 255, 235), "box_ink": (25, 25, 30)},
    "Dusk": {"bg": (30, 34, 44), "ink": (198, 205, 216), "grid": (255, 255, 255, 26),
             "curve": (96, 156, 236), "fill": (96, 156, 236, 46),
             "datum": (70, 185, 128), "cursor": (236, 96, 96),
             "box_bg": (18, 22, 30, 238), "box_ink": (208, 214, 224)},
    "Night": {"bg": (8, 8, 13), "ink": (168, 120, 120), "grid": (255, 120, 120, 20),
              "curve": (208, 96, 96), "fill": (208, 96, 96, 34),
              "datum": (128, 150, 120), "cursor": (240, 130, 130),
              "box_bg": (16, 9, 9, 238), "box_ink": (206, 156, 156)},
}


class TidalGraph(QWidget):
    """A time axis with a draggable cursor (the time slider), plus an optional
    water-level curve for a picked S-104 location."""

    if pyqtSignal is not None:
        # emitted while the user drags the cursor: the new time-step index
        timeIndexChanged = pyqtSignal(int)

    def __init__(self, parent=None):
        super().__init__(parent)
        self._axis = []                 # [datetime] time steps (the X domain)
        self._index = 0                 # selected step (cursor position)
        self._series = []               # [(datetime, height_m)] — S-104 curve
        self._loc = None                # picked-location label
        self._title = "Time"
        self._palette = "Day"
        self._px0 = self._px1 = 0.0     # plot x-range (set in paint, read on drag)
        self.setMinimumHeight(170)
        self.setMouseTracking(False)

    # ── public API ──────────────────────────────────────────────────────────
    def set_palette(self, name):
        self._palette = name if name in _C else "Day"
        self.update()

    def set_time_axis(self, times):
        """The list of time steps that define the X axis (the time domain)."""
        self._axis = list(times or [])
        if self._index >= len(self._axis):
            self._index = max(0, len(self._axis) - 1)
        self.update()

    def set_index(self, i):
        """Move the cursor to time step ``i`` (no signal — programmatic)."""
        if self._axis:
            i = max(0, min(int(i), len(self._axis) - 1))
        self._index = i
        self.update()

    def set_title(self, text):
        self._title = text or "Time"
        self.update()

    def set_series(self, series, location=None):
        """The water-level curve for a picked point (empty list = axes only)."""
        self._series = list(series or [])
        if location is not None:
            self._loc = location
        if not self._series:
            self._loc = None
        self.update()

    def clear(self):
        self._series = []
        self._loc = None
        self.update()

    # ── mouse: the cursor is the time slider ─────────────────────────────────
    def _index_at_x(self, x):
        if len(self._axis) < 2 or self._px1 <= self._px0:
            return self._index
        f = (x - self._px0) / (self._px1 - self._px0)
        f = max(0.0, min(1.0, f))
        return int(round(f * (len(self._axis) - 1)))

    def _scrub(self, x):
        i = self._index_at_x(x)
        if i != self._index:
            self._index = i
            self.update()
            if pyqtSignal is not None:
                self.timeIndexChanged.emit(i)
        else:
            self.update()

    def mousePressEvent(self, e):
        if len(self._axis) >= 2:
            self._scrub(e.pos().x())

    def mouseMoveEvent(self, e):
        if len(self._axis) >= 2 and (e.buttons() & Qt.LeftButton):
            self._scrub(e.pos().x())

    # ── helpers ─────────────────────────────────────────────────────────────
    def _height_at(self, dt):
        s = self._series
        if not s or dt is None:
            return None
        if dt <= s[0][0]:
            return s[0][1]
        if dt >= s[-1][0]:
            return s[-1][1]
        for k in range(1, len(s)):
            if s[k][0] >= dt:
                (t0, h0), (t1, h1) = s[k - 1], s[k]
                span = (t1 - t0).total_seconds() or 1.0
                f = (dt - t0).total_seconds() / span
                return h0 + (h1 - h0) * f
        return s[-1][1]

    @staticmethod
    def _col(rgb):
        return QColor(*rgb)

    # ── painting ────────────────────────────────────────────────────────────
    def paintEvent(self, _e):
        c = _C.get(self._palette, _C["Day"])
        qp = QPainter(self)
        qp.setRenderHint(QPainter.Antialiasing, True)
        w, h = self.width(), self.height()
        qp.fillRect(0, 0, w, h, self._col(c["bg"]))

        title_font = QFont(); title_font.setPointSize(9); title_font.setBold(True)
        small_font = QFont(); small_font.setPointSize(8)

        qp.setFont(title_font)
        qp.setPen(self._col(c["ink"]))
        qp.drawText(QRectF(8, 4, w - 16, 18), Qt.AlignLeft | Qt.AlignVCenter,
                    self._title)

        # No time domain yet — nothing to control.
        if len(self._axis) < 2:
            qp.setFont(small_font)
            qp.drawText(QRectF(8, 24, w - 16, h - 32),
                        Qt.AlignCenter | Qt.TextWordWrap,
                        "Load an S-111 or S-104 product to\nscrub time here.")
            qp.end()
            return

        have_curve = len(self._series) >= 2
        if self._loc and have_curve:
            qp.setFont(small_font)
            qp.setPen(self._col(c["ink"]))
            qp.drawText(QRectF(8, 20, w - 16, 14),
                        Qt.AlignLeft | Qt.AlignVCenter, self._loc)

        # Plot rectangle (leave room for Y labels only when a curve is shown).
        ml = 42 if have_curve else 14
        mr, mt, mb = 12, 38, 26
        px0, py0 = ml, mt
        pw, ph = max(10, w - ml - mr), max(10, h - mt - mb)
        px1, py1 = px0 + pw, py0 + ph
        self._px0, self._px1 = px0, px1     # remembered for drag → index

        t0, t1 = self._axis[0], self._axis[-1]
        tspan = (t1 - t0).total_seconds() or 1.0

        def X(dt):
            return px0 + pw * ((dt - t0).total_seconds() / tspan)

        grid_pen = QPen(self._col(c["grid"])); grid_pen.setWidth(1)

        # Plot frame.
        qp.setPen(QPen(self._col(c["grid"]), 1))
        qp.drawRect(QRectF(px0, py0, pw, ph))

        # Y axis + curve — only for a picked S-104 location.
        Y = None
        if have_curve:
            s = self._series
            hs = [v for _t, v in s]
            hmin, hmax = min(hs), max(hs)
            if hmin > 0:
                hmin = 0.0
            if hmax < 0:
                hmax = 0.0
            if hmax - hmin < 0.5:
                hmax += 0.25; hmin -= 0.25
            pad = (hmax - hmin) * 0.08
            hmin -= pad; hmax += pad
            hspan = (hmax - hmin) or 1.0

            def Y(val):
                return py1 - ph * ((val - hmin) / hspan)

            qp.setFont(small_font)
            for k in range(5):
                val = hmin + (hmax - hmin) * k / 4.0
                yy = Y(val)
                qp.setPen(grid_pen)
                qp.drawLine(QPointF(px0, yy), QPointF(px1, yy))
                qp.setPen(self._col(c["ink"]))
                qp.drawText(QRectF(0, yy - 7, ml - 4, 14),
                            Qt.AlignRight | Qt.AlignVCenter, f"{val:+.1f}")
            if hmin <= 0.0 <= hmax:
                dp = QPen(self._col(c["datum"])); dp.setWidth(1)
                dp.setStyle(Qt.DashLine)
                qp.setPen(dp)
                qp.drawLine(QPointF(px0, Y(0.0)), QPointF(px1, Y(0.0)))
            poly = QPolygonF([QPointF(X(t), Y(v)) for t, v in s])
            fill = QPolygonF(poly)
            fill.append(QPointF(px1, Y(hmin)))
            fill.append(QPointF(px0, Y(hmin)))
            qp.setPen(Qt.NoPen)
            qp.setBrush(QBrush(self._col(c["fill"])))
            qp.drawPolygon(fill)
            cpen = QPen(self._col(c["curve"])); cpen.setWidth(2)
            qp.setPen(cpen); qp.setBrush(Qt.NoBrush)
            qp.drawPolyline(poly)

        # X grid + time labels (~5 ticks).
        qp.setFont(small_font)
        for k in range(5):
            dt = t0 + timedelta(seconds=tspan * k / 4.0)
            xx = X(dt)
            qp.setPen(grid_pen)
            qp.drawLine(QPointF(xx, py0), QPointF(xx, py1))
            qp.setPen(self._col(c["ink"]))
            lbl = dt.strftime("%H:%M")
            align = Qt.AlignHCenter
            rect = QRectF(xx - 26, py1 + 2, 52, 12)
            if k == 0:
                rect = QRectF(xx - 2, py1 + 2, 60, 12); align = Qt.AlignLeft
            qp.drawText(rect, align | Qt.AlignVCenter, lbl)
        qp.setPen(self._col(c["ink"]))
        qp.drawText(QRectF(px0, py1 + 13, pw, 12), Qt.AlignRight | Qt.AlignVCenter,
                    t0.strftime("%Y-%m-%d") + " UTC")

        # The cursor (= the time slider) at the selected step.
        cur = self._axis[max(0, min(self._index, len(self._axis) - 1))]
        xx = X(cur)
        cur_pen = QPen(self._col(c["cursor"])); cur_pen.setWidth(2)
        qp.setPen(cur_pen)
        qp.drawLine(QPointF(xx, py0), QPointF(xx, py1))
        # grab handle at the top of the cursor
        qp.setBrush(QBrush(self._col(c["cursor"])))
        qp.setPen(Qt.NoPen)
        qp.drawEllipse(QPointF(xx, py0), 3.5, 3.5)

        # readout box: the time always; the height too when a curve is present.
        txt1 = cur.strftime("%H:%M UTC")
        hv = self._height_at(cur) if have_curve else None
        bw = 76
        bh = 30 if hv is not None else 18
        yy = Y(hv) if (Y is not None and hv is not None) else py0 + 10
        bx = min(max(px0, xx + 8), px1 - bw)
        by = min(max(py0, yy - bh - 6), py1 - bh)
        qp.setBrush(QBrush(self._col(c["box_bg"])))
        qp.setPen(QPen(self._col(c["cursor"])))
        qp.drawRoundedRect(QRectF(bx, by, bw, bh), 4, 4)
        qp.setPen(self._col(c["box_ink"]))
        qp.setFont(small_font)
        qp.drawText(QRectF(bx + 6, by + 2, bw - 8, 13),
                    Qt.AlignLeft | Qt.AlignVCenter, txt1)
        if hv is not None:
            bold = QFont(); bold.setPointSize(9); bold.setBold(True)
            qp.setFont(bold)
            qp.drawText(QRectF(bx + 6, by + 14, bw - 8, 14),
                        Qt.AlignLeft | Qt.AlignVCenter, f"{hv:+.2f} m")
            qp.setBrush(QBrush(self._col(c["cursor"])))
            qp.setPen(Qt.NoPen)
            qp.drawEllipse(QPointF(xx, Y(hv)), 3.2, 3.2)

        # a subtle prompt when no location has been graphed yet
        if not have_curve:
            qp.setPen(self._col(c["ink"]))
            qp.setFont(small_font)
            qp.drawText(QRectF(px0, py0 + ph / 2 - 8, pw, 16),
                        Qt.AlignCenter,
                        "Pick a point on the chart to graph its tide (S-104)")
        qp.end()
