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

Extracts the **point** features of one ENC object class from a parsed
``S57Cell``, using the native ``GeometryBuilder`` (already shipping) for the
position and the catalogue to turn ATTL/OBJL codes into acronyms. Attribute
*values* are left as raw ENC codes; the human-readable label is resolved on
demand at compare time (so a recipe authored on "red" still matches a cell that
stores COLOUR=3).

Only PRIM=1 (point) features are returned, matching the v1 scope. A sounding
multipoint is reduced to its first coordinate for matching purposes (soundings
are an unlikely Source-Validation target, but we stay defensive rather than
raising).
"""

try:                                   # QGIS: ENCoreGIS.sourceval -> ENCoreGIS.s57
    from ..s57.geometry import GeometryBuilder
except ImportError:                    # headless harness: plugin root on sys.path
    from s57.geometry import GeometryBuilder


class ChartFeature:
    """One ENC point feature, normalised for reconciliation."""

    __slots__ = ("fid", "foid", "klass", "lon", "lat", "attrs", "cell")

    def __init__(self, fid, foid, klass, lon, lat, attrs, cell=""):
        self.fid = fid
        self.foid = foid                # (AGEN, FIDN, FIDS) or None
        self.klass = klass
        self.lon = lon
        self.lat = lat
        self.attrs = dict(attrs)        # {acronym: raw ENC value}
        self.cell = cell

    def foid_key(self):
        """A stable string identity from the FOID, e.g. 'AG/FIDN/FIDS'."""
        if not self.foid:
            return None
        return "/".join(str(p) for p in self.foid)

    def lnam(self):
        """The S-57 Long Name: the FOID packed as a 16-hex-digit string,
        AGEN(2 bytes) + FIDN(4 bytes) + FIDS(2 bytes). Stable, unique per
        feature, and what the exclusion denylist stores. None if no FOID."""
        if not self.foid or len(self.foid) < 3:
            return None
        try:
            agen, fidn, fids = (int(self.foid[0]), int(self.foid[1]),
                                int(self.foid[2]))
        except (TypeError, ValueError):
            return None
        return f"{agen & 0xFFFF:04X}{fidn & 0xFFFFFFFF:08X}{fids & 0xFFFF:04X}"

    def identity(self, enc_key):
        """The value used for the same-id-different-location check. ``enc_key``
        is either 'FOID' or an attribute acronym (e.g. 'OBJNAM')."""
        if enc_key in (None, "", "FOID"):
            return self.foid_key()
        v = self.attrs.get(enc_key)
        return None if v is None else str(v)

    def wkt(self):
        return f"POINT ({self.lon:.7f} {self.lat:.7f})"

    def __repr__(self):
        return f"<ChartFeature {self.klass} fid={self.fid}>"


def extract_points(cell, klass, catalogue):
    """Return ``[ChartFeature]`` for every point feature of class ``klass`` in
    the parsed ``cell``. ``klass`` is the object-class acronym (e.g. 'BOYLAT')."""
    target_code = catalogue.acronym_to_code.get(klass)
    builder = GeometryBuilder(cell)
    out = []
    fid = 0
    for fr in cell.features:
        if fr.prim != 1:
            continue
        # Match by code when the catalogue knows the class, else by resolved
        # acronym (so an unlisted class can still be addressed by its acronym).
        if target_code is not None:
            if fr.objl != target_code:
                continue
        else:
            if catalogue.code_to_acronym.get(fr.objl) != klass:
                continue
        geom = builder.point(fr)
        if geom is None:
            continue
        coords = geom.coords()
        if not coords:
            continue
        lon, lat = coords[0][0], coords[0][1]
        attrs = {}
        for code, val in fr.attrs.items():
            acr = catalogue.attr_code_to_acronym.get(code, str(code))
            attrs[acr] = val
        fid += 1
        out.append(ChartFeature(
            fid=fid, foid=fr.foid, klass=klass,
            lon=lon, lat=lat, attrs=attrs, cell=cell.name))
    return out


def extract_points_multi(cells, klass, catalogue):
    """Extract point features of ``klass`` across several parsed cells."""
    out = []
    for cell in cells:
        out.extend(extract_points(cell, klass, catalogue))
    return out
