# -*- coding: utf-8 -*-
"""Synthesize a heartbeat/power-driven rhythm bed for the ride.

The pulse's TEMPO literally tracks the rider's real heart rate second by
second (60/HR seconds between hits — when HR climbs into the red zone the
beat genuinely speeds up), and each hit's INTENSITY/timbre tracks power:
a soft low thump when he's coasting, a brighter layered blip stacked on
top when he's putting out real wattage. Retro/chiptune-ish on purpose
(short synthesized tones, not sampled drums) rather than trying to fake a
real drum kit.
"""
import json
import sys
from pathlib import Path

import numpy as np

sys.stdout.reconfigure(encoding="utf-8")

SR = 48000
SRC_DUR = 220.203
FADE_IN, FADE_OUT = 2.0, 3.0

win = json.loads(Path(r"C:\ai\AUTOMOVIE\window_data.json").read_text(encoding="utf-8"))["records"]
by_t = {}
for r in win:
    t = round(r["vt"])
    if 0 <= t <= int(SRC_DUR) + 1:
        by_t[t] = r

# dense per-second series with simple forward/back fill for gaps
hr = np.zeros(int(SRC_DUR) + 2)
pw = np.zeros(int(SRC_DUR) + 2)
last_hr, last_pw = 150.0, 150.0
for t in range(len(hr)):
    r = by_t.get(t)
    if r and r.get("hr"):
        last_hr = r["hr"]
    if r and r.get("pwr") is not None:
        last_pw = r["pwr"]
    hr[t] = last_hr
    pw[t] = last_pw

# smooth power a little so single-second spikes don't make the pulse jittery
kernel = np.ones(3) / 3
pw_smooth = np.convolve(pw, kernel, mode="same")


def hr_at(t):
    t = max(0, min(len(hr) - 1, t))
    i = int(t)
    frac = t - i
    j = min(i + 1, len(hr) - 1)
    return hr[i] * (1 - frac) + hr[j] * frac


def pw_at(t):
    t = max(0, min(len(pw_smooth) - 1, t))
    i = int(t)
    frac = t - i
    j = min(i + 1, len(pw_smooth) - 1)
    return pw_smooth[i] * (1 - frac) + pw_smooth[j] * frac


# --- walk the timeline placing beats at the instantaneous heart-rate interval
beats = []
t = 0.0
while t < SRC_DUR:
    beats.append(t)
    interval = 60.0 / max(60.0, hr_at(t))  # guard div-by-zero on bad samples
    t += interval
print(f"{len(beats)} beats over {SRC_DUR:.0f}s  "
      f"(avg interval {SRC_DUR/len(beats):.3f}s -> ~{60/(SRC_DUR/len(beats)):.0f} bpm avg)")

PW_LO, PW_HI = 80.0, 550.0          # power range mapped to intensity 0..1
# pentatonic-ish scale for the melodic overlay (Hz), low->high with power
SCALE = [220.00, 261.63, 329.63, 392.00, 440.00, 523.25, 659.25]


def env(n, attack, decay):
    e = np.ones(n)
    a = min(attack, n)
    d = min(decay, n)
    e[:a] = np.linspace(0, 1, a)
    e[-d:] *= np.linspace(1, 0, d)
    return e


def synth_kick(dur_s, freq_start, freq_end, amp):
    n = int(SR * dur_s)
    tt = np.linspace(0, dur_s, n, endpoint=False)
    freq = np.linspace(freq_start, freq_end, n)
    phase = 2 * np.pi * np.cumsum(freq) / SR
    tone = np.sin(phase)
    decay = np.exp(-tt / (dur_s * 0.32))
    return (tone * decay * amp).astype(np.float64)


def synth_blip(dur_s, freq, amp, wave="triangle"):
    n = int(SR * dur_s)
    tt = np.linspace(0, dur_s, n, endpoint=False)
    if wave == "triangle":
        tone = 2 * np.abs(2 * (tt * freq - np.floor(tt * freq + 0.5))) - 1
    else:
        tone = np.sign(np.sin(2 * np.pi * freq * tt))
    decay = np.exp(-tt / (dur_s * 0.22))
    fade = env(n, int(SR * 0.003), int(SR * 0.01))
    return (tone * decay * fade * amp).astype(np.float64)


out = np.zeros(int(SR * (SRC_DUR + 1.0)))

for b in beats:
    intensity = np.clip((pw_at(b) - PW_LO) / (PW_HI - PW_LO), 0.0, 1.0)

    # low thump: always present, gets punchier (higher amp, slightly higher
    # start-freq) as power rises
    kick_amp = 0.16 + 0.22 * intensity
    kick = synth_kick(0.11, 95 + 40 * intensity, 42, kick_amp)
    i0 = int(b * SR)
    i1 = i0 + len(kick)
    if i1 <= len(out):
        out[i0:i1] += kick

    # melodic blip layered on top only once power is meaningfully high,
    # pitch climbs the scale with intensity -> audible "tension rising"
    # during the real efforts, silent while coasting
    if intensity > 0.22:
        note = SCALE[min(len(SCALE) - 1, int(intensity * len(SCALE)))]
        blip = synth_blip(0.16, note, 0.05 + 0.09 * intensity, "triangle")
        i1b = i0 + len(blip)
        if i1b <= len(out):
            out[i0:i1b] += blip

# fades so the bed doesn't hard-cut at the edges
n_in = int(SR * FADE_IN)
n_out = int(SR * FADE_OUT)
out[:n_in] *= np.linspace(0, 1, n_in)
tail_start = int(SR * SRC_DUR) - n_out
if tail_start > 0:
    out[tail_start:tail_start + n_out] *= np.linspace(1, 0, n_out)
    out[tail_start + n_out:] = 0

peak = np.max(np.abs(out))
if peak > 0:
    out = out / peak * 0.9  # headroom, final mix level is set later in ffmpeg

pcm = (out * 32767).astype(np.int16)

import wave
dst = Path(r"C:\ai\AUTOMOVIE\build\pulse.wav")
dst.parent.mkdir(parents=True, exist_ok=True)
with wave.open(str(dst), "wb") as w:
    w.setnchannels(1)
    w.setsampwidth(2)
    w.setframerate(SR)
    w.writeframes(pcm.tobytes())
print(f"wrote {dst}  ({len(pcm)/SR:.1f}s, peak-normalized)")
