# Copyright (C) 2026 Nathan Cerisara <https://github.com/nath54/nasong>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.


"""Percussion and drum instrument definitions.

This module provides factory functions for synthesized drum kit components,
including kick drums, snares, hi-hats, and cymbals. These use a combination
of frequency-modulated oscillators and filtered noise.
"""

#
### Import Modules. ###
#
import math

#
import numpy as np
from numpy.typing import NDArray

#
import nasong.core.all_values as lv


#
def KickDrum(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 0.5,
    velocity: float = 1.0,
) -> lv.Value:
    """Creates a basic synthesized kick drum sound.

    Synthesized using a pitch-modulated sine wave (dropping from 150Hz) and
    a short impulse "click" for impact.

    Args:
        time (lv.Value): The global time value.
        frequency (float): Unused (standard signature).
        start_time (float): The time the drum is struck.
        duration (float): Unused (standard signature).
        velocity (float): Overall volume scaling. Defaults to 1.0.

    Returns:
        lv.Value: The audio value graph for the kick drum.
    """

    #
    ### The original had a 0.5s hard gate. We use ADSR2 for this. ###
    #
    gate_env: lv.Value = lv.ADSR2(time, start_time, 0.5, 0.001, 0.001, 1.0, 0.001)

    #
    ### Amplitude envelope: exp(-relative_time * 8) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, 8.0)

    #
    ### Pitch envelope: 150 * exp(-relative_time * 20) ###
    #
    pitch_env: lv.Value = lv.Product(
        lv.c(150), lv.ExponentialDecay(time, start_time, 20.0)
    )

    #
    ### Tone: sin(2 * pi * pitch * relative_time) ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    #
    tone: lv.Value = lv.Sin(
        relative_time, frequency=lv.Product(pitch_env, lv.c(2 * math.pi))
    )

    #
    ### Click: 0.3 * exp(-relative_time * 50) ###
    #
    click: lv.Value = lv.Product(lv.c(0.3), lv.ExponentialDecay(time, start_time, 50.0))

    #
    ### Signal = (Tone + Click) ###
    #
    signal: lv.Value = lv.Sum(tone, click)

    #
    ### Final = 0.6 * velocity * Gate * AmpEnv * Signal ###
    #
    return lv.Product(lv.c(0.6 * velocity), gate_env, amp_env, signal)


#
def KickDrum2(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 0.5,
    velocity: float = 1.0,
) -> lv.Value:
    """Creates an alternate, punchier synthesized kick drum.

    Features a steeper pitch drop and a noise-based "click" at the start for
    added transient definition.

    Args:
        time (lv.Value): The global time value.
        start_time (float): The strike time in seconds.

    Returns:
        lv.Value: The audio value graph for the alternate kick drum.
    """

    #
    ### Gate: 0.5s hard gate. ###
    #
    gate_env: lv.Value = lv.ADSR2(time, start_time, 0.5, 0.001, 0.001, 1.0, 0.001)

    #
    ### Amplitude envelope: exp(-relative_t * 15) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, 15.0)

    #
    ### Pitch envelope: 60 + 100 * exp(-relative_t * 50) ###
    #
    pitch_decay: lv.Value = lv.Product(
        lv.c(100), lv.ExponentialDecay(time, start_time, 50.0)
    )
    #
    pitch_env: lv.Value = lv.Sum(lv.c(60), pitch_decay)

    #
    ### Tone: sin(2 * pi * pitch * relative_time) ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    #
    tone: lv.Value = lv.Sin(
        relative_time, frequency=lv.Product(pitch_env, lv.c(2 * math.pi))
    )

    #
    ### Click: 0.5 * exp(-relative_t * 100) * (noise) ###
    #
    click_env: lv.Value = lv.Product(
        lv.c(0.5), lv.ExponentialDecay(time, start_time, 100.0)
    )
    #
    click_noise: lv.Value = lv.WhiteNoise(seed=4567)  # Seed from original
    #
    click: lv.Value = lv.Product(click_env, click_noise)

    #
    ### Signal = (Tone + Click) ###
    #
    signal: lv.Value = lv.Sum(tone, click)

    #
    ### Final = 0.6 * velocity * Gate * AmpEnv * Signal ###
    #
    return lv.Product(lv.c(0.6 * velocity), gate_env, amp_env, signal)


#
def Snare(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 0.3,
    velocity: float = 1.0,
) -> lv.Value:
    """Creates a basic synthesized snare drum sound.

    Combines a fixed 200Hz sine "body" tone with a broad-spectrum white noise
    "snap" or "wire" component.

    Args:
        time (lv.Value): The global time value.
        frequency (float): Unused (standard signature).
        start_time (float): The time the snare is struck.
        duration (float): Unused (standard signature).
        velocity (float): Overall volume scaling. Defaults to 1.0.

    Returns:
        lv.Value: The audio value graph for the snare.
    """

    #
    ### Gate: 0.3s hard gate. ###
    #
    gate_env: lv.Value = lv.ADSR2(time, start_time, 0.3, 0.001, 0.001, 1.0, 0.001)

    #
    ### Amplitude envelope: exp(-relative_time * 15) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, 15.0)

    #
    ### Tone: 0.4 * sin(2 * pi * 200 * relative_time) ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    #
    tone: lv.Value = lv.Sin(
        relative_time, frequency=lv.c(200 * 2 * math.pi), amplitude=lv.c(0.4)
    )

    #
    ### Noise: 0.8 * (noise) ###
    #
    noise: lv.Value = lv.Product(
        lv.WhiteNoise(seed=9973),  # Seed from original
        lv.c(0.8),
    )

    #
    ### Signal = (Tone + Noise) ###
    #
    signal: lv.Value = lv.Sum(tone, noise)

    #
    ### Final = 0.4 * velocity * Gate * AmpEnv * Signal ###
    #
    return lv.Product(lv.c(0.4 * velocity), gate_env, amp_env, signal)


#
def SnareDrum(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 0.2,
    velocity: float = 1.0,
) -> lv.Value:
    """Creates a tighter, shorter synthesized snare drum.

    Args:
        time (lv.Value): The global time value.
        start_time (float): The strike time in seconds.

    Returns:
        lv.Value: The audio value graph for the snare drum.
    """

    #
    ### Gate: 0.2s hard gate. ###
    #
    gate_env: lv.Value = lv.ADSR2(time, start_time, 0.2, 0.001, 0.001, 1.0, 0.001)

    #
    ### Amplitude envelope: exp(-relative_t * 30) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, 30.0)

    #
    ### Tone: 0.3 * sin(2 * pi * 200 * relative_time) ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    #
    tone: lv.Value = lv.Sin(
        relative_time, frequency=lv.c(200 * 2 * math.pi), amplitude=lv.c(0.3)
    )

    #
    ### Noise: 0.7 * (noise) ###
    #
    noise: lv.Value = lv.Product(
        lv.WhiteNoise(seed=1337),  # Seed from original
        lv.c(0.05),
    )

    #
    ### Signal = (Tone + Noise) ###
    #
    signal: lv.Value = lv.Sum(tone, noise)

    #
    ### Final = 0.4 * velocity * Gate * AmpEnv * Signal ###
    #
    return lv.Product(lv.c(0.4 * velocity), gate_env, amp_env, signal)


#
def HiHat(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 0.1,
    velocity: float = 1.0,
    open: bool = False,
) -> lv.Value:
    """Creates a synthesized hi-hat sound (closed or open).

    Synthesized using multiple high-frequency sine oscillators and a small
    amount of white noise, with decay rates dictated by the `open` state.

    Args:
        time (lv.Value): The global time value.
        start_time (float): The strike time in seconds.
        open (bool, optional): Whether to play an open hi-hat (longer decay).
            Defaults to False.

    Returns:
        lv.Value: The audio value graph for the hi-hat.
    """

    #
    duration_hat: float = 0.4 if open else 0.08
    decay_rate: float = 8.0 if open else 50.0

    #
    ### Gate: hard gate at duration. ###
    #
    gate_env: lv.Value = lv.ADSR2(
        time, start_time, duration_hat, 0.001, 0.001, 1.0, 0.001
    )

    #
    ### Amplitude envelope: exp(-relative_t * decay_rate) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, decay_rate)

    #
    ### Tone: Sum of high-frequency sines ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    pi2: float = 2 * math.pi
    cymbal_freqs: list[float] = [8000.0, 9000.0, 10000.0, 11000.0, 12000.0]
    #
    tone_list: list[lv.Value] = [
        lv.Sin(relative_time, lv.c(f * pi2)) for f in cymbal_freqs
    ]
    #
    tone: lv.Value = lv.Product(lv.Sum(tone_list), lv.c(1.0 / len(cymbal_freqs)))

    #
    ### Noise: 0.3 * (noise) ###
    #
    noise: lv.Value = lv.Product(
        lv.WhiteNoise(seed=2222),  # Seed from original
        lv.c(0.05),
    )

    #
    ### Signal = (Tone + Noise) ###
    #
    signal: lv.Value = lv.Sum(tone, noise)

    #
    ### Final = 0.15 * velocity * Gate * AmpEnv * Signal ###
    #
    return lv.Product(lv.c(0.15 * velocity), gate_env, amp_env, signal)


#
def CrashCymbal(
    time: lv.Value,
    frequency: float,
    start_time: float,
    duration: float = 2.0,
    velocity: float = 1.0,
) -> lv.Value:
    """Creates a synthesized crash cymbal sound.

    Uses high-frequency additive synthesis with random phases to simulate
    the complex, non-harmonic ring of a large-diameter cymbal.

    Args:
        time (lv.Value): The global time value.
        start_time (float): The strike time in seconds.

    Returns:
        lv.Value: The audio value graph for the crash cymbal.
    """

    #
    ### Gate: 2.0s hard gate. ###
    #
    gate_env: lv.Value = lv.ADSR2(time, start_time, 2.0, 0.001, 0.001, 1.0, 0.001)

    #
    ### Amplitude envelope: exp(-relative_t * 2) ###
    #
    amp_env: lv.Value = lv.ExponentialDecay(time, start_time, 2.0)

    #
    ### Tone: Sum of high-frequency sines with random phase ###
    #
    relative_time: lv.Value = lv.BasicScaling(time, lv.c(1), lv.c(-start_time))
    pi2: float = 2 * math.pi
    #
    cymbal_freqs: list[float] = [7000.0, 8500.0, 10000.0, 11500.0, 13000.0, 14500.0]
    #
    ### Pre-calculate static random phases, as in original  ###
    #
    static_phases: NDArray[np.float32] = np.random.uniform(
        low=0.0, high=pi2, size=len(cymbal_freqs)
    ).astype(np.float32)
    #
    tone_list: list[lv.Value] = [
        lv.Sin(relative_time, lv.c(f * pi2), delta=lv.c(float(phase)))
        for f, phase in zip(cymbal_freqs, static_phases)
    ]
    #
    tone: lv.Value = lv.Product(lv.Sum(tone_list), lv.c(1.0 / len(cymbal_freqs)))

    #
    ### Final = 0.25 * velocity * Gate * AmpEnv * Tone ###
    #
    return lv.Product(lv.c(0.25 * velocity), gate_env, amp_env, tone)
