import { Arrays, isDefined, panic, RuntimeNotifier, safeExecute, Terminator, UUID } from "@opendaw/lib-std";
import { BoxEditing, DeleteUpdate, NewUpdate } from "@opendaw/lib-box";
import { ApparatDeviceBox, AudioFileBox, AudioRegionBox, SpielwerkDeviceBox, WerkstattDeviceBox } from "@opendaw/studio-boxes";
import { AudioUnitBoxAdapter, BoxAdapters, DeviceBoxUtils, Devices, isVertexOfBox, ParameterFieldAdapters, ProjectSkeleton, RegionAdapters, RootBoxAdapter, ScriptCompiler, TimelineBoxAdapter, UnionBoxTypes, UserEditingManager, VaryingTempoMap, VertexSelection } from "@opendaw/studio-adapters";
import { LiveStreamReceiver } from "@opendaw/lib-fusion";
import { Mixer } from "../Mixer";
import { ProjectApi } from "./ProjectApi";
import { ProjectMigration } from "./ProjectMigration";
import { CaptureDevices, Recording } from "../capture";
import { EngineFacade } from "../EngineFacade";
import { MidiDevices, MIDILearning } from "../midi";
import { ProjectValidation } from "./ProjectValidation";
import { TimeBase } from "@opendaw/lib-dsp";
import { MidiData } from "@opendaw/lib-midi";
import { StudioPreferences } from "../StudioPreferences";
import { RegionOverlapResolver, TimelineFocus } from "../ui";
import { SampleStorage } from "../samples";
import { AudioUnitFreeze } from "../AudioUnitFreeze";
// Main Entry Point for a Project
export class Project {
    static new(env, options) {
        const createDefaultUser = options?.noDefaultUser !== true;
        const createOutputMaximizer = StudioPreferences.settings.engine["auto-create-output-maximizer"];
        const { boxGraph, mandatoryBoxes } = ProjectSkeleton.empty({
            createOutputMaximizer,
            createDefaultUser
        });
        const project = new Project(env, boxGraph, mandatoryBoxes);
        if (createDefaultUser) {
            project.follow(mandatoryBoxes.userInterfaceBoxes[0]);
        }
        return project;
    }
    static load(env, arrayBuffer) {
        return this.fromSkeleton(env, ProjectSkeleton.decode(arrayBuffer));
    }
    static async loadAnyVersion(env, arrayBuffer) {
        console.debug("loadAnyVersion");
        const skeleton = ProjectSkeleton.decode(arrayBuffer);
        await ProjectMigration.migrate(env, skeleton);
        return this.fromSkeleton(env, skeleton);
    }
    static fromSkeleton(env, skeleton, followFirstUser = true) {
        ProjectValidation.validate(skeleton);
        const project = new Project(env, skeleton.boxGraph, skeleton.mandatoryBoxes);
        if (followFirstUser) {
            project.follow(project.userInterfaceBoxes[0]);
        }
        return project;
    }
    #terminator = new Terminator();
    #sampleRegistrations;
    #userCreatedSamples = UUID.newSet(uuid => uuid);
    #env;
    boxGraph;
    rootBox;
    userInterfaceBoxes;
    primaryAudioBusBox;
    primaryAudioUnitBox;
    timelineBox;
    api;
    captureDevices;
    editing;
    selection;
    deviceSelection;
    regionSelection;
    boxAdapters;
    userEditingManager;
    parameterFieldAdapters;
    liveStreamReceiver;
    midiLearning;
    mixer;
    tempoMap;
    overlapResolver;
    timelineFocus;
    engine = new EngineFacade();
    audioUnitFreeze;
    #rootBoxAdapter;
    #timelineBoxAdapter;
    constructor(env, boxGraph, { rootBox, userInterfaceBoxes, primaryAudioBusBox, primaryAudioUnitBox, timelineBox }) {
        this.#env = env;
        this.boxGraph = boxGraph;
        this.rootBox = rootBox;
        this.userInterfaceBoxes = userInterfaceBoxes;
        this.primaryAudioBusBox = primaryAudioBusBox;
        this.primaryAudioUnitBox = primaryAudioUnitBox;
        this.timelineBox = timelineBox;
        this.api = new ProjectApi(this);
        this.editing = env.createEditing?.(this.boxGraph) ?? new BoxEditing(this.boxGraph);
        this.selection = new VertexSelection(this.editing, this.boxGraph);
        this.parameterFieldAdapters = new ParameterFieldAdapters();
        this.boxAdapters = this.#terminator.own(new BoxAdapters(this));
        this.deviceSelection = this.#terminator.own(this.selection.createFilteredSelection(isVertexOfBox(DeviceBoxUtils.isDeviceBox), {
            fx: (adapter) => adapter.box,
            fy: vertex => this.boxAdapters.adapterFor(vertex.box, Devices.isAny)
        }));
        this.regionSelection = this.#terminator.own(this.selection.createFilteredSelection(isVertexOfBox(UnionBoxTypes.isRegionBox), {
            fx: (adapter) => adapter.box,
            fy: vertex => RegionAdapters.for(this.boxAdapters, vertex.box)
        }));
        this.#timelineBoxAdapter = this.boxAdapters.adapterFor(this.timelineBox, TimelineBoxAdapter);
        this.tempoMap = this.#terminator.own(new VaryingTempoMap(this.#timelineBoxAdapter));
        this.userEditingManager = new UserEditingManager(this.editing);
        this.liveStreamReceiver = this.#terminator.own(new LiveStreamReceiver());
        this.midiLearning = this.#terminator.own(new MIDILearning(this));
        this.captureDevices = this.#terminator.own(new CaptureDevices(this));
        this.#rootBoxAdapter = this.boxAdapters.adapterFor(this.rootBox, RootBoxAdapter);
        this.mixer = new Mixer(this.#rootBoxAdapter.audioUnits);
        this.overlapResolver = new RegionOverlapResolver(this.editing, this.api, this.boxAdapters);
        this.timelineFocus = this.#terminator.own(new TimelineFocus());
        this.audioUnitFreeze = this.#terminator.own(new AudioUnitFreeze(this));
        console.debug(`Project was created on ${this.rootBoxAdapter.created.toString()}`);
        this.#sampleRegistrations = UUID.newSet(({ uuid }) => uuid);
        for (const box of this.boxGraph.boxes()) {
            if (box instanceof AudioFileBox) {
                this.#registerSample(box.address.uuid);
            }
        }
        this.#terminator.own(this.boxGraph.subscribeToAllUpdates({
            onUpdate: (update) => {
                if (update instanceof NewUpdate && update.name === AudioFileBox.ClassName) {
                    this.#registerSample(update.uuid);
                }
                else if (update instanceof DeleteUpdate && update.name === AudioFileBox.ClassName) {
                    this.#unregisterSample(update.uuid);
                    this.#deleteUserCreatedSample(update.uuid).finally();
                }
            }
        }));
    }
    startAudioWorklet(restart, options) {
        console.debug(`start AudioWorklet`);
        const audioContext = this.#env.audioWorklets.context;
        const loadScript = (config, deviceBox) => ScriptCompiler.create(config).load(audioContext, deviceBox).catch(reason => console.warn(`Failed to load script device ${UUID.toString(deviceBox.address.uuid)}:`, reason));
        for (const box of this.boxGraph.boxes()) {
            if (box instanceof ApparatDeviceBox) {
                loadScript({
                    headerTag: "apparat",
                    registryName: "apparatProcessors",
                    functionName: "apparat"
                }, box).finally();
            }
            else if (box instanceof WerkstattDeviceBox) {
                loadScript({
                    headerTag: "werkstatt",
                    registryName: "werkstattProcessors",
                    functionName: "werkstatt"
                }, box).finally();
            }
            else if (box instanceof SpielwerkDeviceBox) {
                loadScript({
                    headerTag: "spielwerk",
                    registryName: "spielwerkProcessors",
                    functionName: "spielwerk"
                }, box).finally();
            }
        }
        const lifecycle = this.#terminator.spawn();
        const worklet = lifecycle.own(this.#env.audioWorklets.createEngine({ project: this, options }));
        const handler = async (event) => {
            console.warn(event);
            // we will only accept the first error
            worklet.removeEventListener("error", handler);
            worklet.removeEventListener("processorerror", handler);
            lifecycle.terminate();
            await safeExecute(restart?.unload, event);
            safeExecute(restart?.load, this.startAudioWorklet(restart));
        };
        worklet.addEventListener("error", handler);
        worklet.addEventListener("processorerror", handler);
        worklet.connect(worklet.context.destination, 0);
        this.engine.setWorklet(worklet);
        return worklet;
    }
    handleCpuOverload() {
        if (!StudioPreferences.settings.engine["stop-playback-when-overloading"]) {
            return;
        }
        this.engine.sleep();
        RuntimeNotifier.info({
            headline: "CPU Overload Detected",
            message: "Playback has been stopped. Try removing heavy plugins or effects."
        }).finally();
    }
    startRecording(countIn = true) {
        this.engine.assertWorklet();
        if (Recording.isRecording) {
            return;
        }
        Recording.start(this, countIn).finally();
    }
    stopRecording() {
        this.engine.stopRecording();
    }
    isRecording() { return Recording.isRecording; }
    follow(box) {
        this.userEditingManager.follow(box);
        this.midiLearning.followUser(box.midiControllers);
        this.selection.switch(box.selection);
    }
    own(terminable) { return this.#terminator.own(terminable); }
    ownAll(...terminables) { return this.#terminator.ownAll(...terminables); }
    spawn() { return this.#terminator.spawn(); }
    get env() { return this.#env; }
    get rootBoxAdapter() { return this.#rootBoxAdapter; }
    get timelineBoxAdapter() { return this.#timelineBoxAdapter; }
    get sampleManager() { return this.#env.sampleManager; }
    get soundfontManager() { return this.#env.soundfontManager; }
    get clipSequencing() { return panic("Only available in audio context"); }
    get isAudioContext() { return false; }
    get isMainThread() { return true; }
    get primaryAudioUnitBoxAdapter() {
        return this.boxAdapters.adapterFor(this.primaryAudioUnitBox, AudioUnitBoxAdapter);
    }
    get liveStreamBroadcaster() { return panic("Only available in audio context"); }
    get skeleton() {
        return {
            boxGraph: this.boxGraph,
            mandatoryBoxes: {
                rootBox: this.rootBox,
                timelineBox: this.timelineBox,
                primaryAudioBusBox: this.primaryAudioBusBox,
                primaryAudioUnitBox: this.primaryAudioUnitBox,
                userInterfaceBoxes: this.userInterfaceBoxes
            }
        };
    }
    receivedMIDIFromEngine(midiDeviceId, data, relativeTimeInMs) {
        const debug = false;
        if (debug) {
            console.debug("receivedMIDIFromEngine", MidiData.debug(data), relativeTimeInMs);
        }
        const timestamp = performance.now() + relativeTimeInMs;
        MidiDevices.findOutputDeviceById(midiDeviceId).ifSome(midiOutputDevice => {
            try {
                midiOutputDevice?.send(data, timestamp);
            }
            catch (reason) {
                console.warn("Failed to send MIDI message", reason);
            }
        });
    }
    collectSampleUUIDs() {
        return this.boxGraph.boxes()
            .filter(box => box.accept({ visitAudioFileBox: (_box) => true }))
            .map(box => box.address.uuid);
    }
    restartRecording() {
        if (this.engine.isRecording.getValue()) {
            const countingIn = Recording.wasCountingIn();
            this.engine.stopRecording();
            this.editing.modify(() => this.captureDevices.filterArmed()
                .forEach(capture => {
                capture.recordedRegions().forEach(region => region.box.delete());
                capture.clearRecordedRegions();
            }), false);
            this.engine.stop(true);
            this.engine.setPosition(Recording.wasStartingAt());
            const subscription = this.engine.isRecording.catchupAndSubscribe(owner => {
                if (!owner.getValue()) {
                    queueMicrotask(() => subscription.terminate());
                    this.startRecording(countingIn);
                }
            });
        }
        else {
            this.startRecording();
        }
    }
    toArrayBuffer() { return ProjectSkeleton.encode(this.boxGraph); }
    copy(env) {
        return Project.load({ ...this.#env, ...env }, this.toArrayBuffer());
    }
    invalid() {
        const now = performance.now();
        const result = this.boxGraph.boxes().some(box => box.accept({
            visitTrackBox: (box) => {
                for (const [current, next] of Arrays.iterateAdjacent(box.regions.pointerHub.incoming()
                    .map(({ box }) => UnionBoxTypes.asRegionBox(box))
                    .sort(({ position: a }, { position: b }) => a.getValue() - b.getValue()))) {
                    if (current instanceof AudioRegionBox && current.timeBase.getValue() === TimeBase.Seconds) {
                        return false;
                    }
                    if (current.position.getValue() + current.duration.getValue() > next.position.getValue()) {
                        return true;
                    }
                }
                return false;
            }
        }) ?? false);
        if (performance.now() - now > 5) {
            console.warn("Evaluation of invalid project takes more than 5ms");
        }
        return result;
    }
    lastRegionAction() {
        return this.rootBoxAdapter.audioUnits.adapters()
            .flatMap(audioUnitAdapter => audioUnitAdapter.tracks.values()
            .map(trackAdapter => trackAdapter.regions.collection.asArray().at(-1)))
            .filter(isDefined).reduce((position, region) => Math.max(position, region.complete), 0);
    }
    trackUserCreatedSample(uuid) {
        this.#userCreatedSamples.add(uuid);
    }
    terminate() {
        this.#sampleRegistrations.forEach(({ terminable }) => terminable.terminate());
        this.#sampleRegistrations.clear();
        this.#terminator.terminate();
    }
    #registerSample(uuid) {
        const terminable = this.sampleManager.register(uuid);
        this.#sampleRegistrations.add({ uuid, terminable });
    }
    #unregisterSample(uuid) {
        this.#sampleRegistrations.removeByKey(uuid).terminable.terminate();
    }
    async #deleteUserCreatedSample(uuid) {
        if (!this.#userCreatedSamples.hasKey(uuid)) {
            return;
        }
        this.#userCreatedSamples.removeByKey(uuid);
        const autoDelete = StudioPreferences.settings.storage["auto-delete-orphaned-samples"];
        if (!autoDelete && await RuntimeNotifier.approve({
            headline: "Keep Sample?",
            message: "The sample is no longer used. Do you want to keep it in storage? Deletion of samples cannot be undone!",
            approveText: "Keep",
            cancelText: "Delete"
        })) {
            return;
        }
        SampleStorage.get().deleteItem(uuid).catch((reason) => console.warn("Failed to delete sample from storage", reason));
    }
}
