#!/usr/bin/env python3
"""Generate deterministic visuals for the numbered formulas in Chapter 2."""

from __future__ import annotations

from pathlib import Path

import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

ROOT = Path(__file__).resolve().parents[3]
SVG = ROOT / "assets/figures/svg"
SVG.mkdir(parents=True, exist_ok=True)

RED = "#7F1D1D"
TEAL = "#0F766E"
BLUE = "#1D4ED8"
PURPLE = "#7C3AED"
GOLD = "#B45309"
GRAY = "#6B7280"
BLACK = "#111827"

plt.rcParams.update(
    {
        "font.family": "DejaVu Sans",
        "font.size": 9,
        "axes.spines.top": False,
        "axes.spines.right": False,
        "axes.titleweight": "bold",
        "svg.fonttype": "none",
    }
)


def save(fig: plt.Figure, filename: str) -> None:
    fig.savefig(SVG / filename, bbox_inches="tight")
    plt.close(fig)


def zero_line(axis: plt.Axes) -> None:
    axis.axhline(0.0, color=GRAY, linewidth=0.6)


# Equation 2.1: LC resonance falls as capacitance rises.
inductance = 1e-3
capacitance_pf = np.linspace(50.0, 500.0, 900)
capacitance_f = capacitance_pf * 1e-12
lc_frequency_khz = 1.0 / (2 * np.pi * np.sqrt(inductance * capacitance_f)) / 1000
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(capacitance_pf, lc_frequency_khz, color=BLUE, linewidth=2)
for c_pf in [100.0, 400.0]:
    f_khz = 1.0 / (2 * np.pi * np.sqrt(inductance * c_pf * 1e-12)) / 1000
    axis.scatter([c_pf], [f_khz], color=RED, zorder=3)
    axis.annotate(f"{c_pf:.0f} pF, {f_khz:.1f} kHz", (c_pf, f_khz), xytext=(8, 6), textcoords="offset points")
axis.set(xlabel="capacitance C (pF)", ylabel="resonant frequency (kHz)", title="Equation 2.1: L = 1 mH; four times C gives half the frequency")
save(fig, "eq-2-1-lc-frequency.svg")

# Equation 2.3: difference and sum move differently as one oscillator moves.
fixed_khz = 500.0
variable_khz = np.linspace(498.0, 502.0, 900)
difference_khz = np.abs(fixed_khz - variable_khz)
sum_khz = fixed_khz + variable_khz
fig, axes = plt.subplots(2, 1, figsize=(7.2, 4.8), sharex=True, constrained_layout=True)
axes[0].plot(variable_khz, difference_khz, color=RED, linewidth=2)
axes[0].set(ylabel="difference (kHz)", title="Absolute difference reaches zero when oscillators match")
axes[1].plot(variable_khz, sum_khz, color=BLUE, linewidth=2)
axes[1].set(xlabel="variable oscillator f2 (kHz); fixed f1 = 500 kHz", ylabel="sum (kHz)", title="Sum remains near 1000 kHz")
fig.suptitle("Equation 2.3: |f1 − f2| and f1 + f2", fontweight="bold")
save(fig, "eq-2-3-sum-difference.svg")

# Equation 2.4: linear addition beats but keeps only the input spectrum lines.
f1 = 10.0
f2 = 11.5
t_beats = np.linspace(0.0, 2.0, 5000, endpoint=False)
linear_sum = np.cos(2 * np.pi * f1 * t_beats) + np.cos(2 * np.pi * f2 * t_beats)
envelope = 2 * np.cos(np.pi * (f2 - f1) * t_beats)
fig, axes = plt.subplots(2, 1, figsize=(7.2, 5.0), constrained_layout=True)
axes[0].plot(t_beats, linear_sum, color=BLACK, linewidth=0.8, label="linear sum")
axes[0].plot(t_beats, envelope, color=RED, linewidth=1.4, linestyle="--", label="± envelope")
axes[0].plot(t_beats, -envelope, color=RED, linewidth=1.4, linestyle="--")
axes[0].set(xlabel="time (s)", ylabel="amplitude", xlim=(0, 2), title="The waveform has a slow envelope")
axes[0].legend(frameon=False)
markerline, stemlines, _ = axes[1].stem([f1, f2], [1.0, 1.0], basefmt=" ")
plt.setp(markerline, color=PURPLE, markersize=6)
plt.setp(stemlines, color=PURPLE, linewidth=1.6)
axes[1].set(xlabel="frequency (scaled Hz)", ylabel="relative amplitude", xticks=[f1, f2], xlim=(0, 15), ylim=(0, 1.1), title="The linear spectrum still has only f1 and f2")
fig.suptitle("Equation 2.4: f1 = 10, f2 = 11.5; beating is not a new 1.5 line", fontweight="bold")
save(fig, "eq-2-4-linear-beating.svg")

# Equation 2.5: map cent offset to frequency around A4.
cents = np.linspace(-100.0, 100.0, 800)
center_hz = 440.0
mapped_hz = center_hz * np.power(2.0, cents / 1200.0)
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(cents, mapped_hz, color=RED, linewidth=2)
for c in [-50.0, 0.0, 50.0]:
    f = center_hz * 2 ** (c / 1200)
    axis.scatter([c], [f], color=BLUE, zorder=3)
    axis.annotate(f"{c:+.0f} cents = {f:.2f} Hz", (c, f), xytext=(5, 7), textcoords="offset points")
axis.set(xlabel="cent offset c", ylabel="frequency f(t) (Hz)", title="Equation 2.5: fc = 440 Hz; equal cents form frequency ratios")
save(fig, "eq-2-5-cents-frequency.svg")

# Equation 2.6: sinusoidal vibrato in cent space.
t_vibrato = np.linspace(0.0, 1.0, 1600)
depth_cents = 25.0
vibrato_rate = 5.5
cent_trajectory = depth_cents * np.sin(2 * np.pi * vibrato_rate * t_vibrato)
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(t_vibrato, cent_trajectory, color=TEAL, linewidth=1.8)
axis.axhline(depth_cents, color=GRAY, linestyle="--", linewidth=0.8, label="±D = ±25 cents")
axis.axhline(-depth_cents, color=GRAY, linestyle="--", linewidth=0.8)
zero_line(axis)
axis.set(xlabel="time t (s)", ylabel="c(t) (cents)", ylim=(-32, 32), title="Equation 2.6: D = 25 cents, fv = 5.5 Hz")
axis.legend(frameon=False)
save(fig, "eq-2-6-vibrato-cents.svg")

# Equation 2.7: LC frequency ratio as capacitance ratio changes.
capacitance_ratio = np.linspace(0.25, 4.0, 900)
frequency_ratio = np.sqrt(1.0 / capacitance_ratio)
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(capacitance_ratio, frequency_ratio, color=BLUE, linewidth=2)
for ratio in [1.0, 4.0]:
    value = np.sqrt(1 / ratio)
    axis.scatter([ratio], [value], color=RED, zorder=3)
    axis.annotate(f"C2/C1 = {ratio:g}, f2/f1 = {value:g}", (ratio, value), xytext=(7, 7), textcoords="offset points")
axis.set(xlabel="capacitance ratio C2/C1", ylabel="frequency ratio f2/f1", title="Equation 2.7: fixed L; frequency scales as 1/√C")
save(fig, "eq-2-7-lc-ratio.svg")

# Equation 2.8: cents expressed directly as a frequency ratio.
cents_wide = np.linspace(-1200.0, 1200.0, 1200)
ratio = np.power(2.0, cents_wide / 1200.0)
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(cents_wide, ratio, color=PURPLE, linewidth=2)
for c, r in [(-1200, 0.5), (0, 1.0), (1200, 2.0)]:
    axis.scatter([c], [r], color=RED, zorder=3)
    axis.annotate(f"{c:+d} cents → {r:g}×", (c, r), xytext=(5, 7), textcoords="offset points")
axis.set(xlabel="cent displacement c", ylabel="frequency ratio f/fc", title="Equation 2.8: 1200 cents doubles frequency")
save(fig, "eq-2-8-cents-ratio.svg")

# Equation 2.9: logarithmic and linear interpolation do not share a midpoint.
u = np.linspace(0.0, 1.0, 800)
start_hz = 220.0
end_hz = 880.0
log_path = start_hz * np.power(end_hz / start_hz, u)
linear_path = start_hz + (end_hz - start_hz) * u
fig, axis = plt.subplots(figsize=(7.2, 3.2), constrained_layout=True)
axis.plot(u, log_path, color=RED, linewidth=2, label="log-frequency path")
axis.plot(u, linear_path, color=GRAY, linewidth=1.4, linestyle="--", label="linear-hertz path")
axis.scatter([0.5, 0.5], [440, 550], color=[RED, GRAY], zorder=3)
axis.annotate("440 Hz", (0.5, 440), xytext=(-35, -18), textcoords="offset points")
axis.annotate("550 Hz", (0.5, 550), xytext=(8, 8), textcoords="offset points")
axis.set(xlabel="normalized progress u", ylabel="frequency (Hz)", title="Equation 2.9: 220 → 880 Hz")
axis.legend(frameon=False)
save(fig, "eq-2-9-log-interpolation.svg")

# Equation 2.10: frequency controls phase increment; accumulated phase controls the waveform.
sample_rate = 48_000
samples = 1_920
time = np.arange(samples) / sample_rate
progress = np.linspace(0.0, 1.0, samples)
frequency_path = 220.0 * np.power(880.0 / 220.0, progress)
increment = 2 * np.pi * frequency_path / sample_rate
phase_path = np.zeros(samples)
phase_path[1:] = np.cumsum(increment[:-1])
waveform = np.sin(phase_path)
assert frequency_path[0] == 220.0 and frequency_path[-1] == 880.0
assert phase_path[0] == 0.0
fig, axes = plt.subplots(3, 1, figsize=(7.2, 6.0), sharex=True, constrained_layout=True)
axes[0].plot(time * 1000, frequency_path, color=BLUE, linewidth=1.8)
axes[0].set(ylabel="f[n] (Hz)", title="Input frequency rises from 220 to 880 Hz")
axes[1].plot(time * 1000, increment, color=TEAL, linewidth=1.8)
axes[1].set(ylabel="phase step (rad)", title="Each sample gets a new 2πf[n]/Fs increment")
axes[2].plot(time * 1000, waveform, color=RED, linewidth=0.9)
zero_line(axes[2])
axes[2].set(xlabel="time (ms)", ylabel="x[n]", title="The sine reads the accumulated phase")
fig.suptitle("Equation 2.10: Fs = 48 kHz, N = 1920, f[0] = 220 Hz, f[1919] = 880 Hz", fontweight="bold")
save(fig, "eq-2-10-phase-accumulation.svg")

print("Generated Chapter 2 formula visuals.")
