# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
s57/update.py
-------------
S-57 update application (Part 3, clause 8.4).

Applies sequential update cells (.001, .002, ...) onto a base cell to produce
the merged, as-delivered feature/attribute state -- the same view GDAL presents
when update files sit beside the base. Attribute/value checks must validate this
merged state, because an update can add, remove, or change a feature's
attributes.

Scope (Tier 1): record-level Record Update Instruction (RUIN) -- insert (1),
delete (2), modify (3) -- for feature and vector records, matched by
(RCNM, RCID); plus attribute deltas (ATTF / NATF / ATTV): each instance in the
update upserts by label, and the ASCII DEL value (0x7F) deletes the attribute.
This is exactly the merged feature/attribute view the attribute checks need.

Not yet applied (Tier 2, geometry/topology): the pointer and coordinate field
controls FSPC/FFPC (feature) and VRPC/SGCC (vector) that edit pointers and
coordinates *in place* within a modified record. Geometry checks stay on GDAL
for now, so a modify record's attribute deltas are applied while its base
geometry pointers/coordinates are left intact. When the geometry checks migrate
off GDAL, these controls complete the picture (an insert/delete of a whole
spatial record is already handled; only in-place pointer/coordinate edits wait).
"""
import glob
import os

from .records import S57Cell

INSERT, DELETE, MODIFY = 1, 2, 3
_DEL = "\x7f"          # S-57 subfield delete value


def _apply_attr_delta(existing, delta):
    """Upsert attribute deltas from an update record's ATTF/NATF/ATTV onto an
    existing record's attribute map. A value equal to the DEL subfield (0x7F)
    removes the attribute; any other value adds or replaces it (matched by
    attribute label)."""
    for code, atvl in delta.items():
        if atvl is not None and str(atvl) == _DEL:
            existing.pop(code, None)
        else:
            existing[code] = atvl


def apply_updates(base, updates):
    """Return a new merged ``S57Cell``: ``base`` with each cell in ``updates``
    applied in sequence. Records are matched by (RCNM, RCID)."""
    feats = {(f.rcnm, f.rcid): f for f in base.features}
    vecs = dict(base.vectors)
    stats = {"ins": 0, "del": 0, "mod": 0, "skipped": 0}

    def _edit(store, rec):
        key = (rec.rcnm, rec.rcid)
        if rec.ruin == DELETE:
            if store.pop(key, None) is not None:
                stats["del"] += 1
        elif rec.ruin == MODIFY:
            tgt = store.get(key)
            if tgt is None:
                stats["skipped"] += 1          # modify of an absent record
                return
            _apply_attr_delta(tgt.attrs, rec.attrs)
            if hasattr(rec, "natf"):
                _apply_attr_delta(tgt.natf, rec.natf)
            stats["mod"] += 1
        else:                                  # INSERT (1) or unmarked
            store[key] = rec
            stats["ins"] += 1

    for upd in updates:
        for f in upd.features:
            _edit(feats, f)
        for v in upd.vectors.values():
            _edit(vecs, v)

    merged = base.__class__(name=base.name)
    merged.dsid = base.dsid
    merged.dspm = getattr(base, "dspm", None)
    merged.dssi = getattr(base, "dssi", None)
    merged.features = list(feats.values())
    merged.vectors = vecs
    merged.update_stats = stats
    return merged


def sibling_updates(base_path):
    """Sequence of update files (.001, .002, ...) sitting beside ``base_path``,
    ordered by update number."""
    root = os.path.splitext(base_path)[0]
    out = []
    for p in glob.glob(glob.escape(root) + ".*"):
        ext = os.path.splitext(p)[1].lstrip(".")
        if ext.isdigit() and int(ext) > 0:
            out.append((int(ext), p))
    return [p for _n, p in sorted(out)]


def merged_cell(base_path):
    """Read a base ``.000`` plus any sibling updates in the same directory and
    return the merged ``S57Cell``. With no updates this is just the base cell --
    so it is always safe to route base cells through here."""
    cell = S57Cell.from_path(base_path)
    if cell.parse_error:
        return cell
    paths = sibling_updates(base_path)
    if not paths:
        return cell
    updates = [S57Cell.from_path(p) for p in paths]
    return apply_updates(cell, updates)
