import recorderProcessorUrl from "./recorder-processor.js?url";
export var RecorderEvents;
(function (RecorderEvents) {
    RecorderEvents["Start"] = "start";
    RecorderEvents["Stop"] = "stop";
    RecorderEvents["Data"] = "data";
    RecorderEvents["Level"] = "level";
    RecorderEvents["Error"] = "error";
})(RecorderEvents || (RecorderEvents = {}));
export class Recorder {
    audioContext;
    mediaStream;
    mediaStreamSource;
    analyserNode;
    recorderProcessorNode;
    silentGainNode;
    meterFrameId;
    isRecording = false;
    setupPromise = null;
    listeners = {
        [RecorderEvents.Start]: new Set(),
        [RecorderEvents.Stop]: new Set(),
        [RecorderEvents.Data]: new Set(),
        [RecorderEvents.Level]: new Set(),
        [RecorderEvents.Error]: new Set(),
    };
    initialized = false;
    on(event, handler) {
        this.listeners[event].add(handler);
        return () => {
            this.listeners[event].delete(handler);
        };
    }
    emit(event, ...args) {
        for (const handler of this.listeners[event]) {
            ;
            handler(...args);
        }
    }
    clearListeners() {
        for (const event of Object.values(RecorderEvents)) {
            this.listeners[event].clear();
        }
    }
    async init(stream) {
        await this.ensureAudioGraph();
        this.attachStream(stream);
        this.initialized = true;
    }
    async ensureAudioGraph() {
        if (!this.setupPromise) {
            this.setupPromise = this.setupAudioGraph();
        }
        await this.setupPromise;
    }
    async setupAudioGraph() {
        try {
            this.audioContext = new AudioContext({ sampleRate: 16000 });
            await this.audioContext.audioWorklet.addModule(recorderProcessorUrl);
            this.recorderProcessorNode = new AudioWorkletNode(this.audioContext, "pmate-mic-recorder-processor");
            this.analyserNode = this.audioContext.createAnalyser();
            this.analyserNode.fftSize = 2048;
            this.analyserNode.smoothingTimeConstant = 0.5;
            this.silentGainNode = this.audioContext.createGain();
            this.silentGainNode.gain.value = 0;
            this.recorderProcessorNode.connect(this.silentGainNode);
            this.silentGainNode.connect(this.audioContext.destination);
            this.recorderProcessorNode.port.onmessage = (event) => {
                if (!this.isRecording) {
                    return;
                }
                const { type, data } = event.data;
                if (type === "pcm" && data) {
                    this.emit(RecorderEvents.Data, new Blob([data]));
                }
            };
        }
        catch (error) {
            const normalized = toError(error);
            this.emit(RecorderEvents.Error, normalized);
            this.dispose();
            throw normalized;
        }
    }
    attachStream(stream) {
        this.detachStream();
        this.mediaStream = stream;
        this.mediaStream.getAudioTracks().forEach((track) => {
            track.enabled = false;
        });
        if (!this.audioContext || !this.recorderProcessorNode) {
            throw new Error("Recorder audio graph is not ready");
        }
        this.mediaStreamSource = this.audioContext.createMediaStreamSource(this.mediaStream);
        this.mediaStreamSource.connect(this.analyserNode);
        this.mediaStreamSource.connect(this.recorderProcessorNode);
    }
    detachStream(options) {
        if (this.mediaStreamSource) {
            try {
                this.mediaStreamSource.disconnect();
            }
            catch {
                // ignore disconnect errors during teardown
            }
            this.mediaStreamSource = undefined;
        }
        if (this.mediaStream) {
            if (options?.stopTracks) {
                this.mediaStream.getTracks().forEach((track) => track.stop());
            }
            else {
                this.mediaStream.getAudioTracks().forEach((track) => {
                    track.enabled = false;
                });
            }
        }
        this.mediaStream = undefined;
    }
    async startRecording() {
        if (this.isRecording) {
            return;
        }
        if (!this.audioContext || !this.mediaStream || !this.recorderProcessorNode) {
            const error = new Error("Recorder not initialized");
            this.emit(RecorderEvents.Error, error);
            throw error;
        }
        this.isRecording = true;
        try {
            this.mediaStream.getAudioTracks().forEach((track) => {
                track.enabled = true;
            });
            await this.audioContext.resume();
            this.startMetering();
            this.emit(RecorderEvents.Start);
        }
        catch (error) {
            const normalized = toError(error);
            this.emit(RecorderEvents.Error, normalized);
            this.isRecording = false;
            throw normalized;
        }
    }
    stopRecording() {
        if (!this.isRecording) {
            return;
        }
        this.isRecording = false;
        this.stopMetering();
        this.emit(RecorderEvents.Stop);
    }
    dispose() {
        this.isRecording = false;
        this.stopMetering();
        this.detachStream({ stopTracks: true });
        this.analyserNode?.disconnect();
        this.recorderProcessorNode?.disconnect();
        this.silentGainNode?.disconnect();
        void this.audioContext?.close();
        this.mediaStream = undefined;
        this.recorderProcessorNode = undefined;
        this.audioContext = undefined;
        this.mediaStreamSource = undefined;
        this.analyserNode = undefined;
        this.silentGainNode = undefined;
        this.setupPromise = null;
        this.initialized = false;
    }
    get isRecordingStatus() {
        return this.isRecording;
    }
    startMetering() {
        this.stopMetering();
        const analyser = this.analyserNode;
        if (!analyser) {
            return;
        }
        const buffer = new Uint8Array(analyser.fftSize);
        const draw = () => {
            if (!this.isRecording || !this.analyserNode) {
                this.stopMetering();
                return;
            }
            this.analyserNode.getByteTimeDomainData(buffer);
            this.emit(RecorderEvents.Level, toWaveformLevels(buffer, 18));
            this.meterFrameId = requestAnimationFrame(draw);
        };
        this.meterFrameId = requestAnimationFrame(draw);
    }
    stopMetering() {
        if (this.meterFrameId) {
            cancelAnimationFrame(this.meterFrameId);
            this.meterFrameId = undefined;
        }
    }
}
function toError(error) {
    return error instanceof Error ? error : new Error(String(error));
}
function toWaveformLevels(buffer, bars) {
    const samplesPerBar = Math.max(1, Math.floor(buffer.length / bars));
    const levels = [];
    for (let barIndex = 0; barIndex < bars; barIndex += 1) {
        let sumSquares = 0;
        let maxAmplitude = 0;
        let counted = 0;
        for (let sampleIndex = 0; sampleIndex < samplesPerBar; sampleIndex += 1) {
            const index = barIndex * samplesPerBar + sampleIndex;
            if (index >= buffer.length) {
                break;
            }
            const normalized = (buffer[index] - 128) / 128;
            const amplitude = Math.abs(normalized);
            sumSquares += normalized * normalized;
            maxAmplitude = Math.max(maxAmplitude, amplitude);
            counted += 1;
        }
        const rms = counted > 0 ? Math.sqrt(sumSquares / counted) : 0;
        const boosted = Math.min(1, Math.max(rms, maxAmplitude) * 9);
        levels.push(0.08 + boosted * 0.92);
    }
    return levels;
}
