"""
Streaming recipe executor.

Implements the full recipe logic for real-time streaming:
- Scans (state machines that evolve on beats)
- Process-pair template (two clips with sporadic effects, blended)
- Cycle-crossfade (dynamic composition cycling through video pairs)
"""

import random
import numpy as np
from typing import Dict, List, Any, Optional
from dataclasses import dataclass, field


@dataclass
class ScanState:
    """State for a scan (beat-driven state machine)."""
    value: Any = 0
    rng: random.Random = field(default_factory=random.Random)


class StreamingScans:
    """
    Real-time scan executor.

    Scans are state machines that evolve on each beat.
    They drive effect parameters like invert triggers, hue shifts, etc.
    """

    def __init__(self, seed: int = 42, n_sources: int = 4):
        self.master_seed = seed
        self.n_sources = n_sources
        self.scans: Dict[str, ScanState] = {}
        self.beat_count = 0
        self.current_time = 0.0
        self.last_beat_time = 0.0
        self._init_scans()

    def _init_scans(self):
        """Initialize all scans with their own RNG seeds."""
        scan_names = []

        # Per-pair scans (dynamic based on n_sources)
        for i in range(self.n_sources):
            scan_names.extend([
                f"inv_a_{i}", f"inv_b_{i}", f"hue_a_{i}", f"hue_b_{i}",
                f"ascii_a_{i}", f"ascii_b_{i}", f"pair_mix_{i}", f"pair_rot_{i}",
            ])

        # Global scans
        scan_names.extend(["whole_spin", "ripple_gate", "cycle"])

        for i, name in enumerate(scan_names):
            rng = random.Random(self.master_seed + i)
            self.scans[name] = ScanState(value=self._init_value(name), rng=rng)

    def _init_value(self, name: str) -> Any:
        """Get initial value for a scan."""
        if name.startswith("inv_") or name.startswith("ascii_"):
            return 0  # Counter for remaining beats
        elif name.startswith("hue_"):
            return {"rem": 0, "hue": 0}
        elif name.startswith("pair_mix"):
            return {"rem": 0, "opacity": 0.5}
        elif name.startswith("pair_rot"):
            pair_idx = int(name.split("_")[-1])
            rot_dir = 1 if pair_idx % 2 == 0 else -1
            return {"beat": 0, "clen": 25, "dir": rot_dir, "angle": 0}
        elif name == "whole_spin":
            return {
                "phase": 0,        # 0 = waiting, 1 = spinning
                "beat": 0,         # beats into current phase
                "plen": 20,        # beats in this phase
                "dir": 1,          # spin direction
                "total_angle": 0.0,      # cumulative angle after all spins
                "spin_start_angle": 0.0, # angle when current spin started
                "spin_start_time": 0.0,  # time when current spin started
                "spin_end_time": 0.0,    # estimated time when spin ends
            }
        elif name == "ripple_gate":
            return {"rem": 0, "cx": 0.5, "cy": 0.5}
        elif name == "cycle":
            return {"cycle": 0, "beat": 0, "clen": 60}
        return 0

    def on_beat(self):
        """Update all scans on a beat."""
        self.beat_count += 1
        # Estimate beat interval from last two beats
        beat_interval = self.current_time - self.last_beat_time if self.last_beat_time > 0 else 0.5
        self.last_beat_time = self.current_time

        for name, state in self.scans.items():
            state.value = self._step_scan(name, state.value, state.rng, beat_interval)

    def _step_scan(self, name: str, value: Any, rng: random.Random, beat_interval: float = 0.5) -> Any:
        """Step a scan forward by one beat."""

        # Invert scan: 10% chance, lasts 1-5 beats
        if name.startswith("inv_"):
            if value > 0:
                return value - 1
            elif rng.random() < 0.1:
                return rng.randint(1, 5)
            return 0

        # Hue scan: 10% chance, random hue 30-330, lasts 1-5 beats
        elif name.startswith("hue_"):
            if value["rem"] > 0:
                return {"rem": value["rem"] - 1, "hue": value["hue"]}
            elif rng.random() < 0.1:
                return {"rem": rng.randint(1, 5), "hue": rng.uniform(30, 330)}
            return {"rem": 0, "hue": 0}

        # ASCII scan: 5% chance, lasts 1-3 beats
        elif name.startswith("ascii_"):
            if value > 0:
                return value - 1
            elif rng.random() < 0.05:
                return rng.randint(1, 3)
            return 0

        # Pair mix: changes every 1-11 beats
        elif name.startswith("pair_mix"):
            if value["rem"] > 0:
                return {"rem": value["rem"] - 1, "opacity": value["opacity"]}
            return {"rem": rng.randint(1, 11), "opacity": rng.choice([0, 0.5, 1.0])}

        # Pair rotation: full rotation every 20-30 beats
        elif name.startswith("pair_rot"):
            beat = value["beat"]
            clen = value["clen"]
            dir_ = value["dir"]
            angle = value["angle"]

            if beat + 1 < clen:
                new_angle = angle + dir_ * (360 / clen)
                return {"beat": beat + 1, "clen": clen, "dir": dir_, "angle": new_angle}
            else:
                return {"beat": 0, "clen": rng.randint(20, 30), "dir": -dir_, "angle": angle}

        # Whole spin: sporadic 720 degree spins (cumulative - stays rotated)
        elif name == "whole_spin":
            phase = value["phase"]
            beat = value["beat"]
            plen = value["plen"]
            dir_ = value["dir"]
            total_angle = value.get("total_angle", 0.0)
            spin_start_angle = value.get("spin_start_angle", 0.0)
            spin_start_time = value.get("spin_start_time", 0.0)
            spin_end_time = value.get("spin_end_time", 0.0)

            if phase == 1:
                # Currently spinning
                if beat + 1 < plen:
                    return {
                        "phase": 1, "beat": beat + 1, "plen": plen, "dir": dir_,
                        "total_angle": total_angle,
                        "spin_start_angle": spin_start_angle,
                        "spin_start_time": spin_start_time,
                        "spin_end_time": spin_end_time,
                    }
                else:
                    # Spin complete - update total_angle with final spin
                    new_total = spin_start_angle + dir_ * 720.0
                    return {
                        "phase": 0, "beat": 0, "plen": rng.randint(20, 40), "dir": dir_,
                        "total_angle": new_total,
                        "spin_start_angle": new_total,
                        "spin_start_time": self.current_time,
                        "spin_end_time": self.current_time,
                    }
            else:
                # Waiting phase
                if beat + 1 < plen:
                    return {
                        "phase": 0, "beat": beat + 1, "plen": plen, "dir": dir_,
                        "total_angle": total_angle,
                        "spin_start_angle": spin_start_angle,
                        "spin_start_time": spin_start_time,
                        "spin_end_time": spin_end_time,
                    }
                else:
                    # Start new spin
                    new_dir = 1 if rng.random() < 0.5 else -1
                    new_plen = rng.randint(10, 25)
                    spin_duration = new_plen * beat_interval
                    return {
                        "phase": 1, "beat": 0, "plen": new_plen, "dir": new_dir,
                        "total_angle": total_angle,
                        "spin_start_angle": total_angle,
                        "spin_start_time": self.current_time,
                        "spin_end_time": self.current_time + spin_duration,
                    }

        # Ripple gate: 5% chance, lasts 1-20 beats
        elif name == "ripple_gate":
            if value["rem"] > 0:
                return {"rem": value["rem"] - 1, "cx": value["cx"], "cy": value["cy"]}
            elif rng.random() < 0.05:
                return {"rem": rng.randint(1, 20),
                        "cx": rng.uniform(0.1, 0.9),
                        "cy": rng.uniform(0.1, 0.9)}
            return {"rem": 0, "cx": 0.5, "cy": 0.5}

        # Cycle: track which video pair is active
        elif name == "cycle":
            beat = value["beat"]
            clen = value["clen"]
            cycle = value["cycle"]

            if beat + 1 < clen:
                return {"cycle": cycle, "beat": beat + 1, "clen": clen}
            else:
                # Move to next pair, vary cycle length
                return {"cycle": (cycle + 1) % 4, "beat": 0,
                        "clen": 40 + (self.beat_count * 7) % 41}

        return value

    def get_emit(self, name: str) -> float:
        """Get emitted value for a scan."""
        value = self.scans[name].value

        if name.startswith("inv_") or name.startswith("ascii_"):
            return 1.0 if value > 0 else 0.0

        elif name.startswith("hue_"):
            return value["hue"] if value["rem"] > 0 else 0.0

        elif name.startswith("pair_mix"):
            return value["opacity"]

        elif name.startswith("pair_rot"):
            return value["angle"]

        elif name == "whole_spin":
            # Smooth time-based interpolation during spin
            phase = value.get("phase", 0)
            if phase == 1:
                # Currently spinning - interpolate based on time
                spin_start_time = value.get("spin_start_time", 0.0)
                spin_end_time = value.get("spin_end_time", spin_start_time + 1.0)
                spin_start_angle = value.get("spin_start_angle", 0.0)
                dir_ = value.get("dir", 1)

                duration = spin_end_time - spin_start_time
                if duration > 0:
                    progress = (self.current_time - spin_start_time) / duration
                    progress = max(0.0, min(1.0, progress))  # clamp to 0-1
                else:
                    progress = 1.0

                return spin_start_angle + progress * 720.0 * dir_
            else:
                # Not spinning - return cumulative angle
                return value.get("total_angle", 0.0)

        elif name == "ripple_gate":
            return 1.0 if value["rem"] > 0 else 0.0

        elif name == "cycle":
            return value

        return 0.0


class StreamingRecipeExecutor:
    """
    Executes a recipe in streaming mode.

    Implements:
    - process-pair: two video clips with opposite effects, blended
    - cycle-crossfade: dynamic cycling through video pairs
    - Final effects: whole-spin rotation, ripple
    """

    def __init__(self, n_sources: int = 4, seed: int = 42):
        self.n_sources = n_sources
        self.scans = StreamingScans(seed, n_sources=n_sources)
        self.last_beat_detected = False
        self.current_time = 0.0

    def on_frame(self, energy: float, is_beat: bool, t: float = 0.0):
        """Called each frame with current audio analysis."""
        self.current_time = t
        self.scans.current_time = t
        # Update scans on beat
        if is_beat and not self.last_beat_detected:
            self.scans.on_beat()
        self.last_beat_detected = is_beat

    def get_effect_params(self, source_idx: int, clip: str, energy: float) -> Dict:
        """
        Get effect parameters for a source clip.

        Args:
            source_idx: Which video source (0-3)
            clip: "a" or "b" (each source has two clips)
            energy: Current audio energy (0-1)
        """
        suffix = f"_{source_idx}"

        # Rotation ranges alternate
        if source_idx % 2 == 0:
            rot_range = [0, 45] if clip == "a" else [0, -45]
            zoom_range = [1, 1.5] if clip == "a" else [1, 0.5]
        else:
            rot_range = [0, -45] if clip == "a" else [0, 45]
            zoom_range = [1, 0.5] if clip == "a" else [1, 1.5]

        return {
            "rotate_angle": rot_range[0] + energy * (rot_range[1] - rot_range[0]),
            "zoom_amount": zoom_range[0] + energy * (zoom_range[1] - zoom_range[0]),
            "invert_amount": self.scans.get_emit(f"inv_{clip}{suffix}"),
            "hue_degrees": self.scans.get_emit(f"hue_{clip}{suffix}"),
            "ascii_mix": 0,  # Disabled - too slow without GPU
            "ascii_char_size": 4 + energy * 28,  # 4-32
        }

    def get_pair_params(self, source_idx: int) -> Dict:
        """Get blend and rotation params for a video pair."""
        suffix = f"_{source_idx}"
        return {
            "blend_opacity": self.scans.get_emit(f"pair_mix{suffix}"),
            "pair_rotation": self.scans.get_emit(f"pair_rot{suffix}"),
        }

    def get_cycle_weights(self) -> List[float]:
        """Get blend weights for cycle-crossfade composition."""
        cycle_state = self.scans.get_emit("cycle")
        active = cycle_state["cycle"]
        beat = cycle_state["beat"]
        clen = cycle_state["clen"]
        n = self.n_sources

        phase3 = beat * 3
        weights = []

        for p in range(n):
            prev = (p + n - 1) % n

            if active == p:
                if phase3 < clen:
                    w = 0.9
                elif phase3 < clen * 2:
                    w = 0.9 - ((phase3 - clen) / clen) * 0.85
                else:
                    w = 0.05
            elif active == prev:
                if phase3 < clen:
                    w = 0.05
                elif phase3 < clen * 2:
                    w = 0.05 + ((phase3 - clen) / clen) * 0.85
                else:
                    w = 0.9
            else:
                w = 0.05

            weights.append(w)

        # Normalize
        total = sum(weights)
        if total > 0:
            weights = [w / total for w in weights]

        return weights

    def get_cycle_zooms(self) -> List[float]:
        """Get zoom amounts for cycle-crossfade."""
        cycle_state = self.scans.get_emit("cycle")
        active = cycle_state["cycle"]
        beat = cycle_state["beat"]
        clen = cycle_state["clen"]
        n = self.n_sources

        phase3 = beat * 3
        zooms = []

        for p in range(n):
            prev = (p + n - 1) % n

            if active == p:
                if phase3 < clen:
                    z = 1.0
                elif phase3 < clen * 2:
                    z = 1.0 + ((phase3 - clen) / clen) * 1.0
                else:
                    z = 0.1
            elif active == prev:
                if phase3 < clen:
                    z = 3.0  # Start big
                elif phase3 < clen * 2:
                    z = 3.0 - ((phase3 - clen) / clen) * 2.0  # Shrink to 1.0
                else:
                    z = 1.0
            else:
                z = 0.1

            zooms.append(z)

        return zooms

    def get_final_effects(self, energy: float) -> Dict:
        """Get final composition effects (whole-spin, ripple)."""
        ripple_gate = self.scans.get_emit("ripple_gate")
        ripple_state = self.scans.scans["ripple_gate"].value

        return {
            "whole_spin_angle": self.scans.get_emit("whole_spin"),
            "ripple_amplitude": ripple_gate * (5 + energy * 45),  # 5-50
            "ripple_cx": ripple_state["cx"],
            "ripple_cy": ripple_state["cy"],
        }
