# Copyright (C) 2026 Wilhelmus Dingenus de Pooter
# SPDX-License-Identifier: GPL-3.0-or-later
"""
enc_config.py
Parses enc_layers.xml and provides lookup helpers for display names, groups,
draw priority, processing recipes (processing.json) and portrayal styles
(styles/*.qml).
"""

import os
import json

try:
    from .enc_xml_safe import parse_root
except (ImportError, ValueError):
    from enc_xml_safe import parse_root


class LayerConfig:
    """
    Parses enc_layers.xml and exposes:
        group_for(code)      -> group name string  (or "Other")
        display_name(code)   -> human readable name (or code itself)
        groups               -> ordered list of (group_name, [layer_codes])
    """

    def __init__(self, xml_path=None):
        base_dir = os.path.dirname(__file__)
        if xml_path is None:
            xml_path = os.path.join(base_dir, "enc_layers.xml")

        self._display_names = {}   # code -> name
        self._code_to_group = {}   # code -> group name
        self._priorities = {}      # code -> int draw priority (0-90)
        self._groups = []          # [(group_name, [codes])]  ordered

        self._parse(xml_path)

        # Processing recipes (processing.json) and portrayal styles (styles/).
        self._recipes = {}
        self._proc_path = os.path.join(base_dir, "processing.json")
        self._styles_dir = os.path.join(base_dir, "styles")
        self._load_recipes()

        # S-62 producer/agency codes (agency_codes.xml): numeric code -> name.
        self._agency_names = {}
        self._agency_path = os.path.join(base_dir, "agency_codes.xml")
        self._load_agencies()

    def _load_recipes(self):
        try:
            with open(self._proc_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            self._recipes = data.get("recipes", {}) or {}
        except FileNotFoundError:
            self._recipes = {}
        except Exception:
            # Malformed JSON should not crash loading; just no recipes.
            self._recipes = {}

    def _load_agencies(self):
        """Load the S-62 producer code -> name map from agency_codes.xml.

        Keys are stored as plain strings of the numeric code (e.g. '7979').
        A missing or malformed file is non-fatal: lookups simply return None.
        """
        try:
            root = parse_root(self._agency_path)
        except FileNotFoundError:
            self._agency_names = {}
            return
        except Exception:
            self._agency_names = {}
            return
        names = {}
        for el in root.findall("agency"):
            code = (el.get("code") or "").strip()
            name = (el.get("name") or "").strip()
            if code and name:
                names[code] = name
        self._agency_names = names

    def agency_name(self, code):
        """Resolve an S-62 numeric producer code to its agency name.

        Accepts int, str, or None. Returns the agency name, or None if the
        code is missing/blank or not found in the S-62 list. Tolerates values
        that arrive as '7979', 7979, or '7979.0'.
        """
        if code is None:
            return None
        key = str(code).strip()
        if not key:
            return None
        # Normalise a float-ish string like '7979.0' to '7979'.
        if key.endswith(".0"):
            key = key[:-2]
        return self._agency_names.get(key)

    def recipe_for(self, code, geom=None):
        """Return the ordered recipe (list of steps) for a layer.

        Lookup order: '<code>_<geom>' then '<code>'. Returns [] if none.
        """
        if geom:
            keyed = self._recipes.get(f"{code}_{geom}")
            if keyed is not None:
                return keyed
        return self._recipes.get(code, [])

    def style_path_for(self, code, geom=None):
        """Return the QML style file path for a layer, or None if no file.

        Lookup is geometry-specific ONLY: styles/<code>_<geom>.qml. We do not
        fall back to a bare styles/<code>.qml, because a style authored for one
        geometry (e.g. an area fill) cannot correctly portray another (a line
        or point). Loaded layers are always geometry-split and named with a
        geometry suffix, so every layer has a well-defined geometry to match.
        Author one QML per geometry: DEPARE_area.qml, SOUNDG_point.qml, etc.
        """
        if not geom:
            return None
        path = os.path.join(self._styles_dir, f"{code}_{geom}.qml")
        return path if os.path.exists(path) else None

    def _parse(self, xml_path):
        root = parse_root(xml_path)
        for group_el in root.findall("group"):
            gname = group_el.get("name", "Other")
            codes = []
            for layer_el in group_el.findall("layer"):
                code = layer_el.get("code", "").strip()
                name = layer_el.get("name", code)
                prio_raw = layer_el.get("priority", "50")
                try:
                    prio = int(prio_raw)
                except (TypeError, ValueError):
                    prio = 50
                if code:
                    self._display_names[code] = name
                    self._code_to_group[code] = gname
                    self._priorities[code] = prio
                    codes.append(code)
            if codes:
                self._groups.append((gname, codes))

    def display_name(self, code):
        """Human-readable name for an S-57 class code (falls back to the code)."""
        return self._display_names.get(code, code)

    def group_for(self, code):
        """UI group a class code belongs to ('Other' if the code is unknown)."""
        return self._code_to_group.get(code, "Other")

    def priority_for(self, code):
        """Draw priority 0-90 (higher = on top). Unknown codes default to 50."""
        return self._priorities.get(code, 50)

    @property
    def groups(self):
        """List of (group_name, [codes]) tuples, in display order."""
        return self._groups


# Module-level singleton — loaded once
_config = None

def get_config():
    """Return the module-level LayerConfig singleton (built on first use)."""
    global _config
    if _config is None:
        _config = LayerConfig()
    return _config
