import { DefaultObservableValue, Errors, Notifier, Option, RuntimeNotifier, Terminator, UUID } from "@opendaw/lib-std";
import { Promises } from "@opendaw/lib-runtime";
import { OfflineEngineRenderer } from "./OfflineEngineRenderer";
export class AudioUnitFreeze {
    #project;
    #terminator = new Terminator();
    #frozenAudioUnits = new Map();
    #notifier = new Notifier();
    constructor(project) {
        this.#project = project;
        const { timelineBoxAdapter } = project;
        this.#terminator.ownAll(timelineBoxAdapter.box.bpm.subscribe(() => this.#unfreezeAll()), timelineBoxAdapter.catchupAndSubscribeTempoAutomation(() => this.#unfreezeAll()));
    }
    isFrozen(audioUnitBoxAdapter) {
        return this.#frozenAudioUnits.has(UUID.toString(audioUnitBoxAdapter.uuid));
    }
    isFrozenUuid(uuid) {
        return this.#frozenAudioUnits.has(UUID.toString(uuid));
    }
    subscribe(observer) { return this.#notifier.subscribe(observer); }
    hasSidechainDependents(audioUnitBoxAdapter) {
        const targetAddresses = [];
        for (const output of audioUnitBoxAdapter.labeledAudioOutputs()) {
            targetAddresses.push(output.address);
        }
        if (targetAddresses.length === 0) {
            return false;
        }
        const edges = audioUnitBoxAdapter.box.graph.edges();
        for (const otherUnit of this.#project.rootBoxAdapter.audioUnits.adapters()) {
            if (UUID.equals(otherUnit.uuid, audioUnitBoxAdapter.uuid)) {
                continue;
            }
            for (const effect of otherUnit.audioEffects.adapters()) {
                for (const [_, target] of edges.outgoingEdgesOf(effect.box)) {
                    if (targetAddresses.some(addr => addr.equals(target))) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    async freeze(audioUnitBoxAdapter) {
        const { engine } = this.#project;
        if (this.hasSidechainDependents(audioUnitBoxAdapter)) {
            await RuntimeNotifier.info({
                headline: "Cannot Freeze",
                message: "This audio unit is used as a sidechain source by another device."
            });
            return;
        }
        const audioUnitUuid = UUID.toString(audioUnitBoxAdapter.uuid);
        const exportConfiguration = {
            [audioUnitUuid]: {
                includeAudioEffects: true,
                includeSends: false,
                useInstrumentOutput: false,
                skipChannelStrip: true,
                fileName: "freeze"
            }
        };
        const copiedProject = this.#project.copy();
        const abortController = new AbortController();
        const progress = new DefaultObservableValue(0.0);
        const dialog = RuntimeNotifier.progress({
            headline: "Freezing AudioUnit...",
            progress,
            cancel: () => abortController.abort()
        });
        const renderResult = await Promises.tryCatch(OfflineEngineRenderer.start(copiedProject, Option.wrap(exportConfiguration), progress, abortController.signal, engine.sampleRate));
        if (renderResult.status === "rejected") {
            dialog.terminate();
            if (!Errors.isAbort(renderResult.error)) {
                await RuntimeNotifier.info({ headline: "Freeze Failed", message: String(renderResult.error) });
            }
            return;
        }
        dialog.terminate();
        const audioData = renderResult.value;
        engine.setFrozenAudio(audioUnitBoxAdapter.uuid, audioData);
        const { regionSelection, userEditingManager } = this.#project;
        const frozenRegions = regionSelection.selected()
            .filter(region => region.trackBoxAdapter
            .mapOr(track => UUID.equals(track.audioUnit.address.uuid, audioUnitBoxAdapter.uuid), false));
        if (frozenRegions.length > 0) {
            regionSelection.deselect(...frozenRegions);
        }
        userEditingManager.timeline.get().ifSome(vertex => {
            const regionUuid = vertex.box.address.uuid;
            for (const track of audioUnitBoxAdapter.tracks.values()) {
                if (track.regions.collection.asArray().some(region => UUID.equals(region.uuid, regionUuid))) {
                    userEditingManager.timeline.clear();
                    break;
                }
            }
        });
        this.#project.captureDevices.get(audioUnitBoxAdapter.uuid)
            .ifSome(capture => capture.armed.setValue(false));
        const deletionSubscription = audioUnitBoxAdapter.box.subscribeDeletion(() => this.#removeFrozen(audioUnitBoxAdapter.uuid, audioUnitUuid));
        this.#frozenAudioUnits.set(audioUnitUuid, { audioData, deletionSubscription });
        this.#notifier.notify(audioUnitBoxAdapter.uuid);
    }
    unfreeze(audioUnitBoxAdapter) {
        this.#removeFrozen(audioUnitBoxAdapter.uuid, UUID.toString(audioUnitBoxAdapter.uuid));
    }
    terminate() {
        this.#terminator.terminate();
        for (const [key, entry] of this.#frozenAudioUnits) {
            entry.deletionSubscription.terminate();
            this.#project.engine.setFrozenAudio(UUID.parse(key), null);
        }
        this.#frozenAudioUnits.clear();
    }
    #unfreezeAll() {
        const keys = Array.from(this.#frozenAudioUnits.keys());
        for (const key of keys) {
            this.#removeFrozen(UUID.parse(key), key);
        }
    }
    #removeFrozen(uuid, key) {
        const entry = this.#frozenAudioUnits.get(key);
        if (entry === undefined) {
            return;
        }
        entry.deletionSubscription.terminate();
        this.#project.engine.setFrozenAudio(uuid, null);
        this.#frozenAudioUnits.delete(key);
        this.#notifier.notify(uuid);
    }
}
