// The metronome — the Phase 1 gate, and the most useful validation tool in the
// project.
//
// NAMED CAREFULLY. This file was `clicktrack.js` until a content blocker ate it:
// `clicktrack` is a click-tracking telemetry pattern that EasyPrivacy and
// friends block by substring, so the request died in the browser, the module
// graph died with it, main.js never ran, and every control in the app went
// inert while looking perfectly fine. The server was serving it with a 200 the
// whole time.
//
// Anything shipped to a browser and named like tracking will be treated as
// tracking. Avoid `click`, `track`, `analytics`, `pixel`, `beacon` and `ad` in
// filenames and URL paths, however honest the code behind them is.
//
// Beat detection cannot be judged by watching visuals: a grid that is 20ms late
// or at half tempo still "looks kind of right". Mixing an audible click onto the
// detected grid makes the answer immediate and unambiguous. Downbeats get a
// higher pitch so bar alignment is audible too.
//
// If the clicks don't sit on the beat, stop and fix tempo.js before touching
// anything downstream — every timing artefact in the finished video originates here.

const CLICK_MS = 25;

/**
 * Render the track with clicks mixed over it.
 * @returns {Promise<AudioBuffer>}
 */
export async function renderClickTrack(audioBuffer, tempo, { clickGain = 0.5, musicGain = 0.6, downbeatsOnly = false } = {}) {
    const sampleRate = audioBuffer.sampleRate;
    const length = audioBuffer.length;
    const offline = new OfflineAudioContext(2, length, sampleRate);

    const music = offline.createBufferSource();
    music.buffer = audioBuffer;
    const musicNode = offline.createGain();
    musicNode.gain.value = musicGain;
    music.connect(musicNode).connect(offline.destination);

    const clickBuffer = makeClick(offline, sampleRate, 1000);
    const downbeatBuffer = makeClick(offline, sampleRate, 1800);

    const downbeatSet = new Set(tempo.downbeats.map((t) => Math.round(t * 1000)));

    for (const time of tempo.beats) {
        if (time >= audioBuffer.duration) break;
        const isDownbeat = downbeatSet.has(Math.round(time * 1000));
        if (downbeatsOnly && !isDownbeat) continue;

        const src = offline.createBufferSource();
        src.buffer = isDownbeat ? downbeatBuffer : clickBuffer;
        const gain = offline.createGain();
        gain.gain.value = clickGain * (isDownbeat ? 1.0 : 0.7);
        src.connect(gain).connect(offline.destination);
        src.start(time);
    }

    music.start(0);
    return await offline.startRendering();
}

function makeClick(ctx, sampleRate, frequency) {
    const length = Math.round((CLICK_MS / 1000) * sampleRate);
    const buffer = ctx.createBuffer(1, length, sampleRate);
    const data = buffer.getChannelData(0);
    for (let i = 0; i < length; i++) {
        const t = i / sampleRate;
        const env = Math.exp(-t * 180);
        data[i] = Math.sin(2 * Math.PI * frequency * t) * env;
    }
    return buffer;
}

/** Wrap an AudioBuffer as a WAV blob so it can be played or downloaded. */
export function audioBufferToWavBlob(audioBuffer) {
    const channels = audioBuffer.numberOfChannels;
    const length = audioBuffer.length;
    const sampleRate = audioBuffer.sampleRate;
    const bytesPerSample = 2;
    const blockAlign = channels * bytesPerSample;
    const dataSize = length * blockAlign;

    const buffer = new ArrayBuffer(44 + dataSize);
    const view = new DataView(buffer);

    const writeString = (offset, str) => {
        for (let i = 0; i < str.length; i++) view.setUint8(offset + i, str.charCodeAt(i));
    };

    writeString(0, 'RIFF');
    view.setUint32(4, 36 + dataSize, true);
    writeString(8, 'WAVE');
    writeString(12, 'fmt ');
    view.setUint32(16, 16, true);
    view.setUint16(20, 1, true);
    view.setUint16(22, channels, true);
    view.setUint32(24, sampleRate, true);
    view.setUint32(28, sampleRate * blockAlign, true);
    view.setUint16(32, blockAlign, true);
    view.setUint16(34, 16, true);
    writeString(36, 'data');
    view.setUint32(40, dataSize, true);

    const data = [];
    for (let c = 0; c < channels; c++) data.push(audioBuffer.getChannelData(c));

    let offset = 44;
    for (let i = 0; i < length; i++) {
        for (let c = 0; c < channels; c++) {
            const s = Math.max(-1, Math.min(1, data[c][i]));
            view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
            offset += 2;
        }
    }
    return new Blob([buffer], { type: 'audio/wav' });
}
