# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
validation/checks_structure.py
------------------------------
Dataset-level S-58 checks relating to the ENC Product Specification and S-57
data structure. Most are non-geometric set/identity checks, so they are
headless-testable; a couple consult attributes only.
"""

from .engine import check, Severity, Finding
from .model import known


# Meta objects every ENC cell must contain (S-58 505).
_MANDATORY_META = ("M_COVR", "M_NSYS", "M_QUAL")

# Object classes prohibited in an ENC dataset (S-58 504).
_PROHIBITED_CLASSES = {
    "CANBNK", "LAKSHR", "RIVBNK", "SQUARE", "M_HDAT", "M_PROD", "M_UNIT",
    "C_STAC", "$AREAS", "$LINES", "$CSYMB", "$COMPS", "$TEXTS",
}

# Attributes prohibited anywhere in an ENC (S-58 511).
_PROHIBITED_ATTRS_ANY = ("DUNITS", "HUNITS", "RECDAT", "RECIND",
                         "SCAMAX", "PUNITS")

# Skin-of-the-earth (Group 1) area classes (S-58 518a).
_SKIN_CLASSES = {"FLODOC", "DRGARE", "LNDARE", "HULKES", "PONTON",
                 "DEPARE", "UNSARE"}


@check(505, Severity.CRITICAL, "ENC product specification",
       "Mandatory meta object is missing (M_COVR, M_NSYS and M_QUAL required).",
       "Include the mandatory meta objects.", "3.4")
def mandatory_meta_present(ds, cat, geom):
    """The mandatory meta objects are required in *each* cell, not merely
    somewhere in the exchange set. Evaluate per cell so a multi-cell set where
    one cell omits a meta object is flagged, and the finding names that cell.

    A meta object present only under its lower-case Inland-ENC class code (e.g.
    ``m_nsys`` for code 17018 instead of ``M_NSYS``) is NOT counted as missing
    here — it is present, just under the Inland (lower-case) class code. Inland
    ENC uses lower-case class codes legitimately, so no casing alert is raised."""
    for cell, classes in ds.classes_by_cell().items():
        for c in _MANDATORY_META:
            if c not in classes and c.lower() not in classes:
                yield Finding(_meta(505), cell, c, None, detail=f"{c} absent")


@check(504, Severity.CRITICAL, "ENC product specification",
       "Prohibited object class exists within the dataset.",
       "Remove prohibited objects.", "3.2")
def prohibited_objects(ds, cat, geom):
    for klass in _PROHIBITED_CLASSES:
        for f in ds.features(klass):
            yield Finding(_meta(504), f.cell, f.klass, f.fid)


@check(511, Severity.CRITICAL, "ENC product specification",
       "Prohibited attribute has been encoded.",
       "Remove prohibited attributes.", "3.5.3")
def prohibited_attrs_anywhere(ds, cat, geom):
    for f in ds.features():
        for attr in _PROHIBITED_ATTRS_ANY:
            if attr in f.attrs:
                yield Finding(_meta(511), f.cell, f.klass, f.fid, detail=attr)


@check("518a", Severity.CRITICAL, "ENC product specification",
       "Skin-of-the-earth object is not encoded as Group 1.",
       "Set the FRID GRUP subfield to 1 for skin-of-the-earth objects.",
       "3.10.1")
def skin_group1(ds, cat, geom):
    """When GDAL exposes the GRUP subfield, verify skin-of-the-earth area
    objects are Group 1. GRUP is not always surfaced; when absent we skip
    rather than raise a false positive."""
    for klass in _SKIN_CLASSES:
        for f in ds.features(klass):
            if f.primitive != "A":
                continue
            grup = f.attrs.get("GRUP")
            if grup is not None and str(grup) not in ("1", "1.0"):
                yield Finding(_meta("518a"), f.cell, f.klass, f.fid,
                              detail=f"GRUP={grup}")


@check(507, Severity.CRITICAL, "ENC product specification",
       "Mandatory attribute is meaningless without a value (encoded empty).",
       "Populate the mandatory attribute with a value.",
       "3.5.2 and Supplement No.3 Ch.4")
def mandatory_attr_unpopulated(ds, cat, geom):
    """S-58 509: a finite set of (class, attribute) pairs that are mandatory and
    meaningless when empty. Flag when the attribute is not Known (absent or
    encoded empty) on a feature of that class."""
    for (klass, attr) in _MANDATORY_VALUE_PAIRS:
        for f in ds.features(klass):
            if cat.ienc_owns(klass, f.cell):
                continue              # FC-owned for this cell -> IE509 owns it
            if not known(f.attrs, attr):
                yield Finding(_meta(507), f.cell, f.klass, f.fid,
                              detail=f"{attr} not populated")


# S-58 509 mandatory-value pairs (subset; extend from the 509 list as needed).
_MANDATORY_VALUE_PAIRS = [
    ("DEPARE", "DRVAL1"), ("DEPARE", "DRVAL2"),
    ("DEPCNT", "VALDCO"),
    ("DRGARE", "DRVAL1"),
    ("M_COVR", "CATCOV"),
    ("M_CSCL", "CSCALE"),
    ("M_QUAL", "CATZOC"),
    ("M_SDAT", "VERDAT"), ("M_VDAT", "VERDAT"),
    ("MAGVAR", "VALMAG"),
    ("SWPARE", "DRVAL1"),
    ("LNDELV", "ELEVAT"),
    ("RESARE", "RESTRN"),
]


# -- helpers ------------------------------------------------------------------
def _meta(number):
    from .engine import REGISTRY
    for m in REGISTRY:
        if m.number == number:
            return m
    from .engine import CheckMeta
    return CheckMeta(number, Severity.ERROR, "", "", "", "", None)
