# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
enc_portrayal.py
----------------
Standard-NEUTRAL portrayal interpreter — the shared core of the unified ENCore
portrayal engine. Both front-ends emit a common list of drawing instructions and
this turns them into QGIS symbology:

    S-57/IENC  ──(S-52 LUPTs + CSPs)──┐
                                       ├─► [instructions] ─► build_symbol() ─► QgsSymbol
    S-101      ──(ported Lua rules)───┘

This works because S-101 portrayal is evolved S-52: shared colour tokens, shared
CSPs (DEPARE03/SOUNDG03/…), and a 1:1 instruction mapping —
  S-52   AC(token)        AP(pat)     LS(style,w,token)  LC(name)  SY(name[,rot])  TX/TE
  model  ('AC',token)     ('AP',pat)  ('LS',style,w,tk)  ('LC',n)  ('SY',n,rot)    ('TX',…)

The interpreter is renderer-neutral in spirit (it also informs the MapLibre
target later); here it produces QGIS symbols. Colour resolution and symbol
lookup are INJECTED (callables) so this module knows nothing S-52- or
S-101-specific — the front-end supplies the right colour table + symbol library.
"""

try:
    from qgis.core import (
        QgsFillSymbol, QgsLineSymbol, QgsMarkerSymbol,
        QgsSimpleFillSymbolLayer, QgsSimpleLineSymbolLayer)
    from qgis.PyQt.QtGui import QColor
    from qgis.PyQt.QtCore import Qt
except (ImportError, ValueError):                       # headless import guard
    QColor = None


# S-52 line-style keyword → Qt pen style.
def _pen_style(style):
    s = (style or "").upper()
    if s in ("DASH", "DASHED"):
        return Qt.DashLine
    if s in ("DOTT", "DOT", "DOTTED"):
        return Qt.DotLine
    return Qt.SolidLine


# ── S-52 instruction-string parser ───────────────────────────────────────────
def parse_s52(instruction):
    """Parse an S-52 LUPT instruction string (e.g.
    "AC(DEPVS);LS(SOLD,2,CHGRN);SY(BOYSPP21);CS(DEPARE03)") into the common model
    [('AC','DEPVS'), ('LS','SOLD',2.0,'CHGRN'), ('SY','BOYSPP21',0.0),
     ('CS','DEPARE03')]. Width is kept in S-52 pen units; the front-end scales to
    mm. Unrecognised commands are skipped."""
    out = []
    for cmd in (instruction or "").split(";"):
        cmd = cmd.strip()
        if not cmd or "(" not in cmd:
            continue
        op = cmd[:cmd.index("(")].strip().upper()
        args = cmd[cmd.index("(") + 1:cmd.rfind(")")].split(",")
        args = [a.strip() for a in args]
        try:
            if op == "AC":
                out.append(("AC", args[0]))
            elif op == "AP":
                out.append(("AP", args[0]))
            elif op == "LS":
                out.append(("LS", args[0], float(args[1]), args[2]))
            elif op == "LC":
                out.append(("LC", args[0]))
            elif op == "SY":
                rot = float(args[1]) if len(args) > 1 and args[1] else 0.0
                out.append(("SY", args[0], rot))
            elif op in ("TX", "TE"):
                out.append(("TX", args))
            elif op == "CS":
                out.append(("CS", args[0]))
        except (IndexError, ValueError):
            continue
    return out


# ── Interpreter: instructions → QgsSymbol ────────────────────────────────────
def build_symbol(instructions, geom_type, palette, color_hex, marker_layer=None,
                 linestyle=None, mm_per_width=0.32):
    """Build a QgsSymbol for ``geom_type`` ('area'|'line'|'point') from the
    primitive instructions (CS must already be expanded by the front-end).

    Injected providers (front-end supplies the right standard's data):
      color_hex(token, palette)            -> '#RRGGBB'
      marker_layer(name, palette, rotation)-> a QgsMarkerSymbolLayer (SVG for
                                              S-101, raster sprite for S-52) or None
      linestyle(name)                      -> {'width','color','dashes':[…]} or None
    ``mm_per_width`` scales S-52 pen-width units to mm (S-52 nominal 0.32 mm)."""
    gt = (geom_type or "").lower()
    if gt.startswith("area") or gt.startswith("poly"):
        return _area_symbol(instructions, palette, color_hex, linestyle,
                            mm_per_width, marker_layer)
    if gt.startswith("line"):
        return _line_symbol(instructions, palette, color_hex, linestyle,
                            mm_per_width)
    return _point_symbol(instructions, palette, marker_layer)


def _qcolor(color_hex, token, palette, default="#000000"):
    hx = color_hex(token, palette) if token else None
    return QColor(hx or default)


def _area_symbol(instrs, palette, color_hex, linestyle, mmpw, marker_layer=None):
    sym = QgsFillSymbol()
    while sym.symbolLayerCount():
        sym.deleteSymbolLayer(0)
    fill_color = None
    stroke = None                                       # (color, width_mm, style)
    patterns = []                                       # AP pattern names
    for ins in instrs:
        op = ins[0]
        if op == "AC":
            fill_color = _qcolor(color_hex, ins[1], palette)
        elif op == "AP":
            patterns.append(ins[1])
        elif op == "LS":
            stroke = (_qcolor(color_hex, ins[3], palette),
                      ins[2] * mmpw, _pen_style(ins[1]))
        elif op == "LC" and linestyle:
            ls = linestyle(ins[1]) or {}
            stroke = (_qcolor(color_hex, ls.get("color"), palette),
                      (ls.get("width") or 0.32), Qt.SolidLine)
    # Base fill (+ boundary) first, so any pattern overlays on top.
    sl = QgsSimpleFillSymbolLayer(fill_color or QColor(0, 0, 0, 0))
    if fill_color is None:
        sl.setBrushStyle(Qt.NoBrush)
    if stroke is not None:
        sl.setStrokeColor(stroke[0])
        sl.setStrokeWidth(stroke[1])
        sl.setStrokeStyle(stroke[2])
    else:
        sl.setStrokeStyle(Qt.NoPen)
    sym.appendSymbolLayer(sl)
    # AP → repeat the pattern symbol over the area (point-pattern fill).
    for pat in patterns:
        pf = _pattern_fill(pat, palette, marker_layer)
        if pf is not None:
            sym.appendSymbolLayer(pf)
    return sym


def _pattern_fill(name, palette, marker_layer):
    """An AP symbol-pattern fill: tile the referenced symbol over the area via a
    QgsPointPatternFillSymbolLayer. Spacing defaults to the symbol footprint."""
    if marker_layer is None:
        return None
    ml = marker_layer(name, palette, 0.0)
    if ml is None:
        return None
    try:
        from qgis.core import (QgsPointPatternFillSymbolLayer, QgsMarkerSymbol)
    except Exception:                                   # noqa: BLE001
        return None
    msym = QgsMarkerSymbol()
    while msym.symbolLayerCount():
        msym.deleteSymbolLayer(0)
    msym.appendSymbolLayer(ml)
    pf = QgsPointPatternFillSymbolLayer()
    pf.setSubSymbol(msym)
    spacing = max(getattr(ml, "size", lambda: 0)() or 0, 5.0) * 1.6
    pf.setDistanceX(spacing)
    pf.setDistanceY(spacing)
    return pf


def _line_symbol(instrs, palette, color_hex, linestyle, mmpw):
    sym = QgsLineSymbol()
    while sym.symbolLayerCount():
        sym.deleteSymbolLayer(0)
    for ins in instrs:
        op = ins[0]
        if op == "LS":
            sl = QgsSimpleLineSymbolLayer(_qcolor(color_hex, ins[3], palette))
            sl.setWidth(ins[2] * mmpw)
            sl.setPenStyle(_pen_style(ins[1]))
            sym.appendSymbolLayer(sl)
        elif op == "LC" and linestyle:
            ls = linestyle(ins[1]) or {}
            sl = QgsSimpleLineSymbolLayer(
                _qcolor(color_hex, ls.get("color"), palette))
            sl.setWidth(ls.get("width") or 0.32)
            dashes = ls.get("dashes") or []
            if dashes:
                pat = []
                for _start, length in dashes:
                    pat += [length, max(length, 1.0)]
                sl.setPenStyle(Qt.CustomDashLine)
                sl.setCustomDashVector(pat)
                sl.setUseCustomDashPattern(True)
            sym.appendSymbolLayer(sl)
    if not sym.symbolLayerCount():
        sym.appendSymbolLayer(QgsSimpleLineSymbolLayer(QColor("#000000")))
    return sym


def _point_symbol(instrs, palette, marker_layer):
    sym = QgsMarkerSymbol()
    while sym.symbolLayerCount():
        sym.deleteSymbolLayer(0)
    for ins in instrs:
        if ins[0] != "SY" or marker_layer is None:
            continue
        rot = float(ins[2]) if len(ins) > 2 and ins[2] else 0.0
        sl = marker_layer(ins[1], palette, rot)
        if sl is not None:
            sym.appendSymbolLayer(sl)
    if not sym.symbolLayerCount():
        from qgis.core import QgsSimpleMarkerSymbolLayer
        sym.appendSymbolLayer(QgsSimpleMarkerSymbolLayer())
    return sym
