import { Errors, isDefined, isUndefined, MutableObservableOption, Option, RuntimeNotifier, Terminable } from "@opendaw/lib-std";
import { dbToGain } from "@opendaw/lib-dsp";
import { Promises } from "@opendaw/lib-runtime";
import { Capture } from "./Capture";
import { RecordAudio } from "./RecordAudio";
import { AudioDevices } from "../AudioDevices";
import { RenderQuantum } from "../RenderQuantum";
export class CaptureAudio extends Capture {
    #stream;
    #streamGenerator;
    #monitoringMode = "off";
    #requestChannels = Option.None;
    #gainDb = 0.0;
    #audioChain = null;
    #preparedWorklet = null;
    constructor(manager, audioUnitBox, captureAudioBox) {
        super(manager, audioUnitBox, captureAudioBox);
        this.#stream = new MutableObservableOption();
        this.#streamGenerator = Promises.sequentialize(() => this.#updateStream());
        this.ownAll(captureAudioBox.requestChannels.catchupAndSubscribe(owner => {
            const channels = owner.getValue();
            this.#requestChannels = channels === 1 || channels === 2 ? Option.wrap(channels) : Option.None;
            this.#stream.ifSome(stream => this.#rebuildAudioChain(stream));
            // Re-register monitoring if in effects mode (channel count may have changed)
            if (this.#monitoringMode === "effects" && isDefined(this.#audioChain)) {
                const engine = this.manager.project.engine;
                engine.unregisterMonitoringSource(this.audioUnitBox.address.uuid);
                engine.registerMonitoringSource(this.audioUnitBox.address.uuid, this.#audioChain.gainNode, this.#audioChain.channelCount);
            }
        }), captureAudioBox.gainDb.catchupAndSubscribe(owner => {
            this.#gainDb = owner.getValue();
            if (isDefined(this.#audioChain)) {
                this.#audioChain.gainNode.gain.value = dbToGain(this.#gainDb);
            }
        }), captureAudioBox.deviceId.catchupAndSubscribe(async () => {
            if (this.armed.getValue()) {
                await this.#streamGenerator();
            }
        }), this.armed.catchupAndSubscribe(async (owner) => {
            const armed = owner.getValue();
            if (armed) {
                await this.#streamGenerator();
            }
            else {
                this.#stopStream();
            }
        }));
    }
    get isMonitoring() { return this.#monitoringMode !== "off"; }
    get monitoringMode() { return this.#monitoringMode; }
    set monitoringMode(value) {
        if (this.#monitoringMode === value) {
            return;
        }
        this.#disconnectMonitoring();
        this.#monitoringMode = value;
        if (this.#monitoringMode !== "off") {
            this.armed.setValue(true);
        }
        this.#connectMonitoring();
    }
    get gainDb() { return this.#gainDb; }
    get requestChannels() { return this.#requestChannels; }
    set requestChannels(value) { this.captureBox.requestChannels.setValue(value); }
    get stream() { return this.#stream; }
    get streamDeviceId() {
        return this.streamMediaTrack.map(settings => settings.getSettings().deviceId ?? "");
    }
    get label() { return this.streamMediaTrack.mapOr(track => track.label, "Default"); }
    get deviceLabel() { return this.streamMediaTrack.map(track => track.label ?? ""); }
    get streamMediaTrack() {
        return this.#stream.flatMap(stream => Option.wrap(stream.getAudioTracks().at(0)));
    }
    get outputNode() { return Option.wrap(this.#audioChain?.gainNode); }
    get effectiveChannelCount() { return this.#audioChain?.channelCount ?? 1; }
    async prepareRecording() {
        const { project } = this.manager;
        const { env: { audioContext, audioWorklets, sampleManager, sampleService } } = project;
        if (isUndefined(audioContext.outputLatency)) {
            const approved = RuntimeNotifier.approve({
                headline: "Warning",
                message: "Your browser does not support 'output latency'. This will cause timing issue while recording.",
                approveText: "Ignore",
                cancelText: "Cancel"
            });
            if (!approved) {
                return Promise.reject("Recording cancelled");
            }
        }
        await this.#streamGenerator();
        const audioChain = this.#audioChain;
        if (!isDefined(audioChain)) {
            return Promise.reject("No audio chain available for recording.");
        }
        const { gainNode, channelCount } = audioChain;
        const recordingWorklet = audioWorklets.createRecording(channelCount, RenderQuantum);
        recordingWorklet.bpm = project.timelineBox.bpm.getValue();
        recordingWorklet.sampleService = sampleService;
        sampleManager.record(recordingWorklet);
        gainNode.connect(recordingWorklet);
        this.#preparedWorklet = recordingWorklet;
    }
    startRecording() {
        const { project } = this.manager;
        const { env: { audioContext, sampleManager } } = project;
        const audioChain = this.#audioChain;
        const recordingWorklet = this.#preparedWorklet;
        if (!isDefined(audioChain) || !isDefined(recordingWorklet)) {
            console.warn("No audio chain or worklet available for recording.");
            return Terminable.Empty;
        }
        this.#preparedWorklet = null;
        const { gainNode } = audioChain;
        return RecordAudio.start({
            recordingWorklet,
            sourceNode: gainNode,
            sampleManager,
            project,
            capture: this,
            outputLatency: audioContext.outputLatency ?? 0
        });
    }
    async #updateStream() {
        if (this.#stream.nonEmpty()) {
            const stream = this.#stream.unwrap();
            const settings = stream.getAudioTracks().at(0)?.getSettings();
            if (isDefined(settings)) {
                const deviceId = this.deviceId.getValue().unwrapOrUndefined();
                if (deviceId === settings.deviceId) {
                    return Promise.resolve();
                }
            }
        }
        this.#stopStream();
        const deviceId = this.deviceId.getValue().unwrapOrUndefined() ?? AudioDevices.defaultInput?.deviceId;
        const channelCount = this.#requestChannels.unwrapOrElse(2);
        return AudioDevices.requestStream({
            deviceId: isDefined(deviceId) ? { exact: deviceId } : undefined,
            echoCancellation: false,
            noiseSuppression: false,
            autoGainControl: false,
            channelCount: { ideal: channelCount }
        }).then(stream => {
            const tracks = stream.getAudioTracks();
            const track = tracks.at(0);
            const settings = track?.getSettings();
            const gotDeviceId = settings?.deviceId;
            console.debug(`new stream. device requested: ${deviceId ?? "default"}, got: ${gotDeviceId ?? "unknown"}. channelCount requested: ${channelCount}, got: ${settings?.channelCount}`);
            if (isUndefined(deviceId) || deviceId === gotDeviceId) {
                this.#rebuildAudioChain(stream);
                this.#stream.wrap(stream);
            }
            else {
                stream.getAudioTracks().forEach(track => track.stop());
                return Errors.warn(`Could not find audio device with id: '${deviceId}' (got: '${gotDeviceId}')`);
            }
        });
    }
    #stopStream() {
        this.#destroyAudioChain();
        this.#stream.clear(stream => stream.getAudioTracks().forEach(track => track.stop()));
    }
    #rebuildAudioChain(stream) {
        const wasMonitoringMode = this.#monitoringMode !== "off" && isDefined(this.#audioChain) ? this.#monitoringMode : "off";
        this.#destroyAudioChain();
        const { audioContext } = this.manager.project.env;
        const sourceNode = audioContext.createMediaStreamSource(stream);
        const gainNode = audioContext.createGain();
        gainNode.gain.value = dbToGain(this.#gainDb);
        const streamChannelCount = Math.min(stream.getAudioTracks().at(0)?.getSettings().channelCount ?? 2, 2);
        const channelCount = this.#requestChannels.unwrapOrElse(streamChannelCount);
        gainNode.channelCount = channelCount;
        gainNode.channelCountMode = "explicit";
        sourceNode.connect(gainNode);
        this.#audioChain = { sourceNode, gainNode, channelCount };
        if (wasMonitoringMode !== "off" || this.#monitoringMode !== "off") {
            this.#connectMonitoring();
        }
    }
    #destroyAudioChain() {
        if (isDefined(this.#audioChain)) {
            const { sourceNode, gainNode } = this.#audioChain;
            sourceNode.disconnect();
            gainNode.disconnect();
            this.#audioChain = null;
        }
    }
    #connectMonitoring() {
        if (!isDefined(this.#audioChain)) {
            return;
        }
        switch (this.#monitoringMode) {
            case "off":
                break;
            case "direct":
                this.#audioChain.gainNode.connect(this.manager.project.env.audioContext.destination);
                break;
            case "effects":
                const engine = this.manager.project.engine;
                engine.registerMonitoringSource(this.audioUnitBox.address.uuid, this.#audioChain.gainNode, this.#audioChain.channelCount);
                break;
        }
    }
    #disconnectMonitoring() {
        if (!isDefined(this.#audioChain)) {
            return;
        }
        switch (this.#monitoringMode) {
            case "off":
                break;
            case "direct":
                this.#audioChain.gainNode.disconnect(this.manager.project.env.audioContext.destination);
                break;
            case "effects":
                this.#audioChain.gainNode.disconnect();
                this.manager.project.engine.unregisterMonitoringSource(this.audioUnitBox.address.uuid);
                break;
        }
    }
}
