import { Arrays, DefaultObservableValue, isDefined, Notifier, Option, SetMultimap, SyncStream, Terminator, UUID } from "@opendaw/lib-std";
import { RenderQuantum } from "@opendaw/lib-dsp";
import { Communicator, Messenger } from "@opendaw/lib-runtime";
import { EngineSettingsSchema, EngineStateSchema, ExportStemsConfiguration, PERF_BUFFER_SIZE, PreferencesHost } from "@opendaw/studio-adapters";
import { SyncSource } from "@opendaw/lib-box";
import { AnimationFrame } from "@opendaw/lib-dom";
import { MIDIReceiver } from "./midi";
import { HRClockWorker } from "./HRClockWorker";
export class EngineWorklet extends AudioWorkletNode {
    static ID = 0 | 0;
    id = EngineWorklet.ID++;
    #terminator = new Terminator();
    #project;
    #playbackTimestamp = new DefaultObservableValue(0.0);
    #position = new DefaultObservableValue(0.0);
    #bpm = new DefaultObservableValue(120.0);
    #isPlaying = new DefaultObservableValue(false);
    #isRecording = new DefaultObservableValue(false);
    #isCountingIn = new DefaultObservableValue(false);
    #countInBeatsRemaining = new DefaultObservableValue(0);
    #preferences;
    #markerState = new DefaultObservableValue(null);
    #cpuLoad = new DefaultObservableValue(0);
    #controlFlags;
    #notifyClipNotification;
    #notifyNoteSignals;
    #playingClips;
    #deviceMessageListeners = new SetMultimap();
    #commands;
    #isReady;
    #perfBuffer = new Float32Array(0);
    #perfIndex = 0;
    #lastPerfReadIndex = 0;
    #consecutiveOverloadCount = 0;
    #lastCpuLoadUpdate = 0;
    #maxMsSinceLastUpdate = 0;
    #channelMerger = null;
    #monitoringSources = new Map();
    constructor(context, project, exportConfiguration, options) {
        const numberOfChannels = ExportStemsConfiguration.countStems(Option.wrap(exportConfiguration)) * 2;
        const budgetMs = (RenderQuantum / context.sampleRate) * 1000;
        const reader = SyncStream.reader(EngineStateSchema(), state => {
            this.#isPlaying.setValue(state.isPlaying);
            this.#isRecording.setValue(state.isRecording);
            this.#isCountingIn.setValue(state.isCountingIn);
            this.#countInBeatsRemaining.setValue(state.countInBeatsRemaining);
            this.#playbackTimestamp.setValue(state.playbackTimestamp);
            this.#bpm.setValue(state.bpm);
            this.#perfBuffer = state.perfBuffer;
            this.#perfIndex = state.perfIndex;
            this.#updateCpuLoad(budgetMs, project);
            this.#position.setValue(state.position); // This must be the last to handle the state values before
        });
        const controlFlagsSAB = new SharedArrayBuffer(4); // 4 bytes minimum
        super(context, "engine-processor", {
            numberOfInputs: 1,
            numberOfOutputs: 1,
            outputChannelCount: [numberOfChannels],
            processorOptions: {
                syncStreamBuffer: reader.buffer,
                controlFlagsBuffer: controlFlagsSAB,
                hrClockBuffer: HRClockWorker.get().sab,
                project: project.toArrayBuffer(),
                exportConfiguration,
                options
            }
        });
        const { resolve, promise: isReady } = Promise.withResolvers();
        const messenger = Messenger.for(this.port);
        this.#project = project;
        this.#isReady = isReady;
        this.#notifyClipNotification = this.#terminator.own(new Notifier());
        this.#notifyNoteSignals = this.#terminator.own(new Notifier());
        this.#playingClips = [];
        this.#controlFlags = new Int32Array(controlFlagsSAB);
        this.#commands = this.#terminator.own(Communicator.sender(messenger.channel("engine-commands"), dispatcher => new class {
            play() { dispatcher.dispatchAndForget(this.play); }
            stop(reset) { dispatcher.dispatchAndForget(this.stop, reset); }
            setPosition(position) { dispatcher.dispatchAndForget(this.setPosition, position); }
            prepareRecordingState(countIn) {
                dispatcher.dispatchAndForget(this.prepareRecordingState, countIn);
            }
            stopRecording() { dispatcher.dispatchAndForget(this.stopRecording); }
            queryLoadingComplete() {
                return dispatcher.dispatchAndReturn(this.queryLoadingComplete);
            }
            panic() { dispatcher.dispatchAndForget(this.panic); }
            sleep() { dispatcher.dispatchAndForget(this.sleep); }
            wake() { dispatcher.dispatchAndForget(this.wake); }
            loadClickSound(index, data) {
                dispatcher.dispatchAndForget(this.loadClickSound, index, data);
            }
            setFrozenAudio(uuid, audioData) {
                dispatcher.dispatchAndForget(this.setFrozenAudio, uuid, audioData);
            }
            noteSignal(signal) { dispatcher.dispatchAndForget(this.noteSignal, signal); }
            ignoreNoteRegion(uuid) {
                dispatcher.dispatchAndForget(this.ignoreNoteRegion, uuid);
            }
            scheduleClipPlay(clipIds) {
                dispatcher.dispatchAndForget(this.scheduleClipPlay, clipIds);
            }
            scheduleClipStop(trackIds) {
                dispatcher.dispatchAndForget(this.scheduleClipStop, trackIds);
            }
            setupMIDI(port, buffer) {
                dispatcher.dispatchAndForget(this.setupMIDI, port, buffer);
            }
            updateMonitoringMap(map) {
                dispatcher.dispatchAndForget(this.updateMonitoringMap, map);
            }
            terminate() { dispatcher.dispatchAndForget(this.terminate); }
        }));
        const { port, sab } = this.#terminator.own(MIDIReceiver.create(() => context instanceof AudioContext ? context.outputLatency * 1000 : 20, (deviceId, data, relativeTimeInMs) => this.#project.receivedMIDIFromEngine(deviceId, data, relativeTimeInMs)));
        this.#commands.setupMIDI(port, sab);
        Communicator.executor(messenger.channel("engine-to-client"), {
            log: (message) => console.log("WORKLET", message),
            error: (reason) => this.dispatchEvent(new ErrorEvent("error", { error: reason })),
            deviceMessage: (uuid, message) => {
                for (const listener of this.#deviceMessageListeners.get(uuid)) {
                    listener(message);
                }
            },
            ready: () => resolve(),
            fetchAudio: (uuid) => {
                return new Promise((resolve, reject) => {
                    const handler = project.sampleManager.getOrCreate(uuid);
                    const subscription = handler.subscribe(state => {
                        if (state.type === "error") {
                            reject(new Error(state.reason));
                            subscription.terminate();
                        }
                        else if (state.type === "loaded") {
                            resolve(handler.data.unwrap());
                            subscription.terminate();
                        }
                    });
                });
            },
            fetchSoundfont: (uuid) => {
                return new Promise((resolve, reject) => {
                    const handler = project.soundfontManager.getOrCreate(uuid);
                    const subscription = handler.subscribe(state => {
                        if (state.type === "error") {
                            reject(new Error(state.reason));
                            subscription.terminate();
                        }
                        else if (state.type === "loaded") {
                            resolve(handler.soundfont.unwrap());
                            subscription.terminate();
                        }
                    });
                });
            },
            fetchNamWasm: async () => {
                const url = new URL("@opendaw/nam-wasm/nam.wasm", import.meta.url);
                const response = await fetch(url);
                return response.arrayBuffer();
            },
            notifyClipSequenceChanges: (changes) => {
                changes.stopped.forEach(uuid => {
                    for (let i = 0; i < this.#playingClips.length; i++) {
                        if (UUID.equals(this.#playingClips[i], uuid)) {
                            this.#playingClips.splice(i, 1);
                            break;
                        }
                    }
                });
                changes.started.forEach(uuid => this.#playingClips.push(uuid));
                this.#notifyClipNotification.notify({ type: "sequencing", changes });
            },
            switchMarkerState: (state) => this.#markerState.setValue(state)
        });
        this.#preferences = this.#terminator.own(new PreferencesHost(EngineSettingsSchema.parse({})));
        this.#terminator.ownAll(AnimationFrame.add(() => reader.tryRead()), project.liveStreamReceiver.connect(messenger.channel("engine-live-data")), this.#preferences.syncWith(messenger.channel("engine-preferences")), new SyncSource(project.boxGraph, messenger.channel("engine-sync"), false));
    }
    play() {
        this.wake();
        this.#commands.play();
    }
    stop(reset = false) {
        this.#isPlaying.setValue(false);
        this.#commands.stop(reset);
    }
    setPosition(position) { this.#commands.setPosition(position); }
    prepareRecordingState(countIn) { this.#commands.prepareRecordingState(countIn); }
    stopRecording() { this.#commands.stopRecording(); }
    panic() { this.#commands.panic(); }
    sleep() {
        Atomics.store(this.#controlFlags, 0, 1);
        this.#isPlaying.setValue(false);
        this.#commands.stop(true);
    }
    wake() { Atomics.store(this.#controlFlags, 0, 0); }
    loadClickSound(index, data) { this.#commands.loadClickSound(index, data); }
    setFrozenAudio(uuid, audioData) { this.#commands.setFrozenAudio(uuid, audioData); }
    get isPlaying() { return this.#isPlaying; }
    get isRecording() { return this.#isRecording; }
    get isCountingIn() { return this.#isCountingIn; }
    get countInBeatsRemaining() { return this.#countInBeatsRemaining; }
    get position() { return this.#position; }
    get bpm() { return this.#bpm; }
    get playbackTimestamp() { return this.#playbackTimestamp; }
    get markerState() { return this.#markerState; }
    get project() { return this.#project; }
    get preferences() { return this.#preferences; }
    get cpuLoad() { return this.#cpuLoad; }
    get perfBuffer() { return this.#perfBuffer; }
    get perfIndex() { return this.#perfIndex; }
    isReady() { return this.#isReady; }
    queryLoadingComplete() { return this.#commands.queryLoadingComplete(); }
    noteSignal(signal) { this.#commands.noteSignal(signal); }
    subscribeNotes(observer) { return this.#notifyNoteSignals.subscribe(observer); }
    ignoreNoteRegion(uuid) { this.#commands.ignoreNoteRegion(uuid); }
    scheduleClipPlay(clipIds) {
        this.#notifyClipNotification.notify({ type: "waiting", clips: clipIds });
        this.#commands.scheduleClipPlay(clipIds);
    }
    scheduleClipStop(trackIds) {
        this.#commands.scheduleClipStop(trackIds);
    }
    subscribeClipNotification(observer) {
        observer({
            type: "sequencing",
            changes: { started: this.#playingClips, stopped: Arrays.empty(), obsolete: Arrays.empty() }
        });
        return this.#notifyClipNotification.subscribe(observer);
    }
    subscribeDeviceMessage(uuid, listener) {
        this.#deviceMessageListeners.add(uuid, listener);
        return { terminate: () => this.#deviceMessageListeners.remove(uuid, listener) };
    }
    registerMonitoringSource(uuid, node, numChannels) {
        this.#monitoringSources.set(UUID.toString(uuid), { node, numChannels });
        this.#rebuildMonitoringMerger();
    }
    unregisterMonitoringSource(uuid) {
        const key = UUID.toString(uuid);
        const entry = this.#monitoringSources.get(key);
        if (isDefined(entry)) {
            entry.node.disconnect();
            this.#monitoringSources.delete(key);
        }
        this.#rebuildMonitoringMerger();
    }
    #rebuildMonitoringMerger() {
        if (isDefined(this.#channelMerger)) {
            this.#channelMerger.disconnect();
            this.#channelMerger = null;
        }
        if (this.#monitoringSources.size === 0) {
            this.#commands.updateMonitoringMap([]);
            return;
        }
        let totalChannels = 0;
        for (const { numChannels } of this.#monitoringSources.values()) {
            totalChannels += numChannels;
        }
        this.#channelMerger = this.context.createChannelMerger(totalChannels);
        this.#channelMerger.connect(this);
        const map = [];
        let channel = 0;
        for (const [uuidString, { node, numChannels }] of this.#monitoringSources) {
            const uuid = UUID.parse(uuidString);
            const splitter = this.context.createChannelSplitter(numChannels);
            node.connect(splitter);
            const channels = [];
            for (let i = 0; i < numChannels; i++) {
                splitter.connect(this.#channelMerger, i, channel);
                channels.push(channel);
                channel++;
            }
            map.push({ uuid, channels });
        }
        this.#commands.updateMonitoringMap(map);
    }
    #updateCpuLoad(budgetMs, project) {
        while (this.#lastPerfReadIndex !== this.#perfIndex) {
            const ms = this.#perfBuffer[this.#lastPerfReadIndex];
            if (ms > this.#maxMsSinceLastUpdate) {
                this.#maxMsSinceLastUpdate = ms;
            }
            if (ms >= budgetMs) {
                this.#consecutiveOverloadCount++;
                if (this.#consecutiveOverloadCount >= 30) {
                    project.handleCpuOverload();
                    this.#consecutiveOverloadCount = 0;
                }
            }
            else {
                this.#consecutiveOverloadCount = 0;
            }
            this.#lastPerfReadIndex = (this.#lastPerfReadIndex + 1) % PERF_BUFFER_SIZE;
        }
        const now = performance.now();
        if (now - this.#lastCpuLoadUpdate >= 1000) {
            this.#cpuLoad.setValue(Math.round((this.#maxMsSinceLastUpdate / budgetMs) * 100));
            this.#maxMsSinceLastUpdate = 0;
            this.#lastCpuLoadUpdate = now;
        }
    }
    terminate() {
        this.#commands.terminate();
        this.#terminator.terminate();
        this.disconnect();
    }
}
