# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
sourceval/enc_features_s101.py — the S-101 ENC side (native, GDAL-free).
========================================================================

The S-57 extractor (``enc_features``) reads ISO 8211 S-57 cells; S-101 cells
have a different structure entirely (feature/spatial records, an S-101 feature
catalogue). This module gives the Source Validator the SAME ``ChartFeature``
list for an S-101 feature type, so everything downstream — the reconciliation,
the generalization dead-band, the coverage-clipped area comparison — is reused
unchanged.

Features are matched by their S-101 ``type_name`` (e.g. ``BuoyLateral``,
``RestrictedArea``, ``NavigationLine``); attributes are keyed by their
self-describing S-101 attribute names (the same flat model the Chart Viewer /
loaded hybrid layer exposes), so a recipe authored against a loaded S-101
layer's fields binds directly.
"""

try:                                   # QGIS: ENCoreGIS.sourceval -> siblings
    from ..iso8211 import Iso8211Reader
    from ..s101.records import S101Cell
    from ..s101.geometry import S101GeometryBuilder
except ImportError:                    # headless harness: plugin root on path
    from iso8211 import Iso8211Reader
    from s101.records import S101Cell
    from s101.geometry import S101GeometryBuilder

from .enc_features import ChartFeature

import os


def _named_attrs(feature):
    """Flat ``{attributeName: value}`` over a feature's populated LEAF
    attributes — a local copy of s101.qgis_loader._named_attrs (which lives in
    a QGIS-importing module) so this stays headless. Repeated attributes are
    comma-joined; complex parents contribute their leaf children."""
    out = {}
    for a in feature.iter_attributes():
        if a.name and not a.children and a.value not in (None, ""):
            out[a.name] = ("%s,%s" % (out[a.name], a.value)
                           if a.name in out else a.value)
    return out


class S101CellHandle:
    """A parsed S-101 cell + its geometry builder, so a run reads each cell
    once and extracts several classes/geometries from it."""

    __slots__ = ("cell", "gb", "name")

    def __init__(self, cell, gb, name):
        self.cell = cell
        self.gb = gb
        self.name = name


def open_cell(path):
    """Parse one S-101 cell → S101CellHandle, or None on a parse error."""
    try:
        recs = list(Iso8211Reader.open(path).records())
    except Exception:                                   # noqa: BLE001
        return None
    name = os.path.splitext(os.path.basename(path))[0]
    cell = S101Cell.from_records(recs, name=name)
    if getattr(cell, "parse_error", None):
        return None
    return S101CellHandle(cell, S101GeometryBuilder(recs), name)


def _iter(handle, ftype):
    """Yield (feature, geometry) for every feature of ``ftype`` that resolves
    a geometry."""
    for f in handle.cell.features:
        if (f.type_name or "") != ftype:
            continue
        g = handle.gb.geometry(f)
        if g is not None:
            yield f, g


def _rings_from(geom):
    """[[exterior, *holes], …] from a Polygon or MultiPolygon geometry, each
    ring a list of (lon, lat); [] for non-area geometry."""
    if hasattr(geom, "rings"):                          # Polygon
        return [[[(c[0], c[1]) for c in r] for r in geom.rings]]
    if hasattr(geom, "polygons"):                       # MultiPolygon
        return [[[(c[0], c[1]) for c in r] for r in poly]
                for poly in geom.polygons]
    return []


def extract_points(handle, ftype, _catalogue=None):
    """[ChartFeature] for every POINT feature of ``ftype`` in the cell."""
    out, fid = [], 0
    for f, g in _iter(handle, ftype):
        if hasattr(g, "rings") or hasattr(g, "polygons"):
            continue
        coords = g.coords() or []
        if not coords:
            continue
        # a curve/composite here would be a line — only accept point-likes
        if hasattr(g, "coords") and len(coords) > 1 and not hasattr(g, "z_present"):
            # MultiPoint has several; a LineString also has several. Only take
            # the first for a genuine multipoint (soundings); skip lines.
            if type(g).__name__ in ("LineString", "MultiLineString"):
                continue
        lon, lat = coords[0][0], coords[0][1]
        fid += 1
        out.append(ChartFeature(fid=fid, foid=f.foid, klass=ftype,
                                lon=lon, lat=lat, attrs=_named_attrs(f),
                                cell=handle.name))
    return out


def extract_lines(handle, ftype, _catalogue=None):
    """[ChartFeature] for every LINE feature of ``ftype`` — coords carry the
    assembled vertex run, lon/lat the midpoint."""
    out, fid = [], 0
    for f, g in _iter(handle, ftype):
        if type(g).__name__ not in ("LineString", "MultiLineString"):
            continue
        coords = [(c[0], c[1]) for c in (g.coords() or [])]
        if len(coords) < 2:
            continue
        mid = coords[len(coords) // 2]
        fid += 1
        out.append(ChartFeature(fid=fid, foid=f.foid, klass=ftype,
                                lon=mid[0], lat=mid[1], attrs=_named_attrs(f),
                                cell=handle.name, coords=coords))
    return out


def extract_areas(handle, ftype, _catalogue=None):
    """[ChartFeature] for every AREA feature of ``ftype`` — ``areas`` carries
    [[exterior, *holes], …], lon/lat the first exterior's midpoint."""
    out, fid = [], 0
    for f, g in _iter(handle, ftype):
        areas = [rings for rings in _rings_from(g)
                 if rings and len(rings[0]) >= 4]
        if not areas:
            continue
        ext = areas[0][0]
        mid = ext[len(ext) // 2]
        fid += 1
        out.append(ChartFeature(fid=fid, foid=f.foid, klass=ftype,
                                lon=mid[0], lat=mid[1], attrs=_named_attrs(f),
                                cell=handle.name, areas=areas))
    return out


def extract(handle, ftype, kind, catalogue=None):
    """Dispatch to the point/line/area extractor for the recipe geometry."""
    return {"line": extract_lines,
            "area": extract_areas}.get(kind, extract_points)(
        handle, ftype, catalogue)
