///////////////////////////////////////////////////////////////////////////////
// Copyright (C) 2002-2025, Open Design Alliance (the "Alliance").
// All rights reserved.
//
// This software and its documentation and related materials are owned by
// the Alliance. The software may only be incorporated into application
// programs owned by members of the Alliance, subject to a signed
// Membership Agreement and Supplemental Software License Agreement with the
// Alliance. The structure and organization of this software are the valuable
// trade secrets of the Alliance and its suppliers. The software is also
// protected by copyright law and international treaty provisions. Application
// programs incorporating this software must include the following statement
// with their copyright notices:
//
//   This application incorporates Open Design Alliance software pursuant to a
//   license agreement with Open Design Alliance.
//   Open Design Alliance Copyright (C) 2002-2025 by Open Design Alliance.
//   All rights reserved.
//
// By use of this software, its documentation or related materials, you
// acknowledge and accept the above terms.
///////////////////////////////////////////////////////////////////////////////

/**
 * The event interface for {@link IEventEmitter}.
 */
export interface IEvent {
  /**
   * Event type.
   */
  type: string;
}

/**
 * Event emitter interface.
 */
export interface IEventEmitter {
  /**
   * Registers a new listener for the event type.
   *
   * @param type - The type of event to listen to.
   * @param listener - The function that gets called when the event is fired.
   */
  addEventListener(type: string, listener: (event: IEvent) => void): this;

  /**
   * Removes the listener for the event type.
   *
   * @param type - The type of the event that gets removed.
   * @param listener - The listener function that gets removed.
   */
  removeEventListener(type: string, listener: (event: IEvent) => void): this;

  /**
   * If `type` is specified, removes all registered listeners for type, otherwise removes all registered
   * listeners.
   *
   * @param type - The type of the listener that gets removed.
   */
  removeAllListeners(type?: string): this;

  /**
   * Fires the event. Calls each of the listeners registered for the event type `event.type`, in the
   * order they were registered.
   *
   * @param event - The event that gets fired.
   */
  emitEvent(event: IEvent): boolean;

  // Node.js style, to emit custom eventsg

  /**
   * Registers a new listener for the event type. Alias to {@link addEventListener | addEventListener()}
   *
   * @param type - The type of event to listen to.
   * @param listener - The function that gets called when the event is fired.
   */
  on(type: string, listener: (event: any) => void): this;

  /**
   * Removes the listener from an event type. Alias to
   * {@link removeEventListener | removeEventListener()}.
   *
   * @param type - The type of the event that gets removed.
   * @param listener - The listener function that gets removed.
   */
  off(type: string, listener: (event: any) => void): this;

  /**
   * Fires the event. Alias to {@link emitEvent | emitEvent()}.
   *
   * @param type - The type of event that gets fired.
   * @param args - The event properties.
   */
  emit(type: string | object, ...args: any[]): boolean;
}

/**
 * The minimal basic event that can be emitted by a {@link EventEmitter2}.
 */
export interface Event<T extends string = string> extends IEvent {
  /**
   * Event type.
   */
  type: T;
}

/**
 * Event emitter for custom objects.
 */
export class EventEmitter2<EventMap extends Record<string, any> = Record<string, any>> implements IEventEmitter {
  private _listeners: Record<any, any[]> = {};

  addEventListener<T extends keyof EventMap>(type: T, listener: (event: EventMap[T]) => void): this {
    if (this._listeners[type] === undefined) this._listeners[type] = [];
    this._listeners[type].push(listener);
    return this;
  }

  removeEventListener<T extends keyof EventMap>(type: T, listener: (event: EventMap[T]) => void): this {
    if (this._listeners[type] === undefined) return this;
    const listeners = this._listeners[type].filter((x: any) => x !== listener);
    if (listeners.length !== 0) this._listeners[type] = listeners;
    else delete this._listeners[type];
    return this;
  }

  removeAllListeners<T extends keyof EventMap>(type?: T): this {
    if (type) delete this._listeners[type];
    else this._listeners = {};
    return this;
  }

  emitEvent<T extends keyof EventMap>(event: Event<Extract<T, string>> & EventMap[T]): boolean {
    if (this._listeners[event.type] === undefined) return false;
    const invoke = this._listeners[event.type].slice();
    invoke.forEach((listener: any) => listener.call(this, event));
    return true;
  }

  on(type: string, listener: (event: any) => void): this {
    return this.addEventListener(type as any, listener as any);
  }

  off(type: string, listener: (event: any) => void): this {
    return this.removeEventListener(type as any, listener as any);
  }

  emit(type: string | object, ...args: any[]): boolean {
    if (typeof type === "string") return this.emitEvent({ type, args } as any);
    else if (typeof type === "object") return this.emitEvent(type as any);
    else return false;
  }
}
