# SPDX-License-Identifier: GPL-3.0-or-later
"""ECDIS-style Day / Dusk / Night display palettes for AIS Watchkeeper.

One central colour set per mode, plus an orchestrator (:func:`apply`) that pushes
the active mode across every surface the plugin controls:

  * the QGIS **map canvas** background;
  * any **raster basemap** (dimmed + desaturated for dusk/night, since an OSM-style
    basemap can't be recoloured — it's darkened with a brightness/contrast/
    saturation filter instead);
  * the **native QGIS UI theme** (Day = default, Dusk = "Blend of Gray",
    Night = "Night Mapping") via ``QgsApplication.setUITheme`` — best-effort;
  * the plugin's own **labels** and the **route-leg viewer**, which read their
    colours from here and re-draw when notified.

The chosen mode is persisted in ``QgsSettings`` so it survives restarts. Every step
is best-effort: a failure on one surface never aborts the others or the feed.
"""
from qgis.PyQt.QtGui import QColor

MODES = ("day", "dusk", "night")
_SETTINGS_KEY = "ais_nmea_source/display_mode"

# --------------------------------------------------------------------------- #
#  Palettes.  RGB tuples; a role missing from a mode falls back to "day".
# --------------------------------------------------------------------------- #
_PAL = {
    "day": {
        "canvas_bg": (255, 255, 255),
        # basemap raster filter: (brightness -255..255, contrast -100..100,
        # saturation -100..100). Day = neutral.
        "raster": (0, 0, 0),
        "qgis_theme": "default",
        # vessel labels
        "label_text": (25, 25, 25),
        "label_halo": (255, 255, 255),
        # stream-rate graph (health line colour stays green/yellow/red)
        "graph_bg": (255, 255, 255),
        "graph_grid": (187, 187, 187),
        "graph_text": (51, 51, 51),
        # route-leg viewer
        "leg_bg": (255, 255, 255),
        "leg_axis": (35, 35, 35),
        "leg_endpoint": (60, 60, 60),
        "leg_text": (20, 20, 20),
        "leg_halo": (255, 255, 255),
        "leg_in": (29, 53, 87),
        "leg_out": (230, 57, 70),
        "leg_cpa_blue": (0, 90, 255),
        "leg_cpa_green": (0, 170, 70),
        "leg_bridge": (140, 40, 170),
        "leg_lock": (230, 140, 0),
        "leg_marker": (0, 150, 136),
    },
    "dusk": {
        "canvas_bg": (58, 64, 78),
        "raster": (-45, 12, -45),
        "qgis_theme": "Blend of Gray",
        "label_text": (232, 224, 198),
        "label_halo": (18, 20, 26),
        "graph_bg": (52, 58, 70),
        "graph_grid": (120, 126, 140),
        "graph_text": (225, 220, 200),
        "leg_bg": (52, 58, 70),
        "leg_axis": (198, 200, 210),
        "leg_endpoint": (210, 210, 220),
        "leg_text": (232, 224, 198),
        "leg_halo": (18, 20, 26),
        "leg_in": (110, 150, 220),
        "leg_out": (240, 96, 108),
        "leg_cpa_blue": (90, 150, 255),
        "leg_cpa_green": (60, 200, 110),
        "leg_bridge": (188, 118, 214),
        "leg_lock": (240, 160, 40),
        "leg_marker": (40, 200, 180),
    },
    "night": {
        "canvas_bg": (8, 10, 18),
        "raster": (-85, 16, -72),
        "qgis_theme": "Night Mapping",
        "label_text": (205, 210, 220),
        "label_halo": (4, 5, 10),
        "graph_bg": (9, 11, 19),
        "graph_grid": (70, 76, 92),
        "graph_text": (200, 205, 215),
        "leg_bg": (9, 11, 19),
        "leg_axis": (150, 156, 172),
        "leg_endpoint": (170, 176, 190),
        "leg_text": (206, 210, 222),
        "leg_halo": (4, 5, 10),
        "leg_in": (96, 146, 226),
        "leg_out": (238, 92, 104),
        "leg_cpa_blue": (86, 150, 255),
        "leg_cpa_green": (60, 205, 112),
        "leg_bridge": (190, 120, 220),
        "leg_lock": (242, 162, 44),
        "leg_marker": (44, 206, 186),
    },
}

# surfaces that want to redraw when the mode changes (e.g. an open route-leg
# viewer). Weakly-held callables; register on open, discard on close.
_listeners = []


# --------------------------------------------------------------------------- #
#  Mode state (persisted)
# --------------------------------------------------------------------------- #
def current_mode():
    """The active display mode ('day'|'dusk'|'night'), from QgsSettings."""
    try:
        from qgis.core import QgsSettings
        m = QgsSettings().value(_SETTINGS_KEY, "day")
    except Exception:                                   # noqa: BLE001
        m = "day"
    return m if m in MODES else "day"


def set_mode(mode):
    """Persist the display mode (no-op for an unknown value)."""
    if mode not in MODES:
        return
    try:
        from qgis.core import QgsSettings
        QgsSettings().setValue(_SETTINGS_KEY, mode)
    except Exception:                                   # noqa: BLE001  # nosec B110
        pass


def palette(mode=None):
    """The role->(r,g,b) map for `mode` (default: the active mode)."""
    return _PAL.get(mode or current_mode(), _PAL["day"])


def color(role, mode=None):
    """A QColor for a palette role, falling back to the day palette then grey."""
    pal = palette(mode)
    rgb = pal.get(role) or _PAL["day"].get(role) or (128, 128, 128)
    return QColor(*rgb)


# --------------------------------------------------------------------------- #
#  Redraw listeners (route-leg viewer, etc.)
# --------------------------------------------------------------------------- #
def register_listener(fn):
    """Register a zero-arg callable invoked (best-effort) after the mode changes."""
    if fn not in _listeners:
        _listeners.append(fn)


def unregister_listener(fn):
    try:
        _listeners.remove(fn)
    except ValueError:
        pass


def _notify():
    for fn in list(_listeners):
        try:
            fn()
        except Exception:                               # noqa: BLE001  # nosec B110
            pass


# --------------------------------------------------------------------------- #
#  Apply
# --------------------------------------------------------------------------- #
def apply(iface, mode=None, set_qgis_theme=True):
    """Push `mode` (default: the active mode) across every surface. Best-effort;
    each step is isolated so one failure can't abort the rest."""
    if mode is not None:
        set_mode(mode)
    mode = current_mode()
    pal = palette(mode)
    _apply_canvas(iface, pal)
    _apply_basemap(iface, pal)
    if set_qgis_theme:
        _apply_qgis_theme(pal, mode)
    _notify()


def _apply_canvas(iface, pal):
    try:
        from qgis.core import QgsProject
        c = QColor(*pal["canvas_bg"])
        canvas = iface.mapCanvas()
        canvas.setCanvasColor(c)
        try:
            QgsProject.instance().setBackgroundColor(c)
        except Exception:                               # noqa: BLE001  # nosec B110
            pass
        canvas.refresh()
    except Exception:                                   # noqa: BLE001  # nosec B110
        pass


def _apply_basemap(iface, pal):
    """Dim + desaturate raster layers (basemaps) for dusk/night; neutral for day.
    Best-effort across every raster layer in the project."""
    try:
        from qgis.core import QgsProject, QgsRasterLayer
    except Exception:                                   # noqa: BLE001
        return
    bright, contrast, sat = pal["raster"]
    for lyr in QgsProject.instance().mapLayers().values():
        if not isinstance(lyr, QgsRasterLayer):
            continue
        try:
            bc = lyr.brightnessFilter()          # QgsBrightnessContrastFilter
            if bc is not None:
                bc.setBrightness(int(bright))
                bc.setContrast(int(contrast))
            hs = lyr.hueSaturationFilter()
            if hs is not None:
                hs.setSaturation(int(sat))
            lyr.triggerRepaint()
        except Exception:                               # noqa: BLE001  # nosec B110
            pass


# the app's chrome (QPalette + stylesheet) captured once, BEFORE we first change
# the theme, so Day can restore the user's own QGIS look exactly.
_baseline_chrome = None


def _apply_qgis_theme(pal, mode):
    """Switch the native QGIS UI theme live. Best-effort.

    Day restores the chrome as it was before we first touched it — because
    QgsApplication.setUITheme("default") clears the stylesheet but does NOT reset
    the dark QPalette a themed mode left behind (so the chrome would otherwise stay
    dark). Dusk/Night apply QGIS's own 'Blend of Gray' / 'Night Mapping' themes
    (names verified against uiThemes())."""
    global _baseline_chrome
    try:
        from qgis.core import QgsApplication
        from qgis.PyQt.QtWidgets import QApplication
        from qgis.PyQt.QtGui import QPalette
    except Exception:                                   # noqa: BLE001
        return
    app = QApplication.instance()
    if app is None:
        return
    if _baseline_chrome is None:                        # capture once, untouched
        _baseline_chrome = (QPalette(app.palette()), app.styleSheet())
    try:
        if mode == "day":
            pal0, ss0 = _baseline_chrome
            try:
                QgsApplication.setUITheme("default")
            except Exception:                           # noqa: BLE001  # nosec B110
                pass
            app.setPalette(QPalette(pal0))              # setUITheme won't reset this
            app.setStyleSheet(ss0)
        else:
            name = pal.get("qgis_theme")
            if name:
                QgsApplication.setUITheme(name)
    except Exception:                                   # noqa: BLE001  # nosec B110
        pass
