// Iterative radix-2 Cooley-Tukey FFT with precomputed twiddles and bit-reversal.
//
// Allocated once per size and reused across all ~21k frames of a track — the
// analysis pass is the one place in this project where JS throughput actually
// matters, and per-frame allocation is what would sink it.

export class FFT {
    constructor(size) {
        if ((size & (size - 1)) !== 0) throw new Error(`FFT size must be a power of two, got ${size}`);
        this.size = size;
        this.half = size >> 1;

        this.re = new Float32Array(size);
        this.im = new Float32Array(size);
        this.magnitude = new Float32Array(this.half);

        // Bit-reversal permutation table.
        this.rev = new Uint32Array(size);
        const bits = Math.log2(size);
        for (let i = 0; i < size; i++) {
            let r = 0;
            for (let b = 0; b < bits; b++) if (i & (1 << b)) r |= 1 << (bits - 1 - b);
            this.rev[i] = r;
        }

        // Twiddle factors, flattened per stage.
        this.cos = new Float32Array(this.half);
        this.sin = new Float32Array(this.half);
        for (let i = 0; i < this.half; i++) {
            this.cos[i] = Math.cos((-2 * Math.PI * i) / size);
            this.sin[i] = Math.sin((-2 * Math.PI * i) / size);
        }
    }

    /**
     * Forward transform of a real windowed signal. Writes into `this.magnitude`
     * (first size/2 bins) and returns it. The buffer is reused: copy it if you
     * need to keep it.
     */
    forward(input) {
        const { size, re, im, rev, cos, sin } = this;

        for (let i = 0; i < size; i++) {
            re[i] = input[rev[i]];
            im[i] = 0;
        }

        for (let len = 2; len <= size; len <<= 1) {
            const halfLen = len >> 1;
            const step = size / len;
            for (let i = 0; i < size; i += len) {
                for (let j = 0, k = 0; j < halfLen; j++, k += step) {
                    const c = cos[k], s = sin[k];
                    const a = i + j, b = a + halfLen;
                    const tre = re[b] * c - im[b] * s;
                    const tim = re[b] * s + im[b] * c;
                    re[b] = re[a] - tre;
                    im[b] = im[a] - tim;
                    re[a] += tre;
                    im[a] += tim;
                }
            }
        }

        const mag = this.magnitude;
        const scale = 2 / size;
        for (let i = 0; i < this.half; i++) {
            mag[i] = Math.sqrt(re[i] * re[i] + im[i] * im[i]) * scale;
        }
        return mag;
    }
}

/** Periodic Hann window. Periodic (not symmetric) is correct for STFT analysis. */
export function hannWindow(size) {
    const w = new Float32Array(size);
    for (let i = 0; i < size; i++) w[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / size));
    return w;
}
