import {
    ConditionDefinition,
    FurnitureFloorRemoveEvent,
    FurnitureWallRemoveEvent,
    GetRoomEngine,
    GetSessionDataManager,
    IFurnitureData,
    IRoomObject,
    OpenMessageComposer,
    RoomObjectCategory,
    RoomObjectVariable,
    RoomSessionEvent,
    Triggerable,
    TriggerDefinition,
    UpdateActionMessageComposer,
    UpdateAddonMessageComposer,
    UpdateConditionMessageComposer,
    UpdateSelectorMessageComposer,
    UpdateTriggerMessageComposer,
    UpdateVariableMessageComposer,
    WiredActionDefinition,
    WiredAddonDefinition,
    WiredFurniActionEvent,
    WiredFurniAddonEvent,
    WiredFurniConditionEvent,
    WiredFurniSelectorEvent,
    WiredFurniTriggerEvent,
    WiredFurniVariableEvent,
    WiredOpenEvent,
    WiredSelectorDefinition,
    WiredVariableDefinition,
    WiredSaveSuccessEvent,
    WiredValidationErrorEvent
} from '@octane/renderer';
import { useCallback, useEffect, useRef, useState } from 'react';
import { registerSharedHook, useSharedHook } from '@/state/useSharedHook';
import {
    GetRoomSession,
    IsOwnerOfFloorFurniture,
    LocalizeText,
    localizeWiredError,
    localizeWithFallback,
    NotificationBubbleType,
    pasteTriggerableData,
    removeFurniFromWiredPicks,
    resolveWiredQuantifier,
    resetTriggerableData,
    SendMessageComposer,
    WiredClipboardEntry,
    wiredClipboardKeyOf,
    WiredFurniType,
    isDualFurniPickingMode,
    WiredSelectionVisualizer,
    wiredPickIdOf,
    wiredPickIndexOf,
    withoutWallPick,
    withServerDefaultIntParams,
    withTriggerableData
} from '../../api';
import { useMessageEvent, useOctaneEvent } from '../events';
import { useNotification } from '../notification';
import { useLiveState } from '../useLiveState';
import { useWiredTools } from '../wired-tools/useWiredTools';

/** English for server error keys a hotel's texts may not have yet. */
/** Whether a clicked floor furni may be picked, from its room object and furnidata. */
export type WiredFurniPickCheck = (roomObject: IRoomObject, furniData: IFurnitureData) => boolean;

const useWiredState = () => {
    const [trigger, setTrigger, triggerRef] = useLiveState<Triggerable>(null);
    const [intParams, setIntParams, intParamsRef] = useLiveState<number[]>([]);
    const [stringParam, setStringParam, stringParamRef] = useLiveState<string>('');
    const [furniIds, setFurniIds, furniIdsRef] = useLiveState<number[]>([]);
    const [actionDelay, setActionDelay, actionDelayRef] = useLiveState<number>(0);
    // Set by an editor that writes the official source fields and selector switches itself; until then
    // the save passes back what the box sent.
    const [, setSourceTypes, sourceTypesRef] = useLiveState<{ furni: number[]; users: number[] } | null>(null);
    // The official second pick list (stuffIds2); null sends back what the box sent.
    const [furniIds2, setFurniIds2, furniIds2Ref] = useLiveState<number[] | null>(null);
    // The official definition variable ids; null sends back what the box sent.
    const [, setVariableIds, variableIdsRef] = useLiveState<string[] | null>(null);
    const [, setSelectorOptions, selectorOptionsRef] = useLiveState<{ filter: boolean; invert: boolean } | null>(null);
    // The condition's all (0) / any (1) choice from the quantifier section; null sends back what the box sent.
    const [quantifierCode, setQuantifierCode, quantifierCodeRef] = useLiveState<number | null>(null);
    const [allowsFurni, setAllowsFurni] = useState<number>(WiredFurniType.STUFF_SELECTION_OPTION_NONE);
    const selectByType = false;
    const [neighborhoodTiles, setNeighborhoodTiles] = useState<{ x: number; y: number }[] | null>(null);
    const [neighborhoodInvert, setNeighborhoodInvert] = useState<boolean>(false);
    const [allowedInteractionTypes, setAllowedInteractionTypes] = useState<string[] | null>(null);
    const [allowedInteractionErrorKey, setAllowedInteractionErrorKey] = useState<string | null>(null);
    // A view's own test for a floor furni pick, next to the interaction names.
    const [allowedFurniCheck, setAllowedFurniCheckState] = useState<WiredFurniPickCheck | null>(null);
    const setAllowedFurniCheck = useCallback((check: WiredFurniPickCheck | null) => setAllowedFurniCheckState(() => check), []);
    const { showConfirm = null, simpleAlert = null, showSingleBubble = null } = useNotification();
    const { requestUserVariables = null, roomSettings = null } = useWiredTools();
    // The quick menu's clipboard: one entry per holder and code, kept for the session.
    const [clipboard, setClipboard] = useState<Map<string, WiredClipboardEntry>>(() => new Map());
    // "Save without closing": the next save success leaves the window open.
    const keepOpenAfterSaveRef = useRef(false);
    // The non-owner confirmation, once answered, holds for the rest of the session.
    const nonOwnerConfirmedRef = useRef(false);
    // Official "paste into": while on, opening another box of the same element saves the open
    // configuration into it instead of opening it.
    const [pasteIntoMode, setPasteIntoModeState] = useState(false);
    const pasteIntoModeRef = useRef(false);
    const setPasteIntoMode = useCallback((value: boolean) => {
        pasteIntoModeRef.current = value;
        setPasteIntoModeState(value);
    }, []);

    /**
     * Saves the open configuration. With a target, it goes into that box instead (paste into): the
     * target keeps its own furni picks.
     */
    // Each save says whether the window stays open: a save the server drops without an answer (more than 20
    // picks, as AIR) must not leave the next one open.
    const saveWired = (target: Triggerable = null, keepOpen = false) => {
        keepOpenAfterSaveRef.current = keepOpen;

        const save = (trigger: Triggerable) => {
            if (!trigger) return;

            const intParams = intParamsRef.current;
            const stringParam = stringParamRef.current;
            const furniIds = target ? (target.selectedItems ?? []) : furniIdsRef.current;
            const actionDelay = actionDelayRef.current;

            // Official save layout: the source types, variable ids and second picks go back as the box
            // sent them until its editor sets them itself.
            const furniSources = sourceTypesRef.current?.furni ?? trigger.furniSourceTypes ?? [];
            const userSources = sourceTypesRef.current?.users ?? trigger.userSourceTypes ?? [];
            const variableIds = variableIdsRef.current ?? trigger.variableIds ?? [];
            const furniIds2 = target ? (target.selectedItems2 ?? []) : (furniIds2Ref.current ?? trigger.selectedItems2 ?? []);
            const id = target ? target.id : trigger.id;

            // Addons, selectors and variables have their own official save packets.
            if (trigger instanceof WiredAddonDefinition) {
                SendMessageComposer(
                    new UpdateAddonMessageComposer(id, intParams, stringParam, furniIds, furniSources, userSources, variableIds, furniIds2)
                );
            } else if (trigger instanceof WiredSelectorDefinition) {
                SendMessageComposer(
                    new UpdateSelectorMessageComposer(
                        id,
                        intParams,
                        stringParam,
                        furniIds,
                        selectorOptionsRef.current?.filter ?? trigger.isFilter ?? false,
                        selectorOptionsRef.current?.invert ?? trigger.isInvert ?? false,
                        furniSources,
                        userSources,
                        variableIds,
                        furniIds2
                    )
                );
            } else if (trigger instanceof WiredVariableDefinition) {
                SendMessageComposer(
                    new UpdateVariableMessageComposer(id, intParams, stringParam, furniIds, furniSources, userSources, variableIds, furniIds2)
                );
            } else if (trigger instanceof WiredActionDefinition) {
                SendMessageComposer(
                    new UpdateActionMessageComposer(id, intParams, stringParam, furniIds, actionDelay, furniSources, userSources, variableIds, furniIds2)
                );
            } else if (trigger instanceof TriggerDefinition) {
                SendMessageComposer(
                    new UpdateTriggerMessageComposer(id, intParams, stringParam, furniIds, furniSources, userSources, variableIds, furniIds2)
                );
            } else if (trigger instanceof ConditionDefinition) {
                SendMessageComposer(
                    new UpdateConditionMessageComposer(
                        id,
                        intParams,
                        stringParam,
                        furniIds,
                        resolveWiredQuantifier(quantifierCodeRef.current, trigger.quantifierCode),
                        furniSources,
                        userSources,
                        variableIds,
                        furniIds2
                    )
                );
            }
        };

        const trigger = triggerRef.current;

        const isSelector = trigger instanceof WiredSelectorDefinition;
        const invert = selectorOptionsRef.current?.invert ?? (isSelector ? trigger.isInvert : false) ?? false;
        const filter = selectorOptionsRef.current?.filter ?? (isSelector ? trigger.isFilter : false) ?? false;

        // Official: a selector saved with Invert but without Filter asks first (only here, so every save
        // path asks once). Accepting any of these confirmations also counts as the non-owner confirmation.
        if (isSelector && invert && !filter) {
            showConfirm(
                localizeWithFallback(
                    'wiredfurni.danger.1.change.confirm.body',
                    'You selected "Invert" without filtering. This selects all furni/users that do NOT meet the requirements, which may be the whole room. Are you sure? (DANGEROUS!)'
                ),
                () => {
                    nonOwnerConfirmedRef.current = true;
                    save(trigger);
                },
                null,
                null,
                null,
                localizeWithFallback('wiredfurni.danger.1.change.confirm.title', 'Are you sure?')
            );
        } else if (!nonOwnerConfirmedRef.current && !IsOwnerOfFloorFurniture(target ? target.id : trigger.id)) {
            showConfirm(
                LocalizeText('wiredfurni.nonowner.change.confirm.body'),
                () => {
                    nonOwnerConfirmedRef.current = true;
                    save(trigger);
                },
                null,
                null,
                null,
                LocalizeText('wiredfurni.nonowner.change.confirm.title')
            );
        } else {
            save(trigger);
        }
    };

    const selectObjectForWired = (objectId: number, category: number) => {
        if (!trigger || !allowsFurni) return;

        // Official: furni picking is off while paste-into is on.
        if (pasteIntoModeRef.current) return;

        if (objectId <= 0) return;

        // Official: wall items are picked only when the box allows wall furni. A box the server sends no input
        // sources conf for keeps accepting them.
        const inputSources = trigger.inputSourcesConf;
        const hasInputSourcesConf = !!(inputSources?.furniAllowed?.length || inputSources?.userAllowed?.length);

        if (category === RoomObjectCategory.WALL && hasInputSourcesConf && !trigger.allowWallFurni) return;

        const getInteractionTypeName = (furniData: any): string => {
            if (!furniData) return null;

            const rawValue = furniData.interactionType ?? furniData.interactionTypeName ?? furniData.interactionTypeId;

            if (rawValue === undefined || rawValue === null) return null;
            if (typeof rawValue !== 'string') return null;

            return rawValue.toLowerCase();
        };

        const getComparableInteractionNames = (furniData: any): string[] => {
            if (!furniData) return [];

            const values = [
                getInteractionTypeName(furniData),
                typeof furniData.className === 'string' ? furniData.className.toLowerCase() : null,
                typeof furniData.fullName === 'string' ? furniData.fullName.toLowerCase() : null,
                typeof furniData.name === 'string' ? furniData.name.toLowerCase() : null
            ];

            return values.filter((value, index, array): value is string => !!value && array.indexOf(value) === index);
        };

        const matchesAllowedPattern = (value: string, pattern: string) => {
            const normalizedPattern = pattern.toLowerCase();

            if (normalizedPattern.endsWith('*')) return value.startsWith(normalizedPattern.slice(0, -1));

            return normalizedPattern === value;
        };

        const isAllowedInteraction = (furniData: any): boolean => {
            if (!allowedInteractionTypes || !allowedInteractionTypes.length) return true;

            const comparableNames = getComparableInteractionNames(furniData);
            if (!comparableNames.length) return true;

            return comparableNames.some((value) => allowedInteractionTypes.some((type) => !!type && matchesAllowedPattern(value, type)));
        };

        const handleDisallowedInteraction = () => {
            if (!allowedInteractionErrorKey) return;

            const message = /^[a-z0-9_.]+$/i.test(allowedInteractionErrorKey) ? LocalizeText(allowedInteractionErrorKey) : allowedInteractionErrorKey;

            simpleAlert(message, null, null, null, LocalizeText('wiredfurni.title'));
        };

        if (selectByType && category === RoomObjectCategory.FLOOR) {
            const roomId = GetRoomSession().roomId;
            const clickedObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.FLOOR);

            if (!clickedObject) return;

            const typeId = clickedObject.model.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID);
            const sourceFurniData = GetSessionDataManager().getFloorItemData(typeId);

            if (!sourceFurniData) return;
            if (!isAllowedInteraction(sourceFurniData)) {
                handleDisallowedInteraction();
                setFurniIds((prevValue) => {
                    if (!prevValue.includes(objectId)) return prevValue;

                    const remaining = prevValue.filter((id) => id !== objectId);

                    WiredSelectionVisualizer.hide(objectId);

                    return remaining;
                });

                return;
            }

            const matchFurniLine = sourceFurniData.furniLine;
            const matchName = sourceFurniData.name;

            const isSameGroup = (id: number): boolean => {
                const obj = GetRoomEngine().getRoomObject(roomId, id, RoomObjectCategory.FLOOR);
                if (!obj) return false;

                const tId = obj.model.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID);
                const fd = GetSessionDataManager().getFloorItemData(tId);
                if (!fd) return false;

                if (!isAllowedInteraction(fd)) return false;

                const furniLineMatch = matchFurniLine && matchFurniLine.length > 0 && fd.furniLine === matchFurniLine;
                return furniLineMatch || fd.name === matchName;
            };

            setFurniIds((prevValue) => {
                // ── Click on already-selected furni: deselect the whole group ──
                if (prevValue.includes(objectId)) {
                    const toRemove = prevValue.filter((id) => isSameGroup(id));
                    const remaining = prevValue.filter((id) => !toRemove.includes(id));

                    for (const id of toRemove) WiredSelectionVisualizer.hide(id);

                    return remaining;
                }

                // ── Select a new group ──────────────────────────────────────
                const allFloorObjects = GetRoomEngine().getRoomObjects(roomId, RoomObjectCategory.FLOOR);
                const newIds = [...prevValue];
                const limit = trigger.maximumItemSelectionCount;

                for (const obj of allFloorObjects) {
                    if (newIds.length >= limit) break;
                    if (obj.id < 0) continue;

                    const tId = obj.model.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID);
                    const fd = GetSessionDataManager().getFloorItemData(tId);
                    if (!fd) continue;
                    if (!isAllowedInteraction(fd)) continue;

                    const furniLineMatch = matchFurniLine && matchFurniLine.length > 0 && fd.furniLine === matchFurniLine;
                    const matches = furniLineMatch || fd.name === matchName;

                    if (matches && !newIds.includes(obj.id)) newIds.push(obj.id);
                }

                const addedIds = newIds.filter((id) => !prevValue.includes(id));
                // Tinted as the list being edited, as a single click is.
                for (const id of addedIds) WiredSelectionVisualizer.show(id);

                return newIds;
            });

            return;
        }

        if (category === RoomObjectCategory.FLOOR && ((allowedInteractionTypes && allowedInteractionTypes.length) || allowedFurniCheck)) {
            const roomId = GetRoomSession().roomId;
            const clickedObject = GetRoomEngine().getRoomObject(roomId, objectId, RoomObjectCategory.FLOOR);

            if (!clickedObject) return;

            const typeId = clickedObject.model.getValue<number>(RoomObjectVariable.FURNITURE_TYPE_ID);
            const sourceFurniData = GetSessionDataManager().getFloorItemData(typeId);

            if (!sourceFurniData) return;
            if (!isAllowedInteraction(sourceFurniData) || (allowedFurniCheck && !allowedFurniCheck(clickedObject, sourceFurniData))) {
                handleDisallowedInteraction();
                setFurniIds((prevValue) => {
                    if (!prevValue.includes(objectId)) return prevValue;

                    const remaining = prevValue.filter((id) => id !== objectId);

                    WiredSelectionVisualizer.hide(objectId);

                    return remaining;
                });

                return;
            }
        }

        // Official: a wall item is picked as its negative id. A wall pick a box stored before (its own id) is
        // still found, so clicking it unpicks it.
        const pickId = wiredPickIdOf(objectId, category);

        setFurniIds((prevValue) => {
            const newFurniIds = [...prevValue];

            const index = wiredPickIndexOf(prevValue, objectId, category);

            if (index >= 0) {
                const [removed] = newFurniIds.splice(index, 1);

                WiredSelectionVisualizer.hide(removed);
            } else if (newFurniIds.length < trigger.maximumItemSelectionCount) {
                newFurniIds.push(pickId);

                WiredSelectionVisualizer.show(pickId);
            }

            return newFurniIds;
        });
    };

    useMessageEvent<WiredOpenEvent>(WiredOpenEvent, (event) => {
        const parser = event.getParser();

        SendMessageComposer(new OpenMessageComposer(parser.stuffId));
    });

    useMessageEvent<WiredSaveSuccessEvent>(WiredSaveSuccessEvent, (event) => {
        const parser = event.getParser();

        if (roomSettings?.canInspect && requestUserVariables) requestUserVariables();

        if (keepOpenAfterSaveRef.current) {
            // Saved without closing: the picks stay highlighted and the window stays up.
            keepOpenAfterSaveRef.current = false;
            return;
        }

        WiredSelectionVisualizer.clearAllSelectionShaders();
        setTrigger(null);
    });

    /** The selector's filter/invert as its editor last set them, or as the box sent them. */
    const getSelectorOptions = useCallback((): { filter: boolean; invert: boolean } => {
        const current = triggerRef.current;
        const sent = current instanceof WiredSelectorDefinition ? { filter: !!current.isFilter, invert: !!current.isInvert } : { filter: false, invert: false };

        return selectorOptionsRef.current ?? sent;
    }, []);

    /** Saves the box and keeps the window open, so the next change starts from what was saved. */
    const saveWiredAndKeepOpen = useCallback(() => {
        saveWired(null, true);
    }, [saveWired]);

    /**
     * The open box as the editor has it now: what the view last pushed into the hook, and what the
     * box sent for anything its view does not set itself.
     */
    const currentTriggerableEntry = useCallback((current: Triggerable): WiredClipboardEntry => {
        const isSelector = current instanceof WiredSelectorDefinition;

        return {
            key: wiredClipboardKeyOf(current),
            intParams: [...(intParamsRef.current ?? [])],
            stringParam: stringParamRef.current ?? '',
            furniIds: [...(furniIdsRef.current ?? [])],
            delayInPulses: actionDelayRef.current ?? 0,
            furniIds2: [...(furniIds2Ref.current ?? current.selectedItems2 ?? [])],
            furniSourceTypes: [...(sourceTypesRef.current?.furni ?? current.furniSourceTypes ?? [])],
            userSourceTypes: [...(sourceTypesRef.current?.users ?? current.userSourceTypes ?? [])],
            variableIds: [...(variableIdsRef.current ?? current.variableIds ?? [])],
            quantifierCode: current instanceof ConditionDefinition ? resolveWiredQuantifier(quantifierCodeRef.current, current.quantifierCode) : 0,
            selectorFilter: selectorOptionsRef.current?.filter ?? (isSelector ? !!current.isFilter : false),
            selectorInvert: selectorOptionsRef.current?.invert ?? (isSelector ? !!current.isInvert : false)
        };
    }, []);

    /**
     * Copies the box's current settings (what the view just pushed into the hook) to the
     * clipboard slot of its holder and code: everything the official ClipboardWiredEntry holds.
     */
    const copyWiredToClipboard = useCallback(() => {
        const current = triggerRef.current;

        if (!current) return;

        const entry = currentTriggerableEntry(current);

        setClipboard((prevValue) => {
            const next = new Map(prevValue);

            next.set(entry.key, entry);

            return next;
        });
    }, [currentTriggerableEntry]);

    /** The clipboard entry that fits the open box, if any. */
    const clipboardEntry = trigger ? (clipboard.get(wiredClipboardKeyOf(trigger)) ?? null) : null;

    /**
     * Pastes the fitting clipboard entry into the open box. The views re-read the box because it
     * is a new object; "paste into" keeps the box's own furni picks and delay.
     */
    const pasteWiredFromClipboard = useCallback(
        (pasteInto: boolean = false) => {
            const current = triggerRef.current;

            if (!current) return;

            const entry = clipboard.get(wiredClipboardKeyOf(current));

            if (!entry) return;

            setTrigger(pasteTriggerableData(current, entry, pasteInto));
        },
        [clipboard, setTrigger]
    );

    /** Puts the open box back to its catalog defaults; nothing is saved until "ready". */
    const resetWiredToDefault = useCallback(() => {
        const current = triggerRef.current;

        if (!current) return;

        setTrigger(resetTriggerableData(current));
    }, [setTrigger]);

    /**
     * Drops every furni pick of the open box, from both lists (official clearPicks). The views keep
     * the second list themselves, so the box comes back without picks and the views re-read it; the
     * rest of what the view had pushed into the hook stays.
     */
    const clearWiredPicks = useCallback(() => {
        const current = triggerRef.current;

        if (!current) return;

        const entry = currentTriggerableEntry(current);

        WiredSelectionVisualizer.clearSelectionShaderFromFurni([...entry.furniIds, ...(entry.furniIds2 ?? [])]);
        setTrigger(withTriggerableData(current, { ...entry, furniIds: [], furniIds2: [] }));
    }, [currentTriggerableEntry, setTrigger]);

    /** Official close: the window goes and its picks lose their highlight. */
    const closeWired = useCallback(() => {
        if (!triggerRef.current) return;

        WiredSelectionVisualizer.clearAllSelectionShaders();
        setPasteIntoMode(false);
        setTrigger(null);
    }, [setPasteIntoMode, setTrigger]);

    // Official onRoomExit: leaving the room closes the editor.
    useOctaneEvent<RoomSessionEvent>(RoomSessionEvent.ENDED, () => closeWired());

    // Official stuffRemoved: a removed furni closes the editor when it is the open box, and otherwise
    // leaves both pick lists.
    useMessageEvent<FurnitureFloorRemoveEvent>(FurnitureFloorRemoveEvent, (event) => {
        const current = triggerRef.current;

        if (!current) return;

        const removedId = event.getParser().itemId;
        const result = removeFurniFromWiredPicks(current.id, furniIdsRef.current ?? [], furniIds2Ref.current ?? current.selectedItems2 ?? [], removedId);

        if (result.closesEditor) {
            closeWired();
            return;
        }

        if (!result.changed) return;

        WiredSelectionVisualizer.hide(removedId);

        // The lists change in place, so nothing the view holds unsaved is reset.
        setFurniIds(result.furniIds);

        if (furniIds2Ref.current !== null || result.furniIds2.length !== (current.selectedItems2 ?? []).length) setFurniIds2(result.furniIds2);
    });

    // A removed wall item leaves the picks in either form (negative as AIR picks it, or the id boxes stored before).
    useMessageEvent<FurnitureWallRemoveEvent>(FurnitureWallRemoveEvent, (event) => {
        const current = triggerRef.current;

        if (!current) return;

        const removedId = event.getParser().itemId;

        if (current.id === removedId) return;

        const furniIds = furniIdsRef.current ?? [];
        const furniIds2 = furniIds2Ref.current ?? current.selectedItems2 ?? [];
        const nextFurniIds = withoutWallPick(furniIds, removedId);
        const nextFurniIds2 = withoutWallPick(furniIds2, removedId);

        if (nextFurniIds.length === furniIds.length && nextFurniIds2.length === furniIds2.length) return;

        WiredSelectionVisualizer.hide(-removedId);
        WiredSelectionVisualizer.hide(removedId);
        setFurniIds(nextFurniIds);

        if (furniIds2Ref.current !== null || nextFurniIds2.length !== furniIds2.length) setFurniIds2(nextFurniIds2);
    });

    useMessageEvent<WiredValidationErrorEvent>(WiredValidationErrorEvent, (event) => {
        const parser = event.getParser();

        if (parser.info && parser.info.length) {
            const message = /^[a-z0-9_.]+$/i.test(parser.info) ? localizeWiredError(parser.info, parser.parameters ?? []) : parser.info;

            // Official: the save failed alert is titled wiredfurni.error.title.
            simpleAlert(message, null, null, null, localizeWithFallback('wiredfurni.error.title', 'Update failed'));
        }
    });

    /**
     * A box the server opened. In paste-into mode, another box of the same element gets the open
     * configuration saved into it (the open window stays); a different element is refused.
     */
    const openDefinition = (definition: Triggerable) => {
        const current = triggerRef.current;

        if (pasteIntoModeRef.current && current && definition && definition.id !== current.id) {
            if (wiredClipboardKeyOf(definition) === wiredClipboardKeyOf(current)) {
                saveWired(definition, true);
                showSingleBubble?.(localizeWithFallback('notification.wired.pasted_into', 'The settings were applied'), NotificationBubbleType.INFO);
            } else {
                showSingleBubble?.(
                    localizeWithFallback('notification.wired.pasted_into_fail', 'This configuration does not fit that wired box'),
                    NotificationBubbleType.INFO
                );
            }

            return;
        }

        // Official: every newly opened box starts with paste-into off.
        setPasteIntoMode(false);
        setTrigger(withServerDefaultIntParams(definition));
    };

    useMessageEvent<WiredFurniAddonEvent>(WiredFurniAddonEvent, (event) => openDefinition(event.getParser().definition));

    useMessageEvent<WiredFurniSelectorEvent>(WiredFurniSelectorEvent, (event) => openDefinition(event.getParser().definition));

    useMessageEvent<WiredFurniVariableEvent>(WiredFurniVariableEvent, (event) => openDefinition(event.getParser().definition));

    useMessageEvent<WiredFurniActionEvent>(WiredFurniActionEvent, (event) => openDefinition(event.getParser().definition));

    useMessageEvent<WiredFurniConditionEvent>(WiredFurniConditionEvent, (event) => openDefinition(event.getParser().definition));

    useMessageEvent<WiredFurniTriggerEvent>(WiredFurniTriggerEvent, (event) => openDefinition(event.getParser().definition));

    // While an editor is open its picks stay highlighted, even when a furni's sprites are rebuilt
    // (a state change, an animation); after it closes, late redraws are swept once more.
    useEffect(() => {
        if (!trigger) return;

        // Official RoomObjectHighLighter: a box with dual furni picking tints its first picks apart from the second.
        WiredSelectionVisualizer.setDualPickingMode(isDualFurniPickingMode(trigger.inputSourcesConf));

        const interval = setInterval(() => WiredSelectionVisualizer.refresh(), 250);

        return () => {
            clearInterval(interval);
            WiredSelectionVisualizer.setDualPickingMode(false);
            WiredSelectionVisualizer.setEditingPickList(1);
            setTimeout(() => {
                if (!triggerRef.current) WiredSelectionVisualizer.clearAllSelectionShaders();
            }, 300);
        };
    }, [trigger]);

    useEffect(() => {
        if (!trigger) return;

        return () => {
            WiredSelectionVisualizer.clearAllSelectionShaders();
            setIntParams([]);
            setStringParam('');
            setActionDelay(0);
            setFurniIds((prevValue) => {
                if (prevValue && prevValue.length) WiredSelectionVisualizer.clearSelectionShaderFromFurni(prevValue);

                return [];
            });
            setAllowsFurni(WiredFurniType.STUFF_SELECTION_OPTION_NONE);
            setNeighborhoodTiles(null);
            setNeighborhoodInvert(false);
            setSourceTypes(null);
            setFurniIds2(null);
            setVariableIds(null);
            setSelectorOptions(null);
            setQuantifierCode(null);
            setAllowedInteractionTypes(null);
            setAllowedInteractionErrorKey(null);
            setAllowedFurniCheckState(null);
        };
    }, [trigger]);

    return {
        trigger,
        setTrigger,
        intParams,
        setIntParams,
        stringParam,
        setStringParam,
        furniIds,
        setFurniIds,
        actionDelay,
        setActionDelay,
        setAllowsFurni,
        saveWired,
        saveWiredAndKeepOpen,
        clipboardEntry,
        copyWiredToClipboard,
        pasteWiredFromClipboard,
        pasteIntoMode,
        setPasteIntoMode,
        getSelectorOptions,
        resetWiredToDefault,
        clearWiredPicks,
        closeWired,
        furniIds2,
        selectObjectForWired,
        setNeighborhoodTiles,
        setNeighborhoodInvert,
        setSourceTypes,
        setFurniIds2,
        setVariableIds,
        setSelectorOptions,
        quantifierCode,
        setQuantifierCode,
        setAllowedInteractionTypes,
        setAllowedInteractionErrorKey,
        setAllowedFurniCheck
    };
};

export const useWired = () => useSharedHook(useWiredState);

registerSharedHook(useWiredState);
