# -*- coding: utf-8 -*-
"""Heart-rate / power-driven rhythm bed.

Beat tempo follows the rider's actual HR second by second; per-hit intensity
and timbre follow power. v2 audibility fix over the first prototype: the
original kick lived at 42-95 Hz (subwoofer territory — inaudible on laptop
or phone speakers), so each hit now stacks three layers:

  sub   40-90 Hz   weight, for systems that can reproduce it
  body  ~190 Hz    the part you actually hear on small speakers
  click ~1.8 kHz   3 ms transient so the beat cuts through wind noise

The melodic blip layer (power-gated pentatonic) is unchanged.
"""
import wave
from pathlib import Path

import numpy as np

SR = 48000
SCALE = [220.00, 261.63, 329.63, 392.00, 440.00, 523.25, 659.25]


def _kick(dur_s, f0, f1, amp):
    n = int(SR * dur_s)
    tt = np.linspace(0, dur_s, n, endpoint=False)
    freq = np.linspace(f0, f1, n)
    phase = 2 * np.pi * np.cumsum(freq) / SR
    return np.sin(phase) * np.exp(-tt / (dur_s * 0.32)) * amp


def _tone(dur_s, freq, amp, decay_frac=0.22):
    n = int(SR * dur_s)
    tt = np.linspace(0, dur_s, n, endpoint=False)
    tone = 2 * np.abs(2 * (tt * freq - np.floor(tt * freq + 0.5))) - 1
    fade_in = min(n, int(SR * 0.002))
    env = np.exp(-tt / (dur_s * decay_frac))
    env[:fade_in] *= np.linspace(0, 1, fade_in)
    return tone * env * amp


def _click(amp):
    n = int(SR * 0.003)
    tt = np.linspace(0, 0.003, n, endpoint=False)
    return np.sin(2 * np.pi * 1800 * tt) * np.linspace(1, 0, n) * amp


def generate(win: list, duration: float, out_path: str,
             pw_lo=80.0, pw_hi=550.0, fade_in=2.0, fade_out=3.0, log=print):
    hr = np.zeros(int(duration) + 2)
    pw = np.zeros(int(duration) + 2)
    by_t = {int(round(r["vt"])): r for r in win if r.get("vt") is not None}
    last_hr, last_pw = 120.0, 120.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
    pw = np.convolve(pw, np.ones(3) / 3, mode="same")

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

    beats, t = [], 0.0
    while t < duration:
        beats.append(t)
        t += 60.0 / max(60.0, at(hr, t))
    log(f"  {len(beats)} 拍 / {duration:.0f}s (~{len(beats)/duration*60:.0f} bpm 平均)")

    out = np.zeros(int(SR * (duration + 1.0)))
    for b in beats:
        inten = float(np.clip((at(pw, b) - pw_lo) / (pw_hi - pw_lo), 0, 1))
        i0 = int(b * SR)
        layers = [
            _kick(0.11, 95 + 40 * inten, 42, 0.13 + 0.18 * inten),      # sub
            _kick(0.07, 240 + 60 * inten, 150, 0.11 + 0.16 * inten),    # body
            _click(0.05 + 0.07 * inten),                                # click
        ]
        if inten > 0.22:
            note = SCALE[min(len(SCALE) - 1, int(inten * len(SCALE)))]
            layers.append(_tone(0.16, note, 0.05 + 0.09 * inten))
        for sig in layers:
            i1 = i0 + len(sig)
            if i1 <= len(out):
                out[i0:i1] += sig

    n_in, n_out = int(SR * fade_in), int(SR * fade_out)
    out[:n_in] *= np.linspace(0, 1, n_in)
    tail = int(SR * duration) - n_out
    if tail > 0:
        out[tail:tail + n_out] *= np.linspace(1, 0, n_out)
        out[tail + n_out:] = 0

    peak = np.max(np.abs(out))
    if peak > 0:
        out = out / peak * 0.9
    pcm = (out * 32767).astype(np.int16)
    dst = Path(out_path)
    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())
    return str(dst)
