"""
Generate 3 demo beats for Tapecast — 808-style drum loops with kick, snare, hat,
bass, optional pad. Each loop is exactly 8 bars at its BPM and loops seamlessly.

Outputs to tapecast/client/public/beats/ as .mp3
"""
import math, struct, wave, os, subprocess
from pathlib import Path

SR = 44100  # sample rate
OUT_DIR = Path(__file__).parent.parent / "client" / "public" / "beats"
OUT_DIR.mkdir(parents=True, exist_ok=True)


# ---------- synthesis primitives ----------

def kick(dur=0.35, vol=1.0):
    """808-style sub kick: pitched sine drop with click attack."""
    n = int(SR * dur)
    out = [0.0] * n
    for i in range(n):
        t = i / SR
        # pitch envelope: 110 Hz → 45 Hz
        f = 110 * math.exp(-t * 18) + 45
        phase = 2 * math.pi * f * t
        # body
        body = math.sin(phase) * math.exp(-t * 5)
        # click
        click = (1 - i / 200) if i < 200 else 0
        click = click * math.sin(2 * math.pi * 2000 * t) * 0.3
        out[i] = (body + click) * vol
    return out


def snare(dur=0.18, vol=1.0):
    """Snare: noise + tonal layer."""
    import random
    random.seed(42)
    n = int(SR * dur)
    out = [0.0] * n
    for i in range(n):
        t = i / SR
        env = math.exp(-t * 22)
        noise = (random.random() * 2 - 1) * env * 0.7
        tone = math.sin(2 * math.pi * 200 * t) * env * 0.3
        out[i] = (noise + tone) * vol
    return out


def hat(dur=0.06, vol=0.5, open_hat=False):
    """Hi-hat: filtered noise with fast decay."""
    import random
    random.seed(7)
    n = int(SR * (dur if not open_hat else dur * 4))
    out = [0.0] * n
    decay = 60 if not open_hat else 12
    for i in range(n):
        t = i / SR
        env = math.exp(-t * decay)
        noise = (random.random() * 2 - 1) * env
        # crude HPF: emphasize high freq via differencing
        if i > 0:
            out[i] = (noise - out[i-1] * 0.3) * vol
        else:
            out[i] = noise * vol
    return out


def bass_note(freq, dur, vol=0.7):
    """Sub bass: sine + sub octave + slight saw."""
    n = int(SR * dur)
    out = [0.0] * n
    for i in range(n):
        t = i / SR
        env = (1 - math.exp(-t * 40)) * math.exp(-t * 2.5)
        s = math.sin(2 * math.pi * freq * t) * 0.7
        sub = math.sin(2 * math.pi * freq / 2 * t) * 0.3
        out[i] = (s + sub) * env * vol
    return out


def pad_chord(freqs, dur, vol=0.18):
    """Soft pad: stack of sines with slow attack."""
    n = int(SR * dur)
    out = [0.0] * n
    for i in range(n):
        t = i / SR
        env = min(1.0, t * 3) * math.exp(-t * 0.6)
        s = 0
        for f in freqs:
            s += math.sin(2 * math.pi * f * t)
            s += math.sin(2 * math.pi * f * 1.005 * t) * 0.5  # slight detune
        out[i] = s / len(freqs) * env * vol
    return out


# ---------- mixer ----------

def mix_at(track, sample, start_sec):
    """Add sample into track at start_sec (in samples)."""
    start = int(start_sec * SR)
    for i, v in enumerate(sample):
        if start + i < len(track):
            track[start + i] += v


def normalize(track, peak=0.9):
    mx = max(abs(v) for v in track) or 1
    g = peak / mx
    return [v * g for v in track]


def write_wav(track, path):
    with wave.open(str(path), 'wb') as w:
        w.setnchannels(1)
        w.setsampwidth(2)
        w.setframerate(SR)
        data = b''.join(struct.pack('<h', max(-32767, min(32767, int(v * 32767)))) for v in track)
        w.writeframes(data)


def wav_to_mp3(wav_path, mp3_path):
    subprocess.run([
        'ffmpeg', '-y', '-i', str(wav_path),
        '-codec:a', 'libmp3lame', '-b:a', '128k',
        str(mp3_path)
    ], check=True, capture_output=True)
    wav_path.unlink()


# ---------- note frequencies (equal temperament) ----------
def note(name):
    """e.g. 'C2' -> freq."""
    notes = {'C':0,'C#':1,'D':2,'D#':3,'E':4,'F':5,'F#':6,'G':7,'G#':8,'A':9,'A#':10,'B':11}
    pitch = name[:-1]
    octave = int(name[-1])
    midi = (octave + 1) * 12 + notes[pitch]
    return 440 * 2 ** ((midi - 69) / 12)


# ---------- patterns ----------

def make_trap(bpm=140, bars=8):
    """Midnight Trap — F minor. Kick on 1, snare on 3, 16th hats, sub bass."""
    spb = 60 / bpm  # sec per beat
    bar_len = spb * 4
    total = bar_len * bars
    n = int(total * SR)
    track = [0.0] * n

    for bar in range(bars):
        b = bar * bar_len
        # kicks: 1, 1.75, 2.5 (hip-hop swing)
        for off in [0, 1.75, 2.5]:
            mix_at(track, kick(vol=1.0), b + off * spb)
        # snare on 2 and 4
        mix_at(track, snare(vol=0.7), b + 1 * spb)
        mix_at(track, snare(vol=0.7), b + 3 * spb)
        # 16th hats with velocity variation
        for step in range(16):
            v = 0.35 if step % 2 == 0 else 0.22
            if step % 4 == 3:  # roll accent
                v = 0.45
            mix_at(track, hat(vol=v), b + step * spb / 4)
        # sub bass: F1 sustained pattern
        for off, dur, n_ in [(0, 1.5, 'F1'), (1.5, 0.5, 'F1'), (2, 1, 'G#1'), (3, 1, 'C2')]:
            mix_at(track, bass_note(note(n_), dur * spb, vol=0.55), b + off * spb)

    # pad: Fm chord
    for bar in range(0, bars, 2):
        mix_at(track, pad_chord([note('F3'), note('G#3'), note('C4')], bar_len * 2, vol=0.12), bar * bar_len)

    return normalize(track, 0.85)


def make_boom_bap(bpm=92, bars=8):
    """Boom Bap — A minor. Dusty kick/snare, swung hats, walking bass."""
    spb = 60 / bpm
    bar_len = spb * 4
    total = bar_len * bars
    n = int(total * SR)
    track = [0.0] * n

    for bar in range(bars):
        b = bar * bar_len
        # boom-bap kicks
        mix_at(track, kick(vol=0.95), b + 0 * spb)
        mix_at(track, kick(vol=0.7), b + 2.5 * spb)
        # snare on 2 and 4
        mix_at(track, snare(vol=0.75), b + 1 * spb)
        mix_at(track, snare(vol=0.75), b + 3 * spb)
        # 8th hats (swung — every 2nd slightly late)
        for step in range(8):
            swing = 0.08 if step % 2 == 1 else 0
            v = 0.32 if step % 2 == 0 else 0.20
            mix_at(track, hat(vol=v), b + (step / 2 + swing) * spb)
        # open hat on the &of 4
        mix_at(track, hat(vol=0.3, open_hat=True), b + 3.5 * spb)
        # bass walk
        for off, n_ in [(0, 'A1'), (1, 'A1'), (2, 'E2'), (2.75, 'G1'), (3, 'A1')]:
            mix_at(track, bass_note(note(n_), 0.5 * spb, vol=0.5), b + off * spb)

    # pad: Am chord
    for bar in range(0, bars, 4):
        mix_at(track, pad_chord([note('A3'), note('C4'), note('E4')], bar_len * 4, vol=0.10), bar * bar_len)

    return normalize(track, 0.85)


def make_lofi(bpm=85, bars=8):
    """Lo-Fi Bounce — C major. Soft kick, brushed snare, jazzy chord pad."""
    spb = 60 / bpm
    bar_len = spb * 4
    total = bar_len * bars
    n = int(total * SR)
    track = [0.0] * n

    for bar in range(bars):
        b = bar * bar_len
        # soft kicks
        mix_at(track, kick(vol=0.7), b + 0 * spb)
        mix_at(track, kick(vol=0.5), b + 2.25 * spb)
        # rim/snare on 2 and 4 (softer)
        mix_at(track, snare(vol=0.5), b + 1 * spb)
        mix_at(track, snare(vol=0.5), b + 3 * spb)
        # lazy 8th hats
        for step in range(8):
            v = 0.18 if step % 2 == 0 else 0.12
            mix_at(track, hat(vol=v), b + step * spb / 2)
        # bass walk in C
        for off, n_ in [(0, 'C2'), (1.5, 'C2'), (2, 'A1'), (3, 'F1')]:
            mix_at(track, bass_note(note(n_), 0.75 * spb, vol=0.45), b + off * spb)

    # jazzy pad: Cmaj7 → Am7 → Fmaj7 → G7 — change every 2 bars
    chords = [
        [note('C3'), note('E3'), note('G3'), note('B3')],  # Cmaj7
        [note('A2'), note('C3'), note('E3'), note('G3')],  # Am7
        [note('F2'), note('A2'), note('C3'), note('E3')],  # Fmaj7
        [note('G2'), note('B2'), note('D3'), note('F3')],  # G7
    ]
    for i in range(4):
        mix_at(track, pad_chord(chords[i], bar_len * 2, vol=0.16), i * 2 * bar_len)

    return normalize(track, 0.8)


# ---------- run ----------

def build(name, track):
    wav = OUT_DIR / f"{name}.wav"
    mp3 = OUT_DIR / f"{name}.mp3"
    write_wav(track, wav)
    wav_to_mp3(wav, mp3)
    print(f"✓ {mp3}  ({mp3.stat().st_size // 1024} KB)")


if __name__ == '__main__':
    print("Generating beats...")
    build("midnight-trap", make_trap(140, 8))
    build("boom-bap", make_boom_bap(92, 8))
    build("lofi-bounce", make_lofi(85, 8))
    print("Done.")
