from __future__ import annotations
from typing import Dict, Iterator, Mapping, Optional
from zempy.zosapi.core import ensure_not_none, run_native
from zempy.bridge import zemax_exceptions as _exc


class Metadata:
    """
    Python wrapper for ZOSAPI.Common.IMetadata.

    Notes:
      - Key numbers are 1-based per ZOSAPI (valid range: 1..NumberOfKeys).
      - This class does not implement any file/IO policy; it’s a thin, safe interop layer.
    """

    __slots__ = ("_native_meta",)

    def __init__(self, native_meta) -> None:
        self._native_meta = native_meta

    def _ensure_native(self) -> None:
        ensure_not_none(self._native_meta, what="Native IMetadata", exc_type=_exc.ZemaxSystemError)

    @property
    def number_of_keys(self) -> int:
        return int(run_native(
            "IMetadata.NumberOfKeys get",
            lambda: self._native_meta.NumberOfKeys,
            ensure=self._ensure_native,
        ))

    def get_key_name(self, key_number: int) -> str:
        n = self.number_of_keys
        if not (1 <= key_number <= n):
            raise IndexError(f"key_number out of range: {key_number} (valid 1..{n})")
        return str(run_native(
            f"IMetadata.GetKeyName({key_number})",
            lambda: self._native_meta.GetKeyName(int(key_number)),
            ensure=self._ensure_native,
        ))

    def keys(self) -> Iterator[str]:
        for i in range(1, self.number_of_keys + 1):
            yield self.get_key_name(i)

    def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
        val = run_native(
            f"IMetadata.GetData({key!r})",
            lambda: self._native_meta.GetData(str(key)),
            ensure=self._ensure_native,
        )
        s = None if val is None else str(val)
        return s if (s not in (None, "")) else default

    def set(self, key: str, value: str) -> None:
        run_native(
            f"IMetadata.SetData({key!r}, <len={len(value)}> chars)",
            lambda: self._native_meta.SetData(str(key), str(value)),
            ensure=self._ensure_native,
        )

    def remove(self, key: str) -> bool:
        ok = bool(run_native(
            f"IMetadata.RemoveData({key!r})",
            lambda: self._native_meta.RemoveData(str(key)),
            ensure=self._ensure_native,
        ))
        return ok

    def items(self) -> Iterator[tuple[str, str]]:
        for k in self.keys():
            v = self.get(k, default="")
            yield k, v if v is not None else ""

    def to_dict(self) -> Dict[str, str]:
        return {k: (self.get(k, default="") or "") for k in self.keys()}

    def set_many(self, items: Mapping[str, str]) -> None:
        for k, v in items.items():
            self.set(k, v)

    def update_from_dict(self, items: Mapping[str, str], *, overwrite: bool = True) -> None:
        existing = set(self.keys())
        for k, v in items.items():
            if overwrite or (k not in existing):
                self.set(k, v)

    def convert_from_binary(self, data: bytes) -> str:
        return str(run_native(
            "IMetadata.ConvertFromBinary(<bytes>)",
            lambda: self._native_meta.ConvertFromBinary(bytearray(data)),
            ensure=self._ensure_native,
        ))

    def convert_to_binary(self, s: str) -> bytes:
        arr = run_native(
            "IMetadata.ConvertToBinary(<str>)",
            lambda: self._native_meta.ConvertToBinary(str(s)),
            ensure=self._ensure_native,
        )
        try:
            return bytes(arr)
        except Exception:
            return bytes(int(b) & 0xFF for b in arr)  # type: ignore[arg-type]

    def create_guid(self) -> str:
        return str(run_native(
            "IMetadata.CreateGuid()",
            lambda: self._native_meta.CreateGuid(),
            ensure=self._ensure_native,
        ))