import { isDevEnvironment } from "../engine/debug/index.js";
const argumentsBuffer = new Array();
/**
 * CallInfo represents a single callback method that can be invoked by the {@link EventList}.
 */
export class CallInfo {
    /**
     * When the CallInfo is enabled it will be invoked when the EventList is invoked
     */
    enabled = true;
    /**
     * The target object to invoke the method on OR the function to invoke
     */
    target;
    methodName;
    /**
     * The arguments to invoke this method with
     */
    arguments;
    get canClone() {
        return this.target instanceof Object;
    }
    constructor(target, methodName, args, enabled) {
        this.target = target;
        this.methodName = methodName || null;
        this.arguments = args;
        if (enabled != undefined)
            this.enabled = enabled;
    }
    invoke(...args) {
        if (this.enabled === false)
            return;
        // CallInfo can just contain a function
        if (typeof this.target === "function") {
            if (this.arguments) {
                argumentsBuffer.length = 0;
                // we pass the custom arguments first and then the event arguments (if any)
                // this is so that invoke("myEvent") will take precedence over the event arguments
                // see https://linear.app/needle/issue/NE-5507
                if (args !== undefined && args.length > 0)
                    argumentsBuffer.push(...args);
                argumentsBuffer.push(...this.arguments);
                this.target(...this.arguments);
                argumentsBuffer.length = 0;
            }
            else {
                this.target(...args);
            }
        }
        else if (this.methodName != null) {
            const method = this.target[this.methodName];
            // If the target is callable
            if (typeof method === "function") {
                if (this.arguments) {
                    argumentsBuffer.length = 0;
                    // we pass the custom arguments first and then the event arguments (if any)
                    // this is so that invoke("myEvent") will take precedence over the event arguments
                    // see https://linear.app/needle/issue/NE-5507
                    if (args !== undefined && args.length > 0)
                        argumentsBuffer.push(...args);
                    argumentsBuffer.push(...this.arguments);
                    method.call(this.target, ...argumentsBuffer);
                    argumentsBuffer.length = 0;
                }
                else {
                    method.call(this.target, ...args);
                }
            }
            // If the target is a property
            else {
                if (this.arguments) {
                    if (args !== undefined && args.length > 0)
                        this.target[this.methodName] = args[0];
                    else
                        this.target[this.methodName] = this.arguments[0];
                }
                else {
                    this.target[this.methodName] = args[0];
                }
            }
        }
    }
}
const isUpperCase = (string) => /^[A-Z]*$/.test(string);
export class EventListEvent extends Event {
    args;
}
/**
 * EventList manages a list of callbacks that can be invoked together.
 * Used for Unity-style events that can be configured in the editor (Unity or Blender).
 *
 * **Serialization:**
 * EventLists are serializable - callbacks configured in Unity/Blender will work at runtime.
 * Mark fields with `@serializable(EventList)` for editor support.
 *
 * **Usage patterns:**
 * - Button click handlers
 * - Animation events
 * - Custom component callbacks
 * - Scene loading events
 *
 * ![](https://cloud.needle.tools/-/media/P7bEKQvfgRUMTb2Wi1hWXg.png)
 * *Screenshot of a Unity component with an EventList field*
 *
 * ![](https://cloud.needle.tools/-/media/i2hi2OHfbaDyHyBL6Gt58A.png)
 * *Screenshot of a Blender component with an EventList field*
 *
 * @example Create and use an EventList
 * ```ts
 * // Define in your component
 * @serializable(EventList)
 * onClick: EventList = new EventList();
 *
 * // Add listeners
 * this.onClick.addEventListener(() => console.log("Clicked!"));
 *
 * // Invoke all listeners
 * this.onClick.invoke();
 * ```
 *
 * @example Listen with arguments
 * ```ts
 * const onScore = new EventList<{ points: number }>();
 * onScore.addEventListener(data => console.log("Scored:", data.points));
 * onScore.invoke({ points: 100 });
 * ```
 *
 * @category Events
 * @group Utilities
 * @see {@link CallInfo} for individual callback configuration
 * @see {@link Button} for UI button events
 */
export class EventList {
    /** checked during instantiate to create a new instance */
    isEventList = true;
    /**
     * @internal Used by the Needle Engine instantiate call to remap the event listeners to the new instance
     */
    __internalOnInstantiate(ctx) {
        const newMethods = new Array();
        for (let i = 0; i < this.methods.length; i++) {
            const method = this.methods[i];
            if (method.target instanceof Function) {
                // can not clone a function
            }
            else {
                const target = method.target;
                let key = target?.uuid;
                if (target) {
                    key = target.guid;
                }
                if (key) {
                    const newTarget = ctx[key];
                    if (newTarget) {
                        // remap the arguments to the new instance (e.g. if an object is passed as an argument to the event list and this object has been cloned we want to remap it to the clone)
                        const newArguments = method.arguments?.map(arg => {
                            if (arg instanceof Object && arg.uuid) {
                                return ctx[arg.uuid].clone;
                            }
                            else if (arg?.isComponent) {
                                return ctx[arg.guid].clone;
                            }
                            return arg;
                        });
                        newMethods.push(new CallInfo(newTarget.clone, method.methodName, newArguments, method.enabled));
                    }
                    else if (isDevEnvironment()) {
                        console.warn("Could not find target for event listener");
                    }
                }
            }
        }
        const newInstance = new EventList(newMethods);
        return newInstance;
    }
    target;
    key;
    // TODO: serialization should not take care of the args but instead give them to the eventlist directly
    // so we can handle passing them on here instead of in the serializer
    // this would also allow us to pass them on to the component EventTarget
    /** set an event target to try invoke the EventTarget dispatchEvent when this EventList is invoked */
    setEventTarget(key, target) {
        this.key = key;
        this.target = target;
        if (this.key !== undefined) {
            let temp = "";
            let foundFirstLetter = false;
            for (const c of this.key) {
                if (foundFirstLetter && isUpperCase(c))
                    temp += "-";
                foundFirstLetter = true;
                temp += c.toLowerCase();
            }
            this.key = temp;
        }
    }
    /** How many callback methods are subscribed to this event */
    get listenerCount() { return this.methods?.length ?? 0; }
    /** If the event is currently being invoked */
    get isInvoking() { return this._isInvoking; }
    _isInvoking = false;
    // TODO: can we make functions serializable?
    methods = [];
    _methodsCopy = [];
    static from(...evts) {
        return new EventList(evts);
    }
    constructor(evts) {
        this.methods = [];
        if (Array.isArray(evts)) {
            for (const evt of evts) {
                if (evt instanceof CallInfo) {
                    this.methods.push(evt);
                }
                else if (typeof evt === "function") {
                    this.methods.push(new CallInfo(evt));
                }
            }
        }
        else {
            if (typeof evts === "function") {
                this.methods.push(new CallInfo(evts));
            }
        }
    }
    /** Invoke all the methods that are subscribed to this event */
    invoke(...args) {
        if (this._isInvoking) {
            console.warn("Circular event invocation detected. Please check your event listeners for circular references.", this);
            return false;
        }
        if (this.methods?.length <= 0)
            return false;
        this._isInvoking = true;
        try {
            // make a copy of the methods array to avoid issues when removing listeners during invocation
            this._methodsCopy.length = 0;
            this._methodsCopy.push(...this.methods);
            // first invoke all the methods that were subscribed to this eventlist
            for (const m of this._methodsCopy) {
                m.invoke(...args);
            }
            // then try to dispatch the event on the object that is owning this eventlist
            // with this we get automatic event listener support for unity events on all componnets
            // so example for a component with a click UnityEvent you can also subscribe to the component like this:
            // myComponent.addEventListener("click", args => {...")
            if (typeof this.target === "object" && typeof this.key === "string") {
                const fn = this.target["dispatchEvent"];
                if (typeof fn === "function") {
                    const evt = new EventListEvent(this.key);
                    evt.args = args;
                    fn.call(this.target, evt);
                }
            }
        }
        finally {
            this._isInvoking = false;
            this._methodsCopy.length = 0;
        }
        return true;
    }
    /** Add a new event listener to this event */
    addEventListener(cb) {
        this.methods.push(new CallInfo(cb));
        return cb;
    }
    removeEventListener(cb) {
        if (!cb)
            return;
        for (let i = this.methods.length - 1; i >= 0; i--) {
            if (this.methods[i].target === cb) {
                this.methods[i].enabled = false;
                this.methods.splice(i, 1);
            }
        }
    }
    removeAllEventListeners() {
        this.methods.length = 0;
    }
}
//# sourceMappingURL=EventList.js.map