"""Core schema definitions for REAPER project generation.

Represents the intermediate representation (SongDefinition) used to build
REAPER .rpp files via RPPBuilder.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from pathlib import Path


# ---------------------------------------------------------------------------
# Key validation
# ---------------------------------------------------------------------------
import re

_KEY_RE = re.compile(r"^[A-G][b#]?m?$")


# ---------------------------------------------------------------------------
# Dataclasses
# ---------------------------------------------------------------------------

@dataclass
class SongMeta:
    """Song metadata — tempo, key, time signature."""

    bpm: float               # 20–999
    key: str                 # e.g. "Am", "Dm", "Gm"
    title: str = ""          # song title
    ppq: int = 960           # ticks per quarter note (REAPER default)
    time_sig_num: int = 4    # numerator e.g. 4
    time_sig_den: int = 4    # denominator e.g. 4
    calibrate: bool = True   # enable post-processing mix calibration


@dataclass
class CCEvent:
    """A MIDI CC event within a clip.

    Attributes:
        controller: CC number (e.g. 11 = Expression)
        time:       Position in beats from clip start
        value:      0–127
    """

    controller: int
    time: float
    value: int


@dataclass
class MidiNote:
    """A single MIDI note event.

    Attributes:
        pitch:    MIDI note number 0–127 (60 = middle C)
        start:    Start time in beats (from start of item)
        duration: Duration in beats
        velocity: 0–127
    """

    pitch: int
    start: float    # beats
    duration: float  # beats
    velocity: int = 64


@dataclass
class ArrangementTrack:
    """A track in the REAPER arrangement with index and display name."""

    index: int
    name: str


@dataclass
class ArrangementItemDef:
    """An item placed in the arrangement referencing a pattern on a track.

    Attributes:
        pattern: Pattern ID
        bar:     Start position in bars (float)
        bars:    Length in bars (float)
        track:   Track index
    """

    pattern: int
    bar: float
    bars: float
    track: int


@dataclass
class PatternDef:
    """A pattern definition with generator and variation axes.

    Attributes:
        id:           Unique pattern ID
        name:         Display name (e.g. "Kick Main")
        instrument:   Sample/instrument key (e.g. "kick", "snare")
        channel:      MIDI channel (11 = kick, 12 = snare, etc.)
        bars:         Length in bars
        generator:    Generator function name
        velocity_mult: Velocity multiplier (0.85–1.1)
        density:      Note density 0.0–1.0
    """

    id: int
    name: str
    instrument: str
    channel: int
    bars: int
    generator: str
    velocity_mult: float = 1.0
    density: float = 1.0


@dataclass
class ClipDef:
    """A clip placed on a track — either audio or MIDI.

    Attributes:
        position: Start position in beats
        length:   Duration in beats
        audio_path: Absolute path to audio file (for audio clips)
        midi_notes: List of MIDI notes (for MIDI clips)
        name: Display name
        loop:     Whether the audio clip loops (default False)
        fade_in:  Fade-in duration in seconds (default 0.0)
        fade_out: Fade-out duration in seconds (default 0.0)
    """

    position: float
    length: float
    name: str = ""
    audio_path: str | None = None           # for audio clips
    midi_notes: list[MidiNote] = field(default_factory=list)  # for MIDI clips
    midi_cc: list[CCEvent] = field(default_factory=list)      # MIDI CC events
    loop: bool = False
    fade_in: float = 0.0
    fade_out: float = 0.0
    vol_mult: float = 1.0

    @property
    def is_midi(self) -> bool:
        return bool(self.midi_notes)

    @property
    def is_audio(self) -> bool:
        return self.audio_path is not None


@dataclass
class PluginDef:
    """A VST plugin instance on a track.

    Attributes:
        name:  Display name (e.g. "Serum 2")
        path:  Plugin path/identifier (e.g. "VST3: Serum 2 (Xfer Records)")
        index: Chain position (0 = first)
        params: Optional dict of parameter index → value
    """

    name: str
    path: str
    index: int = 0
    params: dict[int, float] = field(default_factory=dict)
    preset_data: list[str] | None = None
    role: str = ""  # track role for role-aware preset lookup (e.g. "bass", "lead", "pad")


@dataclass
class TrackDef:
    """A track in the REAPER project.

    Attributes:
        name:       Track display name
        volume:     0.0–1.0 (maps to REAPER volume fader)
        pan:        -1.0 to 1.0
        color:      REAPER color index (0–67), 0 = default
        clips:      Audio/MIDI clips placed on this track
        plugins:    VST plugins on this track
        send_reverb: Reverb send level 0.0–1.0
        send_delay: Delay send level 0.0–1.0
        send_level:  Dict mapping return track index → send level 0.0–1.0
    """

    name: str
    volume: float = 0.85
    pan: float = 0.0
    color: int = 0
    clips: list[ClipDef] = field(default_factory=list)
    plugins: list[PluginDef] = field(default_factory=list)
    send_reverb: float = 0.0
    send_delay: float = 0.0
    send_level: dict[int, float] = field(default_factory=dict)


@dataclass
class SectionDef:
    """A section in the song arrangement with energy and dynamics.

    Attributes:
        name:        Display name (e.g. "intro", "chorus", "verse")
        bars:        Length in bars
        energy:      Energy level 0.0–1.0 (controls velocity multiplier)
        velocity_mult: Velocity multiplier applied to all notes in section
        vol_mult:    Volume multiplier applied to track in section
    """

    name: str
    bars: int
    energy: float = 0.5
    velocity_mult: float = 1.0
    vol_mult: float = 1.0


@dataclass
class SongDefinition:
    """Complete song definition — the source of truth for one .rpp file.

    This holds the minimal data needed by RPPBuilder to write a complete .rpp.

    Attributes:
        meta:             Song metadata (bpm, key, title, time signature)
        tracks:           List of REAPER tracks (TrackDef) with clips and plugins
        patterns:         Pattern definitions (PatternDef) for arrangement
        items:            Arrangement items (ArrangementItemDef) referencing patterns
        progression_name: Chord progression name (e.g. "i-VII-VI-VII")
        section_template: Section template name (default "standard")
        samples:          Sample file map (name → filename)
        sections:         Section definitions in playback order
        master_plugins:   List of plugin registry keys for master FX chain
    """

    meta: SongMeta
    tracks: list[TrackDef] = field(default_factory=list)
    patterns: list[PatternDef] = field(default_factory=list)
    items: list[ArrangementItemDef] = field(default_factory=list)
    progression_name: str = "i-VII-VI-VII"
    section_template: str = "standard"
    samples: dict[str, str] = field(default_factory=dict)
    sections: list[SectionDef] = field(default_factory=list)
    master_plugins: list[str] = field(default_factory=list)

    # -------------------------------------------------------------------------
    # Validation
    # -------------------------------------------------------------------------

    def validate(self) -> list[str]:
        """Return list of validation errors (empty list = valid)."""
        errors: list[str] = []

        # BPM range
        if not (20 <= self.meta.bpm <= 999):
            errors.append(f"meta.bpm must be 20–999, got {self.meta.bpm}")

        # Key format
        if not _KEY_RE.match(self.meta.key):
            errors.append(f"meta.key must match ^[A-G][b#]?m?$, got '{self.meta.key}'")

        # Track names unique
        names = [t.name for t in self.tracks]
        if len(names) != len(set(names)):
            errors.append("Duplicate track names found")

        # Check for clips with neither audio_path nor midi_notes
        for ti, track in enumerate(self.tracks):
            for ci, clip in enumerate(track.clips):
                if not clip.is_audio and not clip.is_midi:
                    errors.append(
                        f"tracks[{ti}].clips[{ci}] has no audio_path and no midi_notes"
                    )

        return errors

    # -------------------------------------------------------------------------
    # Computed helpers
    # -------------------------------------------------------------------------

    @property
    def length_beats(self) -> float:
        """Compute the total length in beats from all clips."""
        if not self.tracks:
            return 0.0
        max_end = 0.0
        for track in self.tracks:
            for clip in track.clips:
                end = clip.position + clip.length
                if end > max_end:
                    max_end = end
        return max_end

    # -------------------------------------------------------------------------
    # Serialization
    # -------------------------------------------------------------------------

    def to_json(self, indent: int = 2) -> str:
        """Serialize to a JSON string."""
        import json
        from dataclasses import asdict
        return json.dumps(asdict(self), indent=indent, ensure_ascii=False)