import { ByteArrayInput, ByteArrayOutput, isDefined, isInstanceOf, isNotNull, Option, RuntimeNotifier } from "@opendaw/lib-std";
import { Pointers } from "@opendaw/studio-enums";
import { RootBox, TrackBox } from "@opendaw/studio-boxes";
import { DeviceBoxUtils, Devices } from "@opendaw/studio-adapters";
import { ClipboardUtils } from "../ClipboardUtils";
export var DevicesClipboard;
(function (DevicesClipboard) {
    const encodeMetadata = (metadata) => {
        const output = ByteArrayOutput.create();
        output.writeBoolean(metadata.hasInstrument);
        output.writeString(metadata.instrumentContent);
        output.writeInt(metadata.midiEffectCount);
        output.writeInt(metadata.midiEffectMaxIndex);
        output.writeInt(metadata.audioEffectCount);
        output.writeInt(metadata.audioEffectMaxIndex);
        return output.toArrayBuffer();
    };
    const decodeMetadata = (buffer) => {
        const input = new ByteArrayInput(buffer);
        return {
            hasInstrument: input.readBoolean(),
            instrumentContent: input.readString(),
            midiEffectCount: input.readInt(),
            midiEffectMaxIndex: input.readInt(),
            audioEffectCount: input.readInt(),
            audioEffectMaxIndex: input.readInt()
        };
    };
    DevicesClipboard.createHandler = ({ getEnabled, editing, selection, boxGraph, boxAdapters, getHost }) => {
        const isCopyable = (adapter) => adapter.box.tags.copyable !== false;
        const copyableSelected = () => selection.selected().filter(isCopyable);
        const copyDevices = () => {
            const selected = copyableSelected();
            if (selected.length === 0) {
                return Option.None;
            }
            let instrument = null;
            const midiEffects = [];
            const audioEffects = [];
            for (const adapter of selected) {
                if (adapter.type === "instrument") {
                    instrument = adapter;
                }
                else if (adapter.type === "midi-effect") {
                    midiEffects.push(adapter);
                }
                else if (adapter.type === "audio-effect") {
                    audioEffects.push(adapter);
                }
            }
            if (instrument === null && midiEffects.length === 0 && audioEffects.length === 0) {
                return Option.None;
            }
            midiEffects.sort((a, b) => a.indexField.getValue() - b.indexField.getValue());
            audioEffects.sort((a, b) => a.indexField.getValue() - b.indexField.getValue());
            const midiEffectMaxIndex = midiEffects.length > 0
                ? midiEffects[midiEffects.length - 1].indexField.getValue()
                : 0;
            const audioEffectMaxIndex = audioEffects.length > 0
                ? audioEffects[audioEffects.length - 1].indexField.getValue()
                : 0;
            const deviceBoxes = [
                ...(instrument !== null ? [instrument.box] : []),
                ...midiEffects.map(adapter => adapter.box),
                ...audioEffects.map(adapter => adapter.box)
            ];
            const dependencies = deviceBoxes.flatMap(box => {
                const ownedChildren = box.incomingEdges()
                    .filter(pointer => pointer.mandatory && !pointer.box.ephemeral
                    && !isDefined(pointer.box.resource))
                    .map(pointer => pointer.box);
                const mandatoryDeps = Array.from(boxGraph.dependenciesOf(box, {
                    alwaysFollowMandatory: true,
                    stopAtResources: true,
                    excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
                        || dep.name === RootBox.ClassName
                }).boxes).filter(dep => dep.resource !== "preserved");
                const preserved = [box, ...ownedChildren].flatMap(root => Array.from(boxGraph.dependenciesOf(root, {
                    alwaysFollowMandatory: true,
                    excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
                }).boxes).filter(dep => dep.resource === "preserved"));
                return [...ownedChildren, ...mandatoryDeps, ...preserved];
            });
            const trackContent = [];
            if (isNotNull(instrument)) {
                getHost().ifSome(host => {
                    const tracksField = host.audioUnitBoxAdapter().tracksField;
                    for (const pointer of tracksField.pointerHub.filter(Pointers.TrackCollection)) {
                        if (!isInstanceOf(pointer.box, TrackBox)) {
                            continue;
                        }
                        const track = pointer.box;
                        trackContent.push(track);
                        for (const regionPointer of track.regions.pointerHub.incoming()) {
                            trackContent.push(regionPointer.box);
                            const regionDeps = Array.from(boxGraph.dependenciesOf(regionPointer.box, {
                                alwaysFollowMandatory: true,
                                stopAtResources: true,
                                excludeBox: (dep) => dep.ephemeral
                                    || isInstanceOf(dep, TrackBox)
                                    || DeviceBoxUtils.isDeviceBox(dep)
                            }).boxes);
                            trackContent.push(...regionDeps);
                        }
                    }
                });
            }
            const allBoxes = [...deviceBoxes, ...dependencies, ...trackContent];
            const instrumentContent = isNotNull(instrument)
                ? instrument.box.tags.content ?? ""
                : "";
            const metadata = {
                hasInstrument: isNotNull(instrument),
                instrumentContent,
                midiEffectCount: midiEffects.length,
                midiEffectMaxIndex,
                audioEffectCount: audioEffects.length,
                audioEffectMaxIndex
            };
            const data = ClipboardUtils.serializeBoxes(allBoxes, encodeMetadata(metadata));
            return Option.wrap({ type: "devices", data });
        };
        return {
            canCopy: () => getEnabled() && copyableSelected().length > 0,
            canCut: () => getEnabled() && copyableSelected().length > 0,
            canPaste: (entry) => getEnabled() && entry.type === "devices",
            copy: copyDevices,
            cut: () => {
                const result = copyDevices();
                result.ifSome(() => {
                    const optHost = getHost();
                    if (optHost.isEmpty()) {
                        return;
                    }
                    const host = optHost.unwrap();
                    const selected = new Set(selection.selected().filter(adapter => adapter.type !== "instrument"));
                    const remainingMidi = host.midiEffects.adapters().filter(adapter => !selected.has(adapter));
                    const remainingAudio = host.audioEffects.adapters().filter(adapter => !selected.has(adapter));
                    editing.modify(() => {
                        selected.forEach(adapter => adapter.box.delete());
                        remainingMidi.forEach((adapter, index) => adapter.indexField.setValue(index));
                        remainingAudio.forEach((adapter, index) => adapter.indexField.setValue(index));
                    });
                });
                return result;
            },
            paste: (entry) => {
                if (entry.type !== "devices" || !getEnabled()) {
                    return;
                }
                const optHost = getHost();
                if (optHost.isEmpty()) {
                    return;
                }
                const host = optHost.unwrap();
                const metadata = decodeMetadata(ClipboardUtils.extractMetadata(entry.data));
                const selected = selection.selected();
                const selectedInstrument = selected.find(adapter => adapter.type === "instrument");
                const selectedMidiEffects = selected.filter(adapter => adapter.type === "midi-effect");
                const selectedAudioEffects = selected.filter(adapter => adapter.type === "audio-effect");
                let replaceInstrument = metadata.hasInstrument && isDefined(selectedInstrument)
                    && selectedInstrument.box.tags.copyable !== false;
                if (replaceInstrument && isDefined(selectedInstrument)) {
                    const selectedContent = selectedInstrument.box.tags.content;
                    if (isDefined(selectedContent) && metadata.instrumentContent !== ""
                        && selectedContent !== metadata.instrumentContent) {
                        RuntimeNotifier.info({
                            headline: "Incompatible Instrument",
                            message: `Cannot replace a '${selectedContent}' instrument with a '${metadata.instrumentContent}' instrument.`
                        }).finally();
                        replaceInstrument = false;
                    }
                }
                const midiInsertIndex = selectedMidiEffects.length > 0
                    ? selectedMidiEffects.reduce((max, adapter) => Math.max(max, adapter.indexField.getValue()), -1) + 1
                    : 0;
                const audioInsertIndex = selectedAudioEffects.length > 0
                    ? selectedAudioEffects.reduce((max, adapter) => Math.max(max, adapter.indexField.getValue()), -1) + 1
                    : 0;
                editing.modify(() => {
                    selection.deselectAll();
                    if (replaceInstrument && isDefined(selectedInstrument)) {
                        const tracksField = host.audioUnitBoxAdapter().tracksField;
                        for (const pointer of tracksField.pointerHub.filter(Pointers.TrackCollection)) {
                            if (isInstanceOf(pointer.box, TrackBox)) {
                                pointer.box.delete();
                            }
                        }
                        selectedInstrument.box.delete();
                    }
                    for (const adapter of host.midiEffects.adapters()) {
                        const currentIndex = adapter.indexField.getValue();
                        if (currentIndex >= midiInsertIndex) {
                            adapter.indexField.setValue(currentIndex + metadata.midiEffectCount);
                        }
                    }
                    for (const adapter of host.audioEffects.adapters()) {
                        const currentIndex = adapter.indexField.getValue();
                        if (currentIndex >= audioInsertIndex) {
                            adapter.indexField.setValue(currentIndex + metadata.audioEffectCount);
                        }
                    }
                    const boxes = ClipboardUtils.deserializeBoxes(entry.data, boxGraph, {
                        mapPointer: (pointer, address) => {
                            if (address.isEmpty()) {
                                return Option.None;
                            }
                            if (pointer.pointerType === Pointers.InstrumentHost && replaceInstrument) {
                                return Option.wrap(host.inputField.address);
                            }
                            if (pointer.pointerType === Pointers.MIDIEffectHost) {
                                return Option.wrap(host.midiEffectsField.address);
                            }
                            if (pointer.pointerType === Pointers.AudioEffectHost) {
                                return Option.wrap(host.audioEffectsField.address);
                            }
                            if (pointer.pointerType === Pointers.TrackCollection) {
                                return Option.wrap(host.audioUnitBoxAdapter().tracksField.address);
                            }
                            if (pointer.pointerType === Pointers.Automation && replaceInstrument) {
                                return Option.wrap(host.audioUnitBoxAdapter().address);
                            }
                            if (pointer.pointerType === Pointers.MIDIDevice) {
                                const rootBox = boxGraph.boxes()
                                    .find(box => isInstanceOf(box, RootBox));
                                if (isDefined(rootBox)) {
                                    return Option.wrap(rootBox.outputMidiDevices.address);
                                }
                            }
                            return Option.None;
                        },
                        excludeBox: box => {
                            if (replaceInstrument) {
                                return false;
                            }
                            if (DeviceBoxUtils.isInstrumentDeviceBox(box)) {
                                return true;
                            }
                            if (isInstanceOf(box, TrackBox)) {
                                return metadata.hasInstrument;
                            }
                            return false;
                        }
                    });
                    const deviceBoxes = boxes.filter(box => DeviceBoxUtils.isDeviceBox(box));
                    const newMidiEffects = deviceBoxes
                        .filter((box) => DeviceBoxUtils.isEffectDeviceBox(box) && box.tags.deviceType === "midi-effect")
                        .sort((a, b) => a.index.getValue() - b.index.getValue());
                    const newAudioEffects = deviceBoxes
                        .filter((box) => DeviceBoxUtils.isEffectDeviceBox(box) && box.tags.deviceType === "audio-effect")
                        .sort((a, b) => a.index.getValue() - b.index.getValue());
                    newMidiEffects.forEach((box, idx) => box.index.setValue(midiInsertIndex + idx));
                    newAudioEffects.forEach((box, idx) => box.index.setValue(audioInsertIndex + idx));
                    const tracksField = host.audioUnitBoxAdapter().tracksField;
                    const allTracks = tracksField.pointerHub.filter(Pointers.TrackCollection)
                        .filter(pointer => isInstanceOf(pointer.box, TrackBox))
                        .map(pointer => pointer.box)
                        .sort((trackA, trackB) => trackA.index.getValue() - trackB.index.getValue());
                    allTracks.forEach((track, idx) => track.index.setValue(idx));
                    selection.select(...deviceBoxes.map(box => boxAdapters.adapterFor(box, Devices.isAny)));
                });
            }
        };
    };
})(DevicesClipboard || (DevicesClipboard = {}));
