"""Music DNA vector layout.

This module defines :class:`MusicDNALayout`, the explicit, versioned
mapping from feature identifiers to vector indices in the
512-dimensional Music DNA vector.

The layout is part of the Music DNA schema and must remain stable
across releases.  It is independent from the FeatureRegistry — the
registry defines what features exist; the layout defines where each
feature is stored in the vector.

Block allocation (from ``docs/04_MUSIC_DNA_SCHEMA.md``):

| Block      | Index Range |
|------------|-------------|
| Signal     | 0–31        |
| Spectral   | 32–95       |
| Dynamic    | 96–159      |
| Rhythm     | 160–223     |
| Harmony    | 224–287     |
| Timbre     | 288–351     |
| Structure  | 352–415     |
| Production | 416–479     |
| Reserved   | 480–511     |
"""

from __future__ import annotations


class MusicDNALayout:
    """Explicit, versioned mapping of feature identifiers to vector indices.

    The layout is deterministic and stable across releases.  It maps
    each of the 55 registered feature identifiers to a unique index
    in the 512-dimensional vector.  Unmapped indices are reserved and
    must be filled with 0.0.
    """

    LAYOUT_VERSION = "1.0.0"
    DIMENSION = 512

    _MAPPING: dict[str, int] = {
        # --- Signal (0–31) ---
        "signal.rms": 0,
        "signal.peak": 1,
        "signal.dc_offset": 2,
        "signal.silence_ratio": 3,
        "signal.peak_db": 4,
        "signal.rms_db": 5,
        "signal.duration_seconds": 6,
        "signal.sample_rate": 7,
        "signal.channels": 8,
        "signal.peak_left": 9,
        "signal.peak_right": 10,
        "signal.rms_left": 11,
        "signal.rms_right": 12,
        # --- Spectral (32–95) ---
        "spectral.centroid": 32,
        "spectral.bandwidth": 33,
        "spectral.rolloff": 34,
        "spectral.flatness": 35,
        "spectral.zero_crossing_rate": 36,
        "spectral.flux": 37,
        # --- Dynamic (96–159) ---
        "dynamic.crest_factor": 96,
        "dynamic.crest_factor_db": 97,
        "dynamic.range": 98,
        "dynamic.headroom": 99,
        "dynamic.clipping_ratio": 100,
        "dynamic.average_rms_db": 101,
        "dynamic.rms_variability": 102,
        # --- Rhythm (160–223) ---
        "rhythm.tempo": 160,
        "rhythm.beat_period": 161,
        "rhythm.density": 162,
        "rhythm.beat_strength": 163,
        "rhythm.onset_count": 164,
        "rhythm.onset_rate": 165,
        "rhythm.regularity": 166,
        # --- Harmony (224–287) ---
        "harmony.chroma_c": 224,
        "harmony.chroma_csharp": 225,
        "harmony.chroma_d": 226,
        "harmony.chroma_dsharp": 227,
        "harmony.chroma_e": 228,
        "harmony.chroma_f": 229,
        "harmony.chroma_fsharp": 230,
        "harmony.chroma_g": 231,
        "harmony.chroma_gsharp": 232,
        "harmony.chroma_a": 233,
        "harmony.chroma_asharp": 234,
        "harmony.chroma_b": 235,
        "harmony.tonnetz_x": 236,
        "harmony.tonnetz_y": 237,
        "harmony.tonnetz_z": 238,
        "harmony.tonnetz_u": 239,
        "harmony.tonnetz_v": 240,
        "harmony.tonnetz_w": 241,
        "harmony.key": 242,
        "harmony.mode": 243,
        "harmony.tonal_stability": 244,
        "harmony.pitch_class_entropy": 245,
    }

    _CATEGORY_RANGES: dict[str, tuple[int, int]] = {
        "signal": (0, 31),
        "spectral": (32, 95),
        "dynamic": (96, 159),
        "rhythm": (160, 223),
        "harmony": (224, 287),
        "timbre": (288, 351),
        "structure": (352, 415),
        "production": (416, 479),
        "reserved": (480, 511),
    }

    def __init__(self) -> None:
        """Initialize and validate the layout."""
        indices = list(self._MAPPING.values())
        if len(indices) != len(set(indices)):
            raise ValueError("Duplicate indices in layout mapping")
        for idx in indices:
            if idx < 0 or idx >= self.DIMENSION:
                raise ValueError(f"Index {idx} out of range [0, {self.DIMENSION})")
        self._reverse_mapping: dict[int, str] = {v: k for k, v in self._MAPPING.items()}

    def get_index(self, identifier: str) -> int:
        """Return the vector index for a feature identifier.

        Args:
            identifier: The canonical feature identifier.

        Returns:
            The vector index.

        Raises:
            KeyError: If the identifier is not in the layout.
        """
        if identifier not in self._MAPPING:
            raise KeyError(f"Feature '{identifier}' is not in the layout")
        return self._MAPPING[identifier]

    def contains(self, identifier: str) -> bool:
        """Check if a feature identifier is in the layout.

        Args:
            identifier: The canonical feature identifier.

        Returns:
            ``True`` if the identifier is mapped, ``False`` otherwise.
        """
        return identifier in self._MAPPING

    def dimension(self) -> int:
        """Return the total vector dimension.

        Returns:
            Always 512.
        """
        return self.DIMENSION

    def list_entries(self) -> list[tuple[str, int]]:
        """Return all (identifier, index) entries sorted by index.

        Returns:
            A list of tuples sorted by vector index.
        """
        return sorted(self._MAPPING.items(), key=lambda x: x[1])

    def reserved_indices(self) -> list[int]:
        """Return indices not mapped to any feature.

        Returns:
            A sorted list of reserved indices (to be filled with 0.0).
        """
        used = set(self._MAPPING.values())
        return [i for i in range(self.DIMENSION) if i not in used]

    def feature_count(self) -> int:
        """Return the number of mapped features.

        Returns:
            The count of feature identifiers in the layout.
        """
        return len(self._MAPPING)

    def get_feature(self, index: int) -> str:
        """Return the feature identifier for a vector index.

        Args:
            index: The vector index.

        Returns:
            The canonical feature identifier.

        Raises:
            KeyError: If the index is not mapped to any feature.
        """
        if index not in self._reverse_mapping:
            raise KeyError(f"Index {index} is not mapped to any feature")
        return self._reverse_mapping[index]

    def contains_index(self, index: int) -> bool:
        """Check if a vector index has a mapped feature.

        Args:
            index: The vector index.

        Returns:
            ``True`` if the index is mapped, ``False`` otherwise.
        """
        return index in self._reverse_mapping

    def category(self, index: int) -> str:
        """Return the category name for a vector index.

        Args:
            index: The vector index.

        Returns:
            The category name (e.g. ``"signal"``, ``"spectral"``).

        Raises:
            KeyError: If the index does not fall within any known
                category range.
        """
        for name, (start, end) in self._CATEGORY_RANGES.items():
            if start <= index <= end:
                return name
        raise KeyError(f"Index {index} does not belong to any category")

    def indices(self, category: str) -> list[int]:
        """Return all mapped indices for a category.

        Args:
            category: The category name (e.g. ``"signal"``).

        Returns:
            A sorted list of vector indices mapped to features in
            the given category.
        """
        return [idx for _, idx in self.list_entries() if self.category(idx) == category]
