#!/usr/bin/env python3
"""
Enhanced Oscilloscope with LFO Modulation Chain

This demo features:
1. Slower frame rate (15 FPS) for human appreciation
2. Reduced flicker using cursor positioning
3. LFO modulation chain: LFO1 modulates LFO2 frequency
4. Multiple visualization modes

Usage:
    # Simple LFO
    uv run python scripts/demo_oscilloscope_mod.py --lfo

    # LFO modulation chain: LFO1 modulates LFO2 frequency
    uv run python scripts/demo_oscilloscope_mod.py --modulate --lfo

    # Custom modulation depth and rate
    uv run python scripts/demo_oscilloscope_mod.py --modulate --lfo --mod-depth 0.5 --mod-rate 0.25
"""

import argparse
import sys
import time
from pathlib import Path

# Add mainline to path
sys.path.insert(0, str(Path(__file__).parent.parent))

from engine.sensors.oscillator import OscillatorSensor, register_oscillator_sensor


class ModulatedOscillator:
    """
    Oscillator with frequency modulation from another oscillator.

    Frequency = base_frequency + (modulator_value * modulation_depth)
    """

    def __init__(
        self,
        name: str,
        waveform: str = "sine",
        base_frequency: float = 1.0,
        modulator: "OscillatorSensor | None" = None,
        modulation_depth: float = 0.5,
    ):
        self.name = name
        self.waveform = waveform
        self.base_frequency = base_frequency
        self.modulator = modulator
        self.modulation_depth = modulation_depth

        # Create the oscillator sensor
        register_oscillator_sensor(
            name=name, waveform=waveform, frequency=base_frequency
        )
        self.osc = OscillatorSensor(
            name=name, waveform=waveform, frequency=base_frequency
        )
        self.osc.start()

    def read(self):
        """Read current value, applying modulation if present."""
        # Update frequency based on modulator
        if self.modulator:
            mod_reading = self.modulator.read()
            if mod_reading:
                # Modulator value (0-1) affects frequency
                # Map 0-1 to -modulation_depth to +modulation_depth
                mod_offset = (mod_reading.value - 0.5) * 2 * self.modulation_depth
                effective_freq = self.base_frequency + mod_offset
                # Clamp to reasonable range
                effective_freq = max(0.1, min(effective_freq, 20.0))
                self.osc._frequency = effective_freq

        return self.osc.read()

    def get_phase(self):
        """Get current phase."""
        return self.osc._phase

    def get_effective_frequency(self):
        """Get current effective frequency (after modulation)."""
        if self.modulator and self.modulator.read():
            mod_reading = self.modulator.read()
            mod_offset = (mod_reading.value - 0.5) * 2 * self.modulation_depth
            return max(0.1, min(self.base_frequency + mod_offset, 20.0))
        return self.base_frequency

    def stop(self):
        """Stop the oscillator."""
        self.osc.stop()


def render_dual_waveform(
    width: int,
    height: int,
    modulator: OscillatorSensor,
    modulated: ModulatedOscillator,
    frame: int,
) -> str:
    """Render both modulator and modulated waveforms."""
    # Get readings
    mod_reading = modulator.read()
    mod_val = mod_reading.value if mod_reading else 0.5

    modulated_reading = modulated.read()
    modulated_val = modulated_reading.value if modulated_reading else 0.5

    # Build visualization
    lines = []

    # Header with sensor info
    header1 = f"MODULATOR: {modulator.name} | Wave: {modulator.waveform} | Freq: {modulator.frequency:.2f}Hz"
    header2 = f"MODULATED: {modulated.name} | Wave: {modulated.waveform} | Base: {modulated.base_frequency:.2f}Hz | Eff: {modulated.get_effective_frequency():.2f}Hz"
    lines.append(header1)
    lines.append(header2)
    lines.append("─" * width)

    # Render modulator waveform (top half)
    top_height = (height - 5) // 2
    waveform_fn = modulator.WAVEFORMS[modulator.waveform]

    # Calculate time offset for scrolling
    mod_time_offset = modulator._phase * modulator.frequency * 0.3

    for row in range(top_height):
        row_pos = 1.0 - (row / (top_height - 1))
        line_chars = []
        for col in range(width):
            col_fraction = col / width
            time_pos = mod_time_offset + col_fraction
            sample = waveform_fn(time_pos * modulator.frequency * 2)
            tolerance = 1.0 / (top_height - 1)
            if abs(sample - row_pos) < tolerance:
                line_chars.append("█")
            else:
                line_chars.append(" ")
        lines.append("".join(line_chars))

    # Separator line with modulation info
    lines.append(
        f"─ MODULATION: depth={modulated.modulation_depth:.2f} | mod_value={mod_val:.2f} ─"
    )

    # Render modulated waveform (bottom half)
    bottom_height = height - top_height - 5
    waveform_fn = modulated.osc.WAVEFORMS[modulated.waveform]

    # Calculate time offset for scrolling
    modulated_time_offset = (
        modulated.get_phase() * modulated.get_effective_frequency() * 0.3
    )

    for row in range(bottom_height):
        row_pos = 1.0 - (row / (bottom_height - 1))
        line_chars = []
        for col in range(width):
            col_fraction = col / width
            time_pos = modulated_time_offset + col_fraction
            sample = waveform_fn(time_pos * modulated.get_effective_frequency() * 2)
            tolerance = 1.0 / (bottom_height - 1)
            if abs(sample - row_pos) < tolerance:
                line_chars.append("█")
            else:
                line_chars.append(" ")
        lines.append("".join(line_chars))

    # Footer with current values
    footer = f"Mod Value: {mod_val:.3f} | Modulated Value: {modulated_val:.3f} | Frame: {frame}"
    lines.append(footer)

    return "\n".join(lines)


def render_single_waveform(
    width: int,
    height: int,
    osc: OscillatorSensor,
    frame: int,
) -> str:
    """Render a single waveform (for non-modulated mode)."""
    reading = osc.read()
    current_value = reading.value if reading else 0.5
    phase = osc._phase
    frequency = osc.frequency

    # Build visualization
    lines = []

    # Header with sensor info
    header = (
        f"Oscilloscope: {osc.name} | Wave: {osc.waveform} | "
        f"Freq: {frequency:.2f}Hz | Phase: {phase:.2f}"
    )
    lines.append(header)
    lines.append("─" * width)

    # Draw oscilloscope trace
    waveform_fn = osc.WAVEFORMS[osc.waveform]
    time_offset = phase * frequency * 0.3

    for row in range(height - 3):
        row_pos = 1.0 - (row / (height - 4))
        line_chars = []
        for col in range(width):
            col_fraction = col / width
            time_pos = time_offset + col_fraction
            sample = waveform_fn(time_pos * frequency * 2)
            tolerance = 1.0 / (height - 4)
            if abs(sample - row_pos) < tolerance:
                line_chars.append("█")
            else:
                line_chars.append(" ")
        lines.append("".join(line_chars))

    # Footer
    footer = f"Value: {current_value:.3f} | Frame: {frame} | Phase: {phase:.2f}"
    lines.append(footer)

    return "\n".join(lines)


def demo_oscilloscope_mod(
    waveform: str = "sine",
    base_freq: float = 1.0,
    modulate: bool = False,
    mod_waveform: str = "sine",
    mod_freq: float = 0.5,
    mod_depth: float = 0.5,
    frames: int = 0,
):
    """Run enhanced oscilloscope demo with modulation support."""
    # Frame timing for smooth 15 FPS
    frame_interval = 1.0 / 15.0  # 66.67ms per frame

    print("Enhanced Oscilloscope Demo")
    print("Frame rate: 15 FPS (66ms per frame)")
    if modulate:
        print(
            f"Modulation: {mod_waveform} @ {mod_freq}Hz -> {waveform} @ {base_freq}Hz"
        )
        print(f"Modulation depth: {mod_depth}")
    else:
        print(f"Waveform: {waveform} @ {base_freq}Hz")
    if frames > 0:
        print(f"Running for {frames} frames")
    else:
        print("Press Ctrl+C to stop")
    print()

    # Create oscillators
    if modulate:
        # Create modulation chain: modulator -> modulated
        modulator = OscillatorSensor(
            name="modulator", waveform=mod_waveform, frequency=mod_freq
        )
        modulator.start()

        modulated = ModulatedOscillator(
            name="modulated",
            waveform=waveform,
            base_frequency=base_freq,
            modulator=modulator,
            modulation_depth=mod_depth,
        )
    else:
        # Single oscillator
        register_oscillator_sensor(
            name="oscilloscope", waveform=waveform, frequency=base_freq
        )
        osc = OscillatorSensor(
            name="oscilloscope", waveform=waveform, frequency=base_freq
        )
        osc.start()

    # Run demo loop with consistent timing
    try:
        frame = 0
        last_time = time.time()

        while frames == 0 or frame < frames:
            # Render based on mode
            if modulate:
                visualization = render_dual_waveform(
                    80, 30, modulator, modulated, frame
                )
            else:
                visualization = render_single_waveform(80, 22, osc, frame)

            # Use cursor positioning instead of full clear to reduce flicker
            print("\033[H" + visualization)

            # Calculate sleep time for consistent 15 FPS
            elapsed = time.time() - last_time
            sleep_time = max(0, frame_interval - elapsed)
            time.sleep(sleep_time)
            last_time = time.time()

            frame += 1

    except KeyboardInterrupt:
        print("\n\nDemo stopped by user")

    finally:
        if modulate:
            modulator.stop()
            modulated.stop()
        else:
            osc.stop()


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Enhanced oscilloscope with LFO modulation chain"
    )
    parser.add_argument(
        "--waveform",
        choices=["sine", "square", "sawtooth", "triangle", "noise"],
        default="sine",
        help="Main waveform type",
    )
    parser.add_argument(
        "--frequency",
        type=float,
        default=1.0,
        help="Main oscillator frequency (LFO range: 0.1-20Hz)",
    )
    parser.add_argument(
        "--lfo",
        action="store_true",
        help="Use slow LFO frequency (0.5Hz) for main oscillator",
    )
    parser.add_argument(
        "--modulate",
        action="store_true",
        help="Enable LFO modulation chain (modulator modulates main oscillator)",
    )
    parser.add_argument(
        "--mod-waveform",
        choices=["sine", "square", "sawtooth", "triangle", "noise"],
        default="sine",
        help="Modulator waveform type",
    )
    parser.add_argument(
        "--mod-freq",
        type=float,
        default=0.5,
        help="Modulator frequency in Hz",
    )
    parser.add_argument(
        "--mod-depth",
        type=float,
        default=0.5,
        help="Modulation depth (0.0-1.0, higher = more frequency variation)",
    )
    parser.add_argument(
        "--frames",
        type=int,
        default=0,
        help="Number of frames to render (0 = infinite until Ctrl+C)",
    )

    args = parser.parse_args()

    # Set frequency based on LFO flag
    base_freq = args.frequency
    if args.lfo:
        base_freq = 0.5

    demo_oscilloscope_mod(
        waveform=args.waveform,
        base_freq=base_freq,
        modulate=args.modulate,
        mod_waveform=args.mod_waveform,
        mod_freq=args.mod_freq,
        mod_depth=args.mod_depth,
        frames=args.frames,
    )
