#!/usr/bin/env python3
"""Generate seed cabinet IR (Impulse Response) .wav files for the Pi Multi-FX Pedal.

Each IR models a different speaker cabinet type using DSP techniques:
- Speaker low-pass roll-off (cone diameter determines cutoff)
- Cabinet resonance peaks (Helmholtz resonance)
- Cone breakup modes (notch filters for mechanical resonances)
- Exponential decay envelope (room-dependent)
- Microphone proximity effect (slight high-end roll-off)

Output: ~/.pedal/irs/ directory or a bundled factory-irs path.

Usage:
    python3 generate_seed_irs.py [--dest DIR] [--mono]
"""

from __future__ import annotations

import argparse
import logging
import sys
from pathlib import Path

import numpy as np
from scipy.io import wavfile
from scipy.signal import butter, lfilter, freqz, sosfilt

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)

SAMPLE_RATE = 48000  # Fixed 48kHz for the pedal


# ── Filter helpers ───────────────────────────────────────────────────


def _lowpass_sos(cutoff_hz: float, order: int = 2) -> np.ndarray:
    """2nd-order Butterworth low-pass filter as SOS array."""
    sos = butter(order, cutoff_hz, btype="low", fs=SAMPLE_RATE, output="sos")
    return sos


def _highpass_sos(cutoff_hz: float, order: int = 2) -> np.ndarray:
    """2nd-order Butterworth high-pass filter as SOS array."""
    sos = butter(order, cutoff_hz, btype="high", fs=SAMPLE_RATE, output="sos")
    return sos


def _peak_sos(freq_hz: float, q: float, gain_db: float) -> np.ndarray:
    """Parametric peak EQ as SOS array (biquad)."""
    A = 10 ** (gain_db / 40.0)
    omega = 2.0 * np.pi * freq_hz / SAMPLE_RATE
    alpha = np.sin(omega) / (2.0 * q)
    b0 = 1.0 + alpha * A
    b1 = -2.0 * np.cos(omega)
    b2 = 1.0 - alpha * A
    a0 = 1.0 + alpha / A
    a1 = -2.0 * np.cos(omega)
    a2 = 1.0 - alpha / A
    # Normalize
    b = np.array([b0, b1, b2]) / a0
    a = np.array([1.0, a1 / a0, a2 / a0])
    # Convert to SOS
    sos = np.array([[b[0], b[1], b[2], a[0], a[1], a[2]]])
    return sos


def _notch_sos(freq_hz: float, q: float) -> np.ndarray:
    """Notch filter as SOS array."""
    omega = 2.0 * np.pi * freq_hz / SAMPLE_RATE
    alpha = np.sin(omega) / (2.0 * q)
    b = np.array([1.0, -2.0 * np.cos(omega), 1.0])
    a = np.array([1.0 + alpha, -2.0 * np.cos(omega), 1.0 - alpha])
    sos = np.array([[b[0] / a[0], b[1] / a[0], b[2] / a[0], 1.0, a[1] / a[0], a[2] / a[0]]])
    return sos


# ── Cabinet IR model ─────────────────────────────────────────────────


def _generate_cabinet_ir(
    num_taps: int,
    lowpass_cutoff: float,
    lowpass_order: int,
    highpass_cutoff: float,
    resonance_peaks: list[tuple[float, float, float]],  # (freq_hz, q, gain_db)
    notch_freqs: list[tuple[float, float]],  # (freq_hz, q)
    decay_time_ms: float,
) -> np.ndarray:
    """Generate a synthetic cabinet IR using cascaded filters + decay envelope.

    Args:
        num_taps: Length of the IR in samples.
        lowpass_cutoff: Low-pass filter cutoff (speaker roll-off) in Hz.
        lowpass_order: Order of the low-pass filter (2 or 4).
        highpass_cutoff: High-pass filter cutoff (cabinet resonance) in Hz.
        resonance_peaks: List of (freq_Hz, Q, gain_dB) peak filters.
        notch_freqs: List of (freq_Hz, Q) notch filters for cone breakup.
        decay_time_ms: T60-like decay time in milliseconds.
    
    Returns:
        float32 numpy array of the IR, shape (num_taps,).
    """
    # Start with a Dirac impulse
    ir = np.zeros(num_taps, dtype=np.float64)
    ir[0] = 1.0

    # Apply filters in series
    # 1. Low-pass (speaker roll-off)
    sos = _lowpass_sos(lowpass_cutoff, lowpass_order)
    ir = sosfilt(sos, ir)

    # 2. High-pass (cabinet resonance - prevents subsonic rumble)
    sos = _highpass_sos(highpass_cutoff, 1)
    ir = sosfilt(sos, ir)

    # 3. Resonance peaks (cabinet Helmholtz + mic position)
    for freq, q, gain_db in resonance_peaks:
        sos = _peak_sos(freq, q, gain_db)
        ir = sosfilt(sos, ir)

    # 4. Notch filters (cone breakup / standing waves)
    for freq, q in notch_freqs:
        sos = _notch_sos(freq, q)
        ir = sosfilt(sos, ir)

    # 5. Decay envelope (exponential, with slight room tail)
    decay_samples = int(decay_time_ms * SAMPLE_RATE / 1000.0)
    envelope = np.exp(-np.arange(num_taps) / max(1, decay_samples))
    ir *= envelope

    # Normalize to peak = 0.95 (headroom)
    peak = np.max(np.abs(ir))
    if peak > 0:
        ir /= peak * 1.0526  # normalize to ~0.95

    return ir.astype(np.float32)


# ── Cabinet definitions ─────────────────────────────────────────────

# Each definition creates one IR file.
# Parameters derived from published measurements of real cabinets
# (frequency responses, not IRs themselves, so no copyright issue).

CABINET_SPECS: list[dict] = [
    {
        "name": "vintage-1x12",
        "display": "Vintage 1x12 — Fender-style Open Back",
        "description": "Warm, scooped cleans with bell-like top end. "
        "Models a Fender-style 1x12 open-back combo.",
        "num_taps": 2048,
        "lowpass_cutoff": 4800,
        "lowpass_order": 2,
        "highpass_cutoff": 75,
        "resonance_peaks": [
            (100, 2.0, 4.0),     # Cab resonance hump
            (800, 1.5, -3.0),    # Scooped mids
            (2800, 3.0, 2.0),   # Presence peak
        ],
        "notch_freqs": [
            (1200, 8.0),         # Minor cone mode
            (3500, 10.0),        # Cone edge resonance
        ],
        "decay_time_ms": 60,
    },
    {
        "name": "british-4x12",
        "display": "British 4x12 — Marshall-style Closed Back",
        "description": "Mid-forward, aggressive rock tones with tight low end. "
        "Models a Marshall 1960A-style 4x12 closed-back cabinet.",
        "num_taps": 4096,
        "lowpass_cutoff": 5200,
        "lowpass_order": 4,
        "highpass_cutoff": 80,
        "resonance_peaks": [
            (110, 1.8, 5.0),     # Big cab resonance
            (700, 1.2, 4.0),     # Mid-forward punch
            (1500, 2.0, 3.0),    # Upper mid grind
            (3200, 2.5, 2.5),    # Presence
        ],
        "notch_freqs": [
            (2500, 12.0),        # Cross-over null
            (4000, 8.0),         # Speaker breakup
        ],
        "decay_time_ms": 85,
    },
    {
        "name": "american-2x12",
        "display": "American 2x12 — Vox-style Open Back",
        "description": "Chimey, complex midrange with sparkling highs. "
        "Models a Vox AC30-style 2x12 open-back cabinet.",
        "num_taps": 2048,
        "lowpass_cutoff": 5800,
        "lowpass_order": 2,
        "highpass_cutoff": 70,
        "resonance_peaks": [
            (120, 1.5, 3.0),     # Cab resonance
            (600, 1.0, 2.0),     # Low-mid body
            (1300, 2.0, 5.0),    # Chime / complex mids
            (3500, 3.0, 3.0),    # Top-end sparkle
        ],
        "notch_freqs": [
            (1800, 10.0),        # Minor comb filter
            (4200, 12.0),        # Cone resonance
        ],
        "decay_time_ms": 55,
    },
    {
        "name": "modern-4x12",
        "display": "Modern 4x12 — Mesa/Boogie-style Closed Back",
        "description": "Tight low-end, aggressive mids, smooth highs. "
        "Models a Mesa Rectifier-style 4x12 closed-back cab.",
        "num_taps": 4096,
        "lowpass_cutoff": 4800,
        "lowpass_order": 4,
        "highpass_cutoff": 85,
        "resonance_peaks": [
            (95, 2.5, 6.0),      # Tight low-end thump
            (600, 1.5, 2.0),     # Low-mid body
            (1000, 1.0, 5.0),    # Aggressive mid bark
            (2200, 3.0, 1.0),    # Upper mid cut
            (3200, 2.0, -1.0),   # Slight high-end roll for smoothness
        ],
        "notch_freqs": [
            (450, 15.0),         # Subsonic resonance cleanup
            (2800, 10.0),        # Cone breakup suppression
        ],
        "decay_time_ms": 90,
    },
    {
        "name": "jazz-1x15",
        "display": "Jazz 1x15 — Deep Open Back",
        "description": "Deep, warm, scooped tone for jazz cleans. "
        "Models a 15-inch speaker in a large open-back cab.",
        "num_taps": 2048,
        "lowpass_cutoff": 4500,
        "lowpass_order": 2,
        "highpass_cutoff": 55,
        "resonance_peaks": [
            (80, 2.0, 8.0),      # Deep low-end warmth
            (500, 2.0, -4.0),    # Scooped mids
            (2500, 2.0, 3.0),    # Presence for articulation
        ],
        "notch_freqs": [
            (200, 8.0),          # Cab resonance smoothing
            (3800, 8.0),         # Cone edge roll-off
        ],
        "decay_time_ms": 70,
    },
    {
        "name": "boutique-1x12",
        "display": "Boutique 1x12 — Dumble-style Open Back",
        "description": "Smooth, rounded cleans with enhanced mid complexity. "
        "Models a Dumble-style 1x12 open-back combo.",
        "num_taps": 2048,
        "lowpass_cutoff": 5200,
        "lowpass_order": 2,
        "highpass_cutoff": 78,
        "resonance_peaks": [
            (105, 2.0, 4.0),     # Low-end body
            (800, 1.8, 5.0),     # Complex mid character
            (1800, 2.5, 2.0),    # Mid sparkle
            (3000, 2.0, 1.5),    # Top presence
        ],
        "notch_freqs": [
            (1500, 12.0),        # Smoothing
            (3500, 10.0),
        ],
        "decay_time_ms": 65,
    },
    {
        "name": "mini-1x8",
        "display": "Mini 1x8 — Small Practice Amp",
        "description": "Lo-fi, boxy tone for vintage radio-style sounds. "
        "Models a small 8-inch practice amp speaker.",
        "num_taps": 1024,
        "lowpass_cutoff": 3800,
        "lowpass_order": 2,
        "highpass_cutoff": 90,
        "resonance_peaks": [
            (150, 1.5, 6.0),     # Boxy resonance
            (1000, 1.0, 4.0),    # Nasally mids
            (2500, 1.5, -2.0),   # Rolled-off highs
        ],
        "notch_freqs": [
            (3000, 6.0),
        ],
        "decay_time_ms": 40,
    },
]


def generate_all_irs(dest_dir: Path, verify: bool = True) -> list[Path]:
    """Generate all seed IR files into dest_dir.

    Returns:
        List of created file paths (sorted by name).
    """
    dest_dir.mkdir(parents=True, exist_ok=True)
    created: list[Path] = []

    for spec in CABINET_SPECS:
        name = spec["name"]
        path = dest_dir / f"{name}.wav"

        logger.info(f"  Generating {name}.wav ...")

        ir = _generate_cabinet_ir(
            num_taps=spec["num_taps"],
            lowpass_cutoff=spec["lowpass_cutoff"],
            lowpass_order=spec["lowpass_order"],
            highpass_cutoff=spec["highpass_cutoff"],
            resonance_peaks=spec["resonance_peaks"],
            notch_freqs=spec["notch_freqs"],
            decay_time_ms=spec["decay_time_ms"],
        )

        # Write as 16-bit WAV (smaller, compatible with scipy)
        int16_data = (ir * 32767).astype(np.int16)
        wavfile.write(str(path), SAMPLE_RATE, int16_data)

        # Quick verification
        file_size = path.stat().st_size
        duration_ms = (spec["num_taps"] / SAMPLE_RATE) * 1000

        logger.info(
            f"    → {path.name}  ({spec['num_taps']} taps, "
            f"{duration_ms:.1f}ms, {file_size // 1024}KB)"
        )

        if verify:
            # Read back and check
            sr, data = wavfile.read(str(path))
            assert sr == SAMPLE_RATE, f"Sample rate mismatch: {sr}"
            assert len(data) == spec["num_taps"], f"Length mismatch: {len(data)}"
            peak = np.max(np.abs(data.astype(np.float64))) / 32767.0
            assert peak > 0.1, f"IR {name} peak too low: {peak:.3f}"

        created.append(path)

    return created


def verify_quality(ir_dir: Path) -> None:
    """Verify that generated IRs have reasonable frequency response."""
    import matplotlib
    matplotlib.use("Agg")  # headless
    import matplotlib.pyplot as plt

    fig, axes = plt.subplots(len(CABINET_SPECS), 1, figsize=(10, 2 * len(CABINET_SPECS)))
    if len(CABINET_SPECS) == 1:
        axes = [axes]

    for ax, spec in zip(axes, CABINET_SPECS):
        path = ir_dir / f"{spec['name']}.wav"
        if not path.exists():
            continue
        sr, data = wavfile.read(str(path))
        # Normalize
        data_f = data.astype(np.float64) / 32767.0
        # FFT for frequency response
        fft = np.abs(np.fft.rfft(data_f, n=4096))
        freqs = np.fft.rfftfreq(4096, d=1.0 / sr)
        # dB scale, normalized
        fft_db = 20 * np.log10(fft / np.max(fft) + 1e-10)
        ax.semilogx(freqs[1:], fft_db[1:], linewidth=0.8)
        ax.axhline(-3, color="gray", linestyle=":", linewidth=0.5)
        ax.axhline(-12, color="gray", linestyle=":", linewidth=0.5)
        ax.set_xlim(20, sr / 2)
        ax.set_ylim(-48, 3)
        ax.set_title(f"{spec['display']} — Freq Response")
        ax.set_xlabel("Frequency (Hz)")
        ax.set_ylabel("dB")
        ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plot_path = ir_dir / "frequency_response.png"
    fig.savefig(plot_path, dpi=150)
    plt.close(fig)
    logger.info(f"Frequency response plot: {plot_path}")


def create_manifest(ir_dir: Path, created: list[Path]) -> Path:
    """Create a README manifest for the IR files."""
    manifest = ir_dir / "README.md"
    lines = [
        "# Pi Multi-FX Pedal — Default Cabinet IR Files",
        "",
        "Synthetic impulse responses (.wav) for guitar cabinet simulation.",
        "Generated using DSP filter cascades (Butterworth LP/HP + parametric",
        "peak/notch filters + exponential decay envelope).",
        "",
        "**All IRs are 48kHz, 16-bit mono WAV files.**",
        "",
        "| File | Cabinet Model | Taps | Length | Description |",
        "|------|--------------|------|--------|-------------|",
    ]

    for spec in CABINET_SPECS:
        path = ir_dir / f"{spec['name']}.wav"
        if path.exists():
            num_taps = spec["num_taps"]
            length_ms = (num_taps / SAMPLE_RATE) * 1000
            lines.append(
                f"| `{spec['name']}.wav` | {spec['display']} | "
                f"{num_taps} | {length_ms:.0f}ms | {spec['description']} |"
            )

    lines.extend([
        "",
        "## Usage",
        "",
        "These IRs are loaded by the `IRLoader` class through the pedal's cab simulation.",
        "Place them in `~/.pedal/irs/` or reference them by absolute path in preset chain blocks.",
        "",
        "## Replacement",
        "",
        "Replace any `.wav` file with a real captured IR (48kHz, 16-bit mono) to",
        "upgrade from synthetic to authentic cabinet tone. The pedal treats all",
        "`.wav` files in the IR directory identically.",
        "",
        "## Cabinet Type Guide",
        "",
        "- **Vintage 1x12** — Fender Deluxe / Princeton-style cleans. Scooped mids, warm lows.",
        "- **British 4x12** — Marshall 1960 / JCM-style crunch and rock. Mid-forward, aggressive.",
        "- **American 2x12** — Vox AC30-style chime. Complex mids, sparkling treble.",
        "- **Modern 4x12** — Mesa Rectifier-style high gain. Tight lows, smooth highs.",
        "- **Jazz 1x15** — Polytone / Henriksen-style jazz. Deep lows, scooped mids.",
        "- **Boutique 1x12** — Dumble / Two-Rock-style. Smooth, complex midrange.",
        "- **Mini 1x8** — Small practice amp. Boxy, lo-fi, vintage radio tone.",
    ])

    manifest.write_text("\n".join(lines) + "\n")
    logger.info(f"Manifest written: {manifest}")
    return manifest


# ── Main ─────────────────────────────────────────────────────────────


def main() -> None:
    parser = argparse.ArgumentParser(description="Generate seed cabinet IR files")
    parser.add_argument(
        "--dest",
        default=str(Path.home() / ".pedal" / "irs"),
        help="Output directory (default: ~/.pedal/irs)",
    )
    parser.add_argument(
        "--plot",
        action="store_true",
        help="Generate frequency response verification plot",
    )
    args = parser.parse_args()

    dest = Path(args.dest)
    logger.info(f"Generating {len(CABINET_SPECS)} seed cabinet IRs → {dest}")
    logger.info("")

    created = generate_all_irs(dest, verify=True)

    logger.info("")
    logger.info(f"Generated {len(created)} IR files successfully.")

    if args.plot:
        verify_quality(dest)

    create_manifest(dest, created)

    logger.info(f"\nAll IRs in: {dest.resolve()}")


if __name__ == "__main__":
    main()
