// SonicForge Studio Audio Engine Service
// High-performance Desktop-Grade Client-Side Audio Engine & DSP Service (21_CLIENT_PRE.md)

(function() {
    let audioCtx = null;
    let workletLoaded = false;

    function getAudioContext() {
        if (!audioCtx) {
            audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (audioCtx.state === 'suspended') {
            audioCtx.resume();
        }
        return audioCtx;
    }

    async function initAudioWorklet() {
        if (workletLoaded) return true;
        const ctx = getAudioContext();
        try {
            if (ctx.audioWorklet) {
                await ctx.audioWorklet.addModule('/static/js/services/sonicAudioWorklet.js');
                workletLoaded = true;
                console.log('[SonicAudio] AudioWorklet registered successfully.');
                return true;
            }
        } catch (err) {
            console.warn('[SonicAudio] AudioWorklet initialization fallback:', err.message);
        }
        return false;
    }

    function analyzeAudioBufferChannels(audioBuffer) {
        if (!audioBuffer) return { channels: 1, isStereo: false, label: 'MONO' };
        const numChannels = audioBuffer.numberOfChannels;
        const isStereo = numChannels >= 2;
        return {
            channels: numChannels,
            isStereo: isStereo,
            label: isStereo ? 'STEREO' : 'MONO',
            sampleRate: audioBuffer.sampleRate,
            duration: audioBuffer.duration,
            length: audioBuffer.length
        };
    }

    async function decodeAudioFile(file) {
        const ctx = getAudioContext();
        const arrayBuffer = await file.arrayBuffer();
        const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
        const channelInfo = analyzeAudioBufferChannels(audioBuffer);
        return { audioBuffer, channelInfo };
    }

    // ── 1. Non-Destructive Edit Decision List (EDL VFS Engine - 21_CLIENT_PRE.md §4) ──
    function createEDL(bufferId, buffer) {
        if (!buffer) return [];
        return [{
            id: 'seg_' + Math.random().toString(36).substr(2, 9),
            sourceBufferId: bufferId,
            startSample: 0,
            length: buffer.length,
            playbackRate: 1.0,
            isSilence: false,
            isReversed: false
        }];
    }

    function deleteEDLRange(edlList, startSec, endSec, sampleRate) {
        const startSample = Math.floor(startSec * sampleRate);
        const endSample = Math.floor(endSec * sampleRate);
        const result = [];
        let currentPos = 0;

        for (const seg of edlList) {
            const segStart = currentPos;
            const segEnd = currentPos + seg.length;

            if (segEnd <= startSample || segStart >= endSample) {
                // Completely outside delete window
                result.push({ ...seg });
            } else {
                // Overlaps delete window
                if (segStart < startSample) {
                    const keepLen = startSample - segStart;
                    result.push({ ...seg, id: 'seg_' + Math.random().toString(36).substr(2, 9), length: keepLen });
                }
                if (segEnd > endSample) {
                    const cutOffset = endSample - segStart;
                    const keepLen = segEnd - endSample;
                    result.push({
                        ...seg,
                        id: 'seg_' + Math.random().toString(36).substr(2, 9),
                        startSample: seg.startSample + cutOffset,
                        length: keepLen
                    });
                }
            }
            currentPos = segEnd;
        }
        return result;
    }

    function renderEDLToBuffer(edlList, sourceBuffersMap, sampleRate) {
        let totalSamples = 0;
        for (const seg of edlList) {
            totalSamples += seg.length;
        }

        const ctx = getAudioContext();
        if (totalSamples === 0) {
            return ctx.createBuffer(2, sampleRate * 0.1, sampleRate);
        }

        const numChannels = 2;
        const outBuffer = ctx.createBuffer(numChannels, totalSamples, sampleRate);
        const outL = outBuffer.getChannelData(0);
        const outR = outBuffer.getChannelData(1);

        let writeOffset = 0;
        for (const seg of edlList) {
            if (seg.isSilence) {
                writeOffset += seg.length;
                continue;
            }
            const srcBuffer = sourceBuffersMap[seg.sourceBufferId];
            if (!srcBuffer) {
                writeOffset += seg.length;
                continue;
            }

            const srcL = srcBuffer.getChannelData(0);
            const srcR = srcBuffer.numberOfChannels > 1 ? srcBuffer.getChannelData(1) : srcL;
            const len = Math.min(seg.length, srcBuffer.length - seg.startSample);

            for (let i = 0; i < len; i++) {
                const readIdx = seg.isReversed
                    ? seg.startSample + len - 1 - i
                    : seg.startSample + i;
                if (readIdx >= 0 && readIdx < srcBuffer.length) {
                    outL[writeOffset + i] = srcL[readIdx];
                    outR[writeOffset + i] = srcR[readIdx];
                }
            }
            writeOffset += seg.length;
        }
        return outBuffer;
    }

    // ── 2. Client-Side DSP Core Engine (21_CLIENT_PRE.md §3 & §5) ──

    // Constant-Power Panning Math
    function calculateConstantPowerPan(panVal, volDb = 0) {
        const gain = Math.pow(10, volDb / 20);
        const theta = ((Math.max(-1, Math.min(1, panVal)) + 1) / 2) * (Math.PI / 2);
        return {
            gainL: Math.cos(theta) * gain,
            gainR: Math.sin(theta) * gain,
            gainLinear: gain
        };
    }

    // Dynamics Compressor / Limiter
    function applyDynamicsCompressor(audioBuffer, thresholdDb = -20, ratio = 4.0, attackMs = 10, releaseMs = 100) {
        const ctx = getAudioContext();
        const numChannels = audioBuffer.numberOfChannels;
        const sampleRate = audioBuffer.sampleRate;
        const len = audioBuffer.length;
        const outBuffer = ctx.createBuffer(numChannels, len, sampleRate);

        const attackCoef = Math.exp(-1 / (sampleRate * (attackMs / 1000)));
        const releaseCoef = Math.exp(-1 / (sampleRate * (releaseMs / 1000)));
        const thresholdLinear = Math.pow(10, thresholdDb / 20);

        const channelsData = [];
        const outData = [];
        for (let ch = 0; ch < numChannels; ch++) {
            channelsData.push(audioBuffer.getChannelData(ch));
            outData.push(outBuffer.getChannelData(ch));
        }

        let envelope = 0;
        const blockSize = 128;

        for (let i = 0; i < len; i += blockSize) {
            const currentBlockSize = Math.min(blockSize, len - i);

            // Compute RMS energy of block
            let sumSq = 0;
            for (let b = 0; b < currentBlockSize; b++) {
                const sampleL = channelsData[0][i + b];
                sumSq += sampleL * sampleL;
            }
            const rms = Math.sqrt(sumSq / currentBlockSize);

            // Envelope follower
            if (rms > envelope) {
                envelope = attackCoef * envelope + (1 - attackCoef) * rms;
            } else {
                envelope = releaseCoef * envelope + (1 - releaseCoef) * rms;
            }

            // Target Gain calculation
            let targetGain = 1.0;
            if (envelope > thresholdLinear && envelope > 0) {
                const envDb = 20 * Math.log10(envelope);
                const overDb = envDb - thresholdDb;
                const compressedDb = thresholdDb + overDb / ratio;
                targetGain = Math.pow(10, (compressedDb - envDb) / 20);
            }

            for (let b = 0; b < currentBlockSize; b++) {
                for (let ch = 0; ch < numChannels; ch++) {
                    outData[ch][i + b] = channelsData[ch][i + b] * targetGain;
                }
            }
        }
        return outBuffer;
    }

    // Phase Vocoder / Overlap-Add Time Stretch
    function applyPhaseVocoderStretch(audioBuffer, speedRatio) {
        if (speedRatio <= 0.01 || Math.abs(speedRatio - 1.0) < 0.001) return audioBuffer;

        const ctx = getAudioContext();
        const numChannels = audioBuffer.numberOfChannels;
        const sampleRate = audioBuffer.sampleRate;
        const inLen = audioBuffer.length;
        const outLen = Math.floor(inLen / speedRatio);

        const outBuffer = ctx.createBuffer(numChannels, outLen, sampleRate);
        const windowSize = 1024;
        const inHop = Math.floor(windowSize / 4);
        const outHop = Math.floor(inHop / speedRatio);

        // Hanning Window
        const win = new Float32Array(windowSize);
        for (let n = 0; n < windowSize; n++) {
            win[n] = 0.5 * (1 - Math.cos((2 * Math.PI * n) / (windowSize - 1)));
        }

        for (let ch = 0; ch < numChannels; ch++) {
            const inData = audioBuffer.getChannelData(ch);
            const outData = outBuffer.getChannelData(ch);

            let inPos = 0;
            let outPos = 0;

            while (inPos + windowSize < inLen && outPos + windowSize < outLen) {
                for (let n = 0; n < windowSize; n++) {
                    outData[outPos + n] += inData[Math.floor(inPos) + n] * win[n];
                }
                inPos += inHop;
                outPos += outHop;
            }
        }
        return outBuffer;
    }

    window.SonicAudio = {
        getAudioContext,
        initAudioWorklet,
        analyzeAudioBufferChannels,
        decodeAudioFile,
        // EDL VFS
        createEDL,
        deleteEDLRange,
        renderEDLToBuffer,
        // DSP Core
        calculateConstantPowerPan,
        applyDynamicsCompressor,
        applyPhaseVocoderStretch
    };
})();
