# 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 feature, normalised for reconciliation. Points carry lon/lat;
    LINE features additionally carry ``coords`` = [(lon, lat), …] with lon/lat
    set to the line's midpoint (the representative point for coverage clipping
    and report locations)."""

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

    def __init__(self, fid, foid, klass, lon, lat, attrs, cell="",
                 coords=None, areas=None):
        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
        self.coords = coords            # [(lon, lat), …] for line features
        self.areas = areas              # [[exterior, *holes], …] for areas

    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):
        if self.coords and len(self.coords) >= 2:
            pts = ", ".join(f"{x:.7f} {y:.7f}" for x, y in self.coords)
            return f"LINESTRING ({pts})"
        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


def extract_lines(cell, klass, catalogue):
    """Return ``[ChartFeature]`` for every LINE feature of class ``klass`` in
    the parsed ``cell`` — coords carry the assembled vertex run, lon/lat the
    line midpoint (used for coverage clipping and report locations)."""
    target_code = catalogue.acronym_to_code.get(klass)
    builder = GeometryBuilder(cell)
    out = []
    fid = 0
    for fr in cell.features:
        if fr.prim != 2:
            continue
        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.line(fr)
        if geom is None:
            continue
        coords = [(c[0], c[1]) for c in (geom.coords() or [])]
        if len(coords) < 2:
            continue
        mid = coords[len(coords) // 2]
        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=mid[0], lat=mid[1], attrs=attrs, cell=cell.name,
            coords=coords))
    return out


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


def extract_areas(cell, klass, catalogue):
    """Return ``[ChartFeature]`` for every AREA feature of class ``klass`` in
    the parsed ``cell`` — ``areas`` carries [[exterior, *holes], …] ring runs
    (a MultiPolygon feature yields several), lon/lat the first exterior's
    midpoint vertex."""
    target_code = catalogue.acronym_to_code.get(klass)
    builder = GeometryBuilder(cell)
    out = []
    fid = 0
    for fr in cell.features:
        if fr.prim != 3:
            continue
        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.area(fr)
        if geom is None:
            continue
        if hasattr(geom, "rings"):                       # Polygon
            areas = [[[(c[0], c[1]) for c in r] for r in geom.rings]]
        elif hasattr(geom, "polygons"):                  # MultiPolygon
            areas = [[[(c[0], c[1]) for c in r] for r in poly]
                     for poly in geom.polygons]
        else:
            continue
        areas = [rings for rings in areas
                 if rings and len(rings[0]) >= 4]
        if not areas:
            continue
        ext = areas[0][0]
        mid = ext[len(ext) // 2]
        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=mid[0], lat=mid[1], attrs=attrs, cell=cell.name,
            areas=areas))
    return out
