"""
music.py — Global music manager for Powernet.

Handles track cycling, volume control, and beat-phase computation
for music-reactive UI effects.

Usage
-----
::

    from helpers.music import music_manager, MUSIC_END

    music_manager.start()              # begin playing (call once after pygame.init)
    music_manager.set_volume(0.55)     # 0.0–1.0

    # In event loop:
    if event.type == MUSIC_END:
        music_manager.next_track()

    # For music-reactive UI:
    beat = music_manager.beat_phase()  # 0.0 – 1.0 peak on each beat
"""

from __future__ import annotations

import math
from pathlib import Path

import pygame

# ---------------------------------------------------------------------------
# Track catalogue
# ---------------------------------------------------------------------------

# Playlists — "menu" uses Candy+Modern Bits; "game" uses all other tracks.
_PLAYLISTS: dict[str, list[str]] = {
    "menu": [
        "Three Red Hearts - Candy.ogg",
        "Three Red Hearts - Modern Bits.ogg",
    ],
    "game": [
        "Three Red Hearts - Box Jump.ogg",
        "Three Red Hearts - Connected.ogg",
        "Three Red Hearts - Deep Blue.ogg",
        "Three Red Hearts - Go (No Vocal).ogg",
        "Three Red Hearts - Penguin Town.ogg",
        "Three Red Hearts - Pixel War 1.ogg",
        "Three Red Hearts - Pixel War 2.ogg",
        "Three Red Hearts - Rumble at the Gates.ogg",
        "Three Red Hearts - Sanctuary.ogg",
        "Three Red Hearts - Save the City.ogg",
    ],
}

# Display names keyed by filename
_TRACK_DISPLAY: dict[str, str] = {
    "Three Red Hearts - Candy.ogg":             "Candy",
    "Three Red Hearts - Modern Bits.ogg":       "Modern Bits",
    "Three Red Hearts - Box Jump.ogg":          "Box Jump",
    "Three Red Hearts - Connected.ogg":         "Connected",
    "Three Red Hearts - Deep Blue.ogg":         "Deep Blue",
    "Three Red Hearts - Go (No Vocal).ogg":     "Go",
    "Three Red Hearts - Penguin Town.ogg":      "Penguin Town",
    "Three Red Hearts - Pixel War 1.ogg":       "Pixel War 1",
    "Three Red Hearts - Pixel War 2.ogg":       "Pixel War 2",
    "Three Red Hearts - Rumble at the Gates.ogg": "Rumble at the Gates",
    "Three Red Hearts - Sanctuary.ogg":         "Sanctuary",
    "Three Red Hearts - Save the City.ogg":     "Save the City",
}

# Approximate BPM keyed by filename
_TRACK_BPM_MAP: dict[str, float] = {
    "Three Red Hearts - Candy.ogg":             120.0,
    "Three Red Hearts - Modern Bits.ogg":       128.0,
    "Three Red Hearts - Box Jump.ogg":          130.0,
    "Three Red Hearts - Connected.ogg":         120.0,
    "Three Red Hearts - Deep Blue.ogg":         110.0,
    "Three Red Hearts - Go (No Vocal).ogg":     140.0,
    "Three Red Hearts - Penguin Town.ogg":      120.0,
    "Three Red Hearts - Pixel War 1.ogg":       135.0,
    "Three Red Hearts - Pixel War 2.ogg":       135.0,
    "Three Red Hearts - Rumble at the Gates.ogg": 125.0,
    "Three Red Hearts - Sanctuary.ogg":         100.0,
    "Three Red Hearts - Save the City.ogg":     120.0,
}

# Active playlist (starts as menu tracks for backward compat)
_TRACKS: list[str]      = _PLAYLISTS["menu"]
_TRACK_NAMES: list[str]   = [_TRACK_DISPLAY.get(t, t)     for t in _TRACKS]  # noqa: F841
_TRACK_BPM:   list[float] = [_TRACK_BPM_MAP.get(t, 120.0) for t in _TRACKS]  # noqa: F841

# ---------------------------------------------------------------------------
# Event constant
# ---------------------------------------------------------------------------

#: Post this event type when a track ends; handle it in your main loop.
MUSIC_END: int = pygame.USEREVENT + 1


# ---------------------------------------------------------------------------
# MusicManager
# ---------------------------------------------------------------------------


class MusicManager:
    """
    Manages background music playback and provides beat-phase data for
    music-reactive animations.

    Instantiated once as the module-level ``music_manager`` singleton.
    """

    def __init__(self) -> None:
        self._track_idx:    int   = 0
        self._volume:       float = 0.55
        self._ready:        bool  = False
        self._playlist:     str   = "menu"
        self._tracks:       list[str]   = list(_PLAYLISTS["menu"])
        self._track_names:  list[str]   = [_TRACK_DISPLAY.get(t, t) for t in self._tracks]
        self._track_bpm:    list[float] = [_TRACK_BPM_MAP.get(t, 120.0) for t in self._tracks]

    # ---- public API ----

    @property
    def track_name(self) -> str:
        """Display name of the currently loaded track."""
        if not self._track_names:
            return ""
        return self._track_names[self._track_idx % len(self._track_names)]

    @property
    def track_idx(self) -> int:
        return self._track_idx

    @property
    def is_playing(self) -> bool:
        try:
            return pygame.mixer.music.get_busy()
        except Exception:
            return False

    def set_playlist(self, playlist_name: str) -> None:
        """Switch to a named playlist and start playing it from the beginning."""
        import random
        if playlist_name not in _PLAYLISTS:
            return
        if playlist_name == self._playlist:
            return
        self._playlist = playlist_name
        tracks = list(_PLAYLISTS[playlist_name])
        if playlist_name == "game":
            random.shuffle(tracks)
        self._tracks      = tracks
        self._track_names = [_TRACK_DISPLAY.get(t, t) for t in tracks]
        self._track_bpm   = [_TRACK_BPM_MAP.get(t, 120.0) for t in tracks]
        self._track_idx   = 0
        self._play_current(fade_ms=1500)

    def start(self, track_idx: int = 0) -> None:
        """Initialise the mixer (if needed) and start the given track."""
        if not self._ensure_mixer():
            return
        self._track_idx = track_idx % max(1, len(self._tracks))
        self._play_current(fade_ms=0)   # immediate start on first play

    def next_track(self) -> None:
        """Advance to the next track and play it with a fade-in."""
        if not self._ensure_mixer():
            return
        self._track_idx = (self._track_idx + 1) % max(1, len(self._tracks))
        self._play_current(fade_ms=1500)

    def stop(self) -> None:
        try:
            pygame.mixer.music.stop()
        except Exception:
            pass
        self._ready = False

    def set_volume(self, volume: float) -> None:
        """Set playback volume (0.0 – 1.0)."""
        self._volume = max(0.0, min(1.0, volume))
        try:
            pygame.mixer.music.set_volume(self._volume)
        except Exception:
            pass

    def beat_phase(self) -> float:
        """
        Return a beat-synchronised intensity value in ``[0, 1]``.

        The value peaks sharply at each beat onset and decays exponentially
        until the next beat — suitable for driving a glow pulse animation.
        """
        if not self._ready:
            return 0.0
        try:
            ms = pygame.mixer.music.get_pos()
            if ms < 0:
                return 0.0
            idx = self._track_idx % max(1, len(self._track_bpm))
            bpm = self._track_bpm[idx] if self._track_bpm else 120.0
            beat_ms = 60_000.0 / bpm
            phase = (ms % beat_ms) / beat_ms   # 0 → 1 within one beat
            # Sharp rise at onset (phase≈0), exponential decay to next beat
            return math.exp(-phase * 7.0)
        except Exception:
            return 0.0

    # ---- private ----

    @staticmethod
    def _music_dir() -> Path:
        return Path(__file__).parent.parent / "assets" / "music"

    def _ensure_mixer(self) -> bool:
        try:
            if not pygame.mixer.get_init():
                pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=1024)
            return True
        except Exception as exc:
            print(f"[MusicManager] Mixer init failed: {exc}")
            return False

    def _play_current(self, fade_ms: int = 1500) -> None:
        if not self._tracks:
            return
        try:
            idx  = self._track_idx % len(self._tracks)
            path = self._music_dir() / self._tracks[idx]
            pygame.mixer.music.load(str(path))
            pygame.mixer.music.set_endevent(MUSIC_END)
            pygame.mixer.music.set_volume(self._volume)
            pygame.mixer.music.play(fade_ms=fade_ms)
            self._ready = True
        except Exception as exc:
            print(f"[MusicManager] Load error: {exc}")
            self._ready = False


# ---------------------------------------------------------------------------
# Module-level singleton
# ---------------------------------------------------------------------------

music_manager = MusicManager()
