import '../../assert/js/assert.js';
import '../../globals.js';
// Copyright 2023-2024, University of Colorado Boulder
/**
 * API Context for description plugins.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import CallbackTimer from '../../axon/js/CallbackTimer.js';
import DerivedProperty from '../../axon/js/DerivedProperty.js';
import Multilink from '../../axon/js/Multilink.js';
import TinyProperty from '../../axon/js/TinyProperty.js';
import LocalizedString from '../../chipper/js/browser/LocalizedString.js';
import dotRandom from '../../dot/js/dotRandom.js';
import { Node } from '../../scenery/js/imports.js';
import DescriptionRegistry from '../../tandem/js/DescriptionRegistry.js';
import Utterance from '../../utterance-queue/js/Utterance.js';
import localeProperty from './i18n/localeProperty.js';
import joist from './joist.js';
class DescriptionContext {
    constructor() {
        this.links = [];
        this.listens = [];
        this.assignments = [];
        this.propertyAssignments = [];
        this.multilinks = [];
        this.disposables = [];
    }
    get(tandemID) {
        return DescriptionRegistry.map.get(tandemID) || null;
    }
    link(property, listener) {
        // TS just... lets us do this?
        property.link(listener);
        this.links.push(new Link(property, listener));
    }
    lazyLink(property, listener) {
        // TS just... lets us do this?
        property.lazyLink(listener);
        this.links.push(new Link(property, listener));
    }
    unlink(property, listener) {
        property.unlink(listener);
        const index = this.links.findIndex(link => link.property === property && link.listener === listener);
        assert && assert(index >= 0);
        this.links.splice(index, 1);
    }
    createDerivedProperty(dependencies, derivation) {
        const derivedProperty = DerivedProperty.deriveAny(dependencies, derivation);
        this.disposables.push(derivedProperty);
        return derivedProperty;
    }
    multilink(dependencies, callback) {
        const multilink = Multilink.multilinkAny(dependencies, callback);
        this.multilinks.push(multilink);
        return multilink;
    }
    addListener(emitter, listener) {
        emitter.addListener(listener);
        this.listens.push(new Listen(emitter, listener));
    }
    removeListener(emitter, listener) {
        emitter.removeListener(listener);
        const index = this.listens.findIndex(listen => listen.emitter === emitter && listen.listener === listener);
        assert && assert(index >= 0);
        this.listens.splice(index, 1);
    }
    nodeSet(node, property, value) {
        const index = this.assignments.findIndex(assignment => assignment.target === node && assignment.property === property);
        if (index < 0) {
            this.assignments.push(new Assignment(node, property, node[property]));
        }
        // eslint-disable-next-line phet/no-simple-type-checking-assertions
        assert && assert(typeof property === 'string', 'Node property name for the set should be a string');
        // @ts-expect-error
        node[property] = value;
    }
    propertySet(property, value) {
        const index = this.propertyAssignments.findIndex(assignment => assignment.property === property);
        if (index < 0) {
            this.propertyAssignments.push(new PropertyAssignment(property, property.value));
        }
        property.value = value;
    }
    createCallbackTimer(options) {
        const callbackTimer = new CallbackTimer(options);
        this.disposables.push(callbackTimer);
        return callbackTimer;
    }
    createUtterance(options) {
        const utterance = new Utterance(options);
        this.disposables.push(utterance);
        return utterance;
    }
    /**
     * Creates a Node through the context.
     *
     * TODO: Consider making the tagName required for this context? See https://github.com/phetsims/joist/issues/941
     */
    createNode(options) {
        const node = new Node(options);
        this.disposables.push(node);
        return node;
    }
    dispose() {
        // NOTE: can links/listens be tied to a tandem/object? So that if we "remove" the object, we will assume it's disposed?
        while (this.links.length) {
            const link = this.links.pop();
            if (!link.property.isDisposed) {
                link.property.unlink(link.listener);
            }
        }
        while (this.listens.length) {
            const listen = this.listens.pop();
            // @ts-expect-error
            if (!listen.emitter.isDisposed) {
                listen.emitter.removeListener(listen.listener);
            }
        }
        while (this.assignments.length) {
            const assignment = this.assignments.pop();
            if (!assignment.target.isDisposed) {
                // @ts-expect-error
                assignment.target[assignment.property] = assignment.initialValue;
            }
        }
        while (this.propertyAssignments.length) {
            const assignment = this.propertyAssignments.pop();
            if (!assignment.property.isDisposed) {
                assignment.property.value = assignment.initialValue;
            }
        }
        while (this.multilinks.length) {
            const multilink = this.multilinks.pop();
            // @ts-expect-error TODO how to support this? https://github.com/phetsims/joist/issues/941
            if (!multilink.isDisposed) {
                multilink.dispose();
            }
        }
        // Dispose after disconnecting assignments so that everything is still usable when disconnecting.
        while (this.disposables.length) {
            const disposable = this.disposables.pop();
            disposable.dispose();
        }
    }
    static startupComplete() {
        DescriptionContext.isStartupCompleteProperty.value = true;
        localeProperty.link(() => this.reload());
        DescriptionRegistry.addedEmitter.addListener((tandemID, obj) => {
            const logic = this.logicProperty.value;
            if (this.activeContextProperty.value && logic) {
                logic.added(tandemID, obj);
            }
        });
        DescriptionRegistry.removedEmitter.addListener((tandemID, obj) => {
            const logic = this.logicProperty.value;
            if (this.activeContextProperty.value && logic) {
                logic.removed(tandemID, obj);
            }
        });
    }
    static reload() {
        // If we haven't started up yet, don't do anything (we'll reload when we start up).
        if (!this.isStartupCompleteProperty.value) {
            return;
        }
        if (DescriptionContext.activeLogic) {
            DescriptionContext.activeLogic.dispose();
            DescriptionContext.activeLogic = null;
        }
        if (this.activeContextProperty.value) {
            this.activeContextProperty.value.dispose();
        }
        const logic = this.logicProperty.value;
        if (logic === null) {
            return;
        }
        const fallbackLocales = LocalizedString.getLocaleFallbacks();
        const strings = {};
        let addedStrings = false;
        // Search in locale fallback order for the best description strings to use. We'll pull out each individual
        // function with fallback.
        for (let i = fallbackLocales.length - 1; i >= 0; i--) {
            const locale = fallbackLocales[i];
            if (DescriptionContext.stringsMap.has(locale)) {
                addedStrings = true;
                const localeStrings = DescriptionContext.stringsMap.get(locale);
                for (const key of Object.keys(localeStrings)) {
                    // @ts-expect-error
                    strings[key] = localeStrings[key];
                }
            }
        }
        if (!addedStrings) {
            return;
        }
        this.activeContextProperty.value = new DescriptionContext();
        DescriptionContext.activeLogic = logic;
        logic.launch(this.activeContextProperty.value, strings);
    }
    static registerStrings(strings) {
        const needsReload = LocalizedString.getLocaleFallbacks().includes(strings.locale);
        DescriptionContext.stringsMap.set(strings.locale, strings);
        if (needsReload) {
            DescriptionContext.reload();
        }
        return strings;
    }
    static registerLogic(logic) {
        DescriptionContext.logicProperty.value = logic;
        DescriptionContext.reload();
        return logic;
    }
    static async externalLoad(str) {
        const dataURI = `data:text/javascript;base64,${btoa(`${dotRandom.nextDouble()};${str}`)}`;
        try {
            (await import(dataURI)).default();
            return null;
        }
        catch (e) {
            return new ExternalLoadError(e, dataURI);
        }
    }
}
// What is available and registered
DescriptionContext.stringsMap = new Map();
DescriptionContext.logicProperty = new TinyProperty(null);
DescriptionContext.isStartupCompleteProperty = new TinyProperty(false);
DescriptionContext.activeContextProperty = new TinyProperty(null);
DescriptionContext.activeLogic = null; // so we can control disposal
export default DescriptionContext;
export class ExternalLoadError extends Error {
    constructor(error, dataURI) {
        // NOTE: this is a guard for the above cast to Error.
        // eslint-disable-next-line phet/no-simple-type-checking-assertions
        assert && assert(error instanceof Error);
        super(error.message);
        this.error = error;
        let stack = error.stack;
        let line = 0;
        let column = 0;
        if (stack && stack.includes(dataURI)) {
            stack = stack.slice(stack.indexOf(dataURI) + dataURI.length);
            // Parse the first two numbers out of the stack string. It will look like ":10:15)\n" or ":10:15\n", etc.
            const match = stack.match(/:(\d+):(\d+)/);
            if (match) {
                line = parseInt(match[1], 10);
                column = parseInt(match[2], 10);
            }
        }
        this.line = line;
        this.column = column;
    }
}
class Link {
    constructor(property, listener) {
        this.property = property;
        this.listener = listener;
    }
}
class Listen {
    constructor(emitter, listener) {
        this.emitter = emitter;
        this.listener = listener;
    }
}
class Assignment {
    constructor(target, property, initialValue) {
        this.target = target;
        this.property = property;
        this.initialValue = initialValue;
    }
}
class PropertyAssignment {
    constructor(property, initialValue) {
        this.property = property;
        this.initialValue = initialValue;
    }
}
joist.register('DescriptionContext', DescriptionContext);
//# sourceMappingURL=DescriptionContext.js.map