// Synthetic audio with known ground truth, so tempo and segmentation can be
// tested against an exact answer instead of "sounds about right". Real music
// goes through the click track and the battery; this catches regressions in CI
// speed and without a GPU.

/** Minimal stand-in for AudioBuffer — the analysis code only needs this surface. */
export class MockAudioBuffer {
    constructor(channels, length, sampleRate) {
        this.numberOfChannels = channels;
        this.length = length;
        this.sampleRate = sampleRate;
        this.duration = length / sampleRate;
        this._data = Array.from({ length: channels }, () => new Float32Array(length));
    }
    getChannelData(i) { return this._data[i]; }
}

function addKick(data, sampleRate, at, gain = 1) {
    const start = Math.round(at * sampleRate);
    const length = Math.round(0.12 * sampleRate);
    for (let i = 0; i < length; i++) {
        const s = start + i;
        if (s < 0 || s >= data.length) continue;
        const t = i / sampleRate;
        const env = Math.exp(-t * 30);
        const freq = 55 * Math.exp(-t * 20) + 40;   // pitch drop, like a real kick
        data[s] += Math.sin(2 * Math.PI * freq * t) * env * gain;
    }
}

function addHat(data, sampleRate, at, gain = 0.3, seed = 1) {
    const start = Math.round(at * sampleRate);
    const length = Math.round(0.04 * sampleRate);
    let s0 = seed >>> 0;
    const rnd = () => {
        s0 = (Math.imul(s0 ^ (s0 >>> 15), s0 | 1) + 0x6d2b79f5) >>> 0;
        return ((s0 >>> 14) & 0xffff) / 0xffff - 0.5;
    };
    for (let i = 0; i < length; i++) {
        const s = start + i;
        if (s < 0 || s >= data.length) continue;
        const env = Math.exp(-(i / sampleRate) * 90);
        data[s] += rnd() * env * gain;
    }
}

function addPad(data, sampleRate, from, to, gain = 0.15, root = 110) {
    const start = Math.round(from * sampleRate);
    const end = Math.min(data.length, Math.round(to * sampleRate));
    for (let s = start; s < end; s++) {
        const t = s / sampleRate;
        data[s] += (Math.sin(2 * Math.PI * root * t) + Math.sin(2 * Math.PI * root * 1.5 * t)) * gain * 0.5;
    }
}

/**
 * A four-to-the-floor track at a known BPM.
 * @param {object} opts
 * @returns {MockAudioBuffer}
 */
export function synthesizeBeat({
    bpm = 128,
    duration = 40,
    sampleRate = 44100,
    hats = true,
    pad = true,
    kickGain = 1,
    hatGain = 0.25,
    padRoot = 110,
    padGain = 0.12,
} = {}) {
    const length = Math.round(duration * sampleRate);
    const buffer = new MockAudioBuffer(2, length, sampleRate);
    const left = buffer.getChannelData(0);
    const right = buffer.getChannelData(1);

    const beat = 60 / bpm;
    let index = 0;
    for (let t = 0; t < duration; t += beat, index++) {
        // Accent the downbeat so the bar phase is detectable.
        addKick(left, sampleRate, t, kickGain * (index % 4 === 0 ? 1.0 : 0.8));
        if (hats) addHat(left, sampleRate, t + beat / 2, hatGain, index + 1);
    }
    if (pad) addPad(left, sampleRate, 0, duration, padGain, padRoot);

    for (let i = 0; i < length; i++) right[i] = left[i] * 0.98;
    return buffer;
}

/**
 * A track with a deliberate structural change at `changeAt` seconds: sparse and
 * dark before, dense and bright after. Segmentation must find that boundary.
 */
export function synthesizeSectioned({
    bpm = 128,
    duration = 120,
    changeAt = 60,
    sampleRate = 44100,
} = {}) {
    const length = Math.round(duration * sampleRate);
    const buffer = new MockAudioBuffer(2, length, sampleRate);
    const left = buffer.getChannelData(0);
    const right = buffer.getChannelData(1);

    const beat = 60 / bpm;
    let index = 0;
    for (let t = 0; t < duration; t += beat, index++) {
        const after = t >= changeAt;
        addKick(left, sampleRate, t, after ? 1.0 : 0.35);
        if (after) {
            addHat(left, sampleRate, t + beat / 2, 0.45, index + 1);
            addHat(left, sampleRate, t + beat / 4, 0.25, index + 7);
        }
    }
    addPad(left, sampleRate, 0, changeAt, 0.10, 110);
    addPad(left, sampleRate, changeAt, duration, 0.22, 440);   // brighter after

    for (let i = 0; i < length; i++) right[i] = left[i] * 0.98;
    return buffer;
}

/** Silence, for degenerate-input checks. */
export function synthesizeSilence({ duration = 10, sampleRate = 44100 } = {}) {
    return new MockAudioBuffer(2, Math.round(duration * sampleRate), sampleRate);
}
