"""
europa.synthesis
~~~~~~~~~~~~~~~~
Audio signal synthesis.

Generators:
    sine       - Sine wave oscillator
    square     - Square wave
    sawtooth   - Sawtooth wave
    triangle   - Triangle wave
    noise      - White / pink / brown noise
    silence    - Silent audio

Envelopes:
    adsr       - ADSR envelope generator
    apply_adsr - Apply ADSR to an Audio object

Utilities:
    mix        - Mix multiple Audio objects together
    chord      - Generate a chord from multiple frequencies
"""

import numpy as np
from ..core.audio import Audio


# ── Oscillators ───────────────────────────────────────────────────────────────

def sine(
    freq: float,
    duration: float,
    sr: int = 44100,
    amplitude: float = 0.5,
    phase: float = 0.0,
) -> Audio:
    """
    Generate a sine wave.

    Args:
        freq      : Frequency in Hz.
        duration  : Duration in seconds.
        sr        : Sample rate.
        amplitude : Peak amplitude (0.0–1.0).
        phase     : Initial phase offset in radians.

    Returns:
        Audio: Sine wave audio.

    Examples:
        >>> tone = europa.synthesis.sine(440, 1.0)  # A4, 1 second
    """
    t = np.linspace(0, duration, int(sr * duration), endpoint=False)
    data = amplitude * np.sin(2 * np.pi * freq * t + phase)
    return Audio(data=data.astype(np.float32), sr=sr)


def square(
    freq: float,
    duration: float,
    sr: int = 44100,
    amplitude: float = 0.5,
    duty: float = 0.5,
) -> Audio:
    """
    Generate a square wave.

    Args:
        freq      : Frequency in Hz.
        duration  : Duration in seconds.
        sr        : Sample rate.
        amplitude : Peak amplitude.
        duty      : Duty cycle (0.0–1.0). 0.5 = symmetric square.

    Returns:
        Audio: Square wave.
    """
    t = np.linspace(0, duration, int(sr * duration), endpoint=False)
    data = amplitude * np.sign(np.sin(2 * np.pi * freq * t))
    data = np.where(np.sin(2 * np.pi * freq * t) >= 0, amplitude, -amplitude)
    return Audio(data=data.astype(np.float32), sr=sr)


def sawtooth(
    freq: float,
    duration: float,
    sr: int = 44100,
    amplitude: float = 0.5,
) -> Audio:
    """
    Generate a sawtooth wave.

    Returns:
        Audio: Sawtooth wave.
    """
    t = np.linspace(0, duration, int(sr * duration), endpoint=False)
    data = amplitude * 2 * (t * freq - np.floor(t * freq + 0.5))
    return Audio(data=data.astype(np.float32), sr=sr)


def triangle(
    freq: float,
    duration: float,
    sr: int = 44100,
    amplitude: float = 0.5,
) -> Audio:
    """
    Generate a triangle wave.

    Returns:
        Audio: Triangle wave.
    """
    t = np.linspace(0, duration, int(sr * duration), endpoint=False)
    saw = 2 * (t * freq - np.floor(t * freq + 0.5))
    data = amplitude * (2 * np.abs(saw) - 1)
    return Audio(data=data.astype(np.float32), sr=sr)


def noise(
    duration: float,
    sr: int = 44100,
    color: str = "white",
    amplitude: float = 0.5,
) -> Audio:
    """
    Generate noise.

    Args:
        duration  : Duration in seconds.
        sr        : Sample rate.
        color     : Noise color: 'white', 'pink', 'brown'.
        amplitude : Peak amplitude.

    Returns:
        Audio: Noise signal.
    """
    n = int(sr * duration)
    white = np.random.randn(n).astype(np.float32)

    if color == "white":
        data = white
    elif color == "pink":
        # Voss-McCartney algorithm approximation
        data = _pink_noise(n)
    elif color == "brown" or color == "red":
        data = np.cumsum(white)
        data /= (np.max(np.abs(data)) + 1e-9)
    else:
        raise ValueError(f"Unknown noise color: {color!r}. Use 'white', 'pink', or 'brown'.")

    data = amplitude * data / (np.max(np.abs(data)) + 1e-9)
    return Audio(data=data.astype(np.float32), sr=sr)


def silence(duration: float, sr: int = 44100) -> Audio:
    """
    Generate silence.

    Returns:
        Audio: Silent audio.
    """
    data = np.zeros(int(sr * duration), dtype=np.float32)
    return Audio(data=data, sr=sr)


# ── Envelopes ─────────────────────────────────────────────────────────────────

def adsr(
    total_samples: int,
    sr: int,
    attack: float = 0.01,
    decay: float = 0.1,
    sustain: float = 0.7,
    release: float = 0.2,
) -> np.ndarray:
    """
    Generate an ADSR envelope.

    Args:
        total_samples : Total number of samples.
        sr            : Sample rate.
        attack        : Attack time in seconds.
        decay         : Decay time in seconds.
        sustain       : Sustain level (0.0–1.0).
        release       : Release time in seconds.

    Returns:
        np.ndarray: Envelope of shape (total_samples,).
    """
    a_s = min(int(attack  * sr), total_samples)
    d_s = min(int(decay   * sr), total_samples - a_s)
    r_s = min(int(release * sr), total_samples)
    s_s = max(total_samples - a_s - d_s - r_s, 0)

    env = np.concatenate([
        np.linspace(0.0, 1.0, a_s),
        np.linspace(1.0, sustain, d_s),
        np.full(s_s, sustain),
        np.linspace(sustain, 0.0, r_s),
    ])
    return env[:total_samples].astype(np.float32)


def apply_adsr(
    audio: Audio,
    attack: float = 0.01,
    decay: float = 0.1,
    sustain: float = 0.7,
    release: float = 0.2,
) -> Audio:
    """
    Apply an ADSR envelope to an Audio object.

    Returns:
        Audio: Audio with ADSR envelope applied.
    """
    env = adsr(audio.samples, audio.sr, attack, decay, sustain, release)
    data = audio.data * env
    return Audio(data=data.astype(np.float32), sr=audio.sr)


# ── Mixing ────────────────────────────────────────────────────────────────────

def mix(*audios: Audio, weights=None) -> Audio:
    """
    Mix multiple Audio objects together.

    Args:
        *audios : Audio objects to mix. Must share the same sample rate.
        weights : Optional list of weights. None = equal mix.

    Returns:
        Audio: Mixed audio (length = longest input).

    Examples:
        >>> mixed = europa.synthesis.mix(bass, drums, melody)
        >>> mixed = europa.synthesis.mix(a, b, weights=[0.8, 0.3])
    """
    if not audios:
        raise ValueError("Provide at least one Audio object to mix.")

    sr = audios[0].sr
    if not all(a.sr == sr for a in audios):
        raise ValueError("All Audio objects must have the same sample rate.")

    max_len = max(a.samples for a in audios)
    weights = weights or [1.0 / len(audios)] * len(audios)

    result = np.zeros(max_len, dtype=np.float32)
    for audio, w in zip(audios, weights):
        data = audio.data if audio.is_mono else np.mean(audio.data, axis=0)
        result[:audio.samples] += w * data

    return Audio(data=result, sr=sr)


def chord(
    freqs,
    duration: float,
    sr: int = 44100,
    waveform: str = "sine",
    amplitude: float = 0.3,
) -> Audio:
    """
    Generate a chord from a list of frequencies.

    Args:
        freqs     : List of frequencies in Hz.
        duration  : Duration in seconds.
        sr        : Sample rate.
        waveform  : Oscillator type: 'sine', 'square', 'sawtooth', 'triangle'.
        amplitude : Per-note amplitude.

    Returns:
        Audio: Chord audio.

    Examples:
        >>> c_major = europa.synthesis.chord([261.63, 329.63, 392.0], 2.0)
    """
    generators = {
        "sine":     sine,
        "square":   square,
        "sawtooth": sawtooth,
        "triangle": triangle,
    }
    gen = generators.get(waveform)
    if gen is None:
        raise ValueError(f"Unknown waveform: {waveform!r}")

    notes = [gen(f, duration, sr=sr, amplitude=amplitude) for f in freqs]
    return mix(*notes, weights=[1.0] * len(notes))


# ── Internal helpers ──────────────────────────────────────────────────────────

def _pink_noise(n: int) -> np.ndarray:
    """Generate approximate pink noise using the Voss-McCartney method."""
    n_rows = 16
    array = np.random.randn(n_rows, n // n_rows + 1)
    cumsum = np.cumsum(array, axis=1)
    noise = np.sum(cumsum[:, :n // n_rows], axis=0)
    # Tile to fill n samples
    result = np.tile(noise, n // len(noise) + 1)[:n]
    return result.astype(np.float32)
