import { isDevEnvironment } from "../engine/debug/index.js";
import type { IComponent, IEventList } from "../engine/engine_types.js";

const argumentsBuffer = new Array<any>();

/**
 * CallInfo represents a single callback method that can be invoked by the {@link EventList}.
 */
export class CallInfo {

    /** @internal Used by the instantiate resolver to recursively resolve references */
    static { CallInfo.prototype.$serializedTypes = { target: Object, arguments: Array }; }
    declare $serializedTypes: Record<string, any>;

    /**
     * When the CallInfo is enabled it will be invoked when the EventList is invoked
     */
    enabled: boolean = true;

    /**
     * The target object to invoke the method on OR the function to invoke
     */
    target: Object | Function;

    methodName: string | null;

    /**
     * The arguments to invoke this method with
     */
    arguments?: Array<any>;

    get canClone() {
        return this.target instanceof Object;
    }

    constructor(target: Function);
    constructor(target: Object, methodName: string | null, args?: Array<any>, enabled?: boolean);
    constructor(target: Object | Function, methodName?: string | null, args?: Array<any>, enabled?: boolean) {
        this.target = target;
        this.methodName = methodName || null;
        this.arguments = args;
        if (enabled != undefined)
            this.enabled = enabled;
    }

    invoke(...args: any) {
        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];
                }
            }
        }
    }
}

/** @deprecated No longer automatically dispatched. Use `eventList.on()` directly instead. */
export class EventListEvent<TArgs extends any> extends Event { //implements ArrayLike<T> {
    args?: TArgs;
}



/**
 * 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<TArgs extends any = any> implements IEventList {

    /** @internal Used by the instantiate resolver to recursively resolve references */
    static { EventList.prototype.$serializedTypes = { methods: Array }; }
    declare $serializedTypes: Record<string, any>;

    /** checked during instantiate to create a new instance */
    readonly isEventList = true;


    /** 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; }

    private _isInvoking: boolean = false;

    // TODO: can we make functions serializable?
    private readonly methods: Array<CallInfo> = [];
    private readonly _methodsCopy: Array<CallInfo> = [];

    /**
     * Create a new EventList with the given callback methods. You can pass either CallInfo instances or functions directly.
     * @returns a new EventList instance with the given callback methods
     * @example
     * ```ts
     * const onClick = EventList.from(
     *   () => console.log("Clicked!"),
     *   new CallInfo(someObject, "someMethod", [arg1, arg2])
     * );
     * onClick.invoke();
     * ```
     */
    static from(...evts: Array<Function>) {
        return new EventList(evts);
    }

    /**
     * Create a new EventList with the given callback methods. You can pass either CallInfo instances or functions directly.
     * @returns a new EventList instance with the given callback methods
     */
    constructor(evts?: Array<CallInfo | Function> | Function) {
        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
     * @param args optional arguments to pass to the event listeners. These will be passed before any custom arguments defined in the CallInfo instances. So if you have a CallInfo with arguments and you also pass arguments to invoke, the arguments passed to invoke will take precedence over the CallInfo arguments.
     * @returns true if the event was successfully invoked, false if there are no listeners or if a circular invocation was detected
     */
    invoke(...args: Array<TArgs>) {
        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);

            for (const m of this._methodsCopy) {
                m.invoke(...args);
            }
        }
        finally {
            this._isInvoking = false;
            this._methodsCopy.length = 0;
        }
        return true;
    }

    /** Add a new event listener to this event
     * @returns a function to remove the event listener
     * @see {@link removeEventListener} for more details and return value information
     * @see {@link off} for an alias with better readability when unsubscribing from events
     * @example
     * ```ts
     * const off = myEvent.addEventListener(args => console.log("Clicked!", args));
     * // later
     * off();
     * ```
     */
    addEventListener(callback: (args: TArgs) => void): Function {
        this.methods.push(new CallInfo(callback));
        return () => this.removeEventListener(callback);
    }

    /**
     * Alias for addEventListener for better readability when subscribing to events. You can use it like this:
     * ```ts
     * myEvent.on(args => console.log("Clicked!", args));
     * ```
      * @returns a function to remove the event listener
      * @see {@link addEventListener} for more details and return value information
     */
    on(callback: (args: TArgs) => void): Function {
        return this.addEventListener(callback);
    }

    /**
     * Remove an event listener from this event.
     * @returns true if the event listener was found and removed, false otherwise
     */
    removeEventListener(fn: Function | null | undefined) {
        if (!fn) return false;
        let found = false;
        for (let i = this.methods.length - 1; i >= 0; i--) {
            if (this.methods[i].target === fn) {
                this.methods[i].enabled = false;
                this.methods.splice(i, 1);
                found = true;
            }
        }
        return found;
    }

    /**
     * Alias for removeEventListener for better readability when unsubscribing from events. You can use it like this:
     * ```ts
     * const off = myEvent.on(args => console.log("Clicked!", args));
     * // later
     * off();
     * ```
     * 
     * @see {@link removeEventListener} for more details and return value information
     */
    off(callback: Function | null | undefined) {
        return this.removeEventListener(callback);
    }

    /**
     * Remove all event listeners from this event. Use with caution! This will remove all listeners!
     */
    removeAllEventListeners() {
        this.methods.length = 0;
    }
}

