Source: Signals.js

/**
 * @typedef {Object} SignalHandler
 * @property {Object.<string, Function[]>} signals - Map of event names to arrays of callback functions
 * @property {string[]} allSignals - List of all registered signal names
 */
/**
 * Adds event handling capabilities to a prototype.
 * Creates a simple event system that allows objects to emit and listen to events.
 * 
 * The function modifies the prototype by adding:
 * - Event registration methods
 * - Event emission methods
 * - Signal initialization
 * - Signal storage
 * 
 *
 * Implementation Details
 * 
 * The signal system works by:
 * 1. Extending the prototype with signal tracking properties
 * 2. Maintaining arrays of callbacks for each signal type
 * 3. Providing methods to register and trigger callbacks
 * 
 * Signal Storage Structure:
 * ```javascript
 * {
 *     signals: {
 *         'eventName1': [callback1, callback2, ...],
 *         'eventName2': [callback3, callback4, ...]
 *     },
 *     allSignals: ['eventName1', 'eventName2', ...]
 * }
 * ```
 * 
 * Performance Considerations:
 * - Callbacks are stored in arrays for fast iteration
 * - Signals are initialized lazily on first use
 * - Direct property access for quick event emission
 * 
 * Usage Notes:
 * - Events must be registered before they can be used
 * - Multiple callbacks can be registered for the same event
 * - Callbacks are executed synchronously
 * - Parameters are passed through to callbacks unchanged
 *
 * @function
 * @param {Object} proto - The prototype to enhance with signal capabilities
 * @param {...string} signals - Names of signals to register
 * 
 * @example
 * ```javascript
 * // Add events to a class
 * class MyClass {}
 * addSignals(MyClass, 'update', 'change');
 * 
 * // Use events
 * const obj = new MyClass();
 * obj.addEvent('update', () => console.log('Updated!'));
 * obj.emit('update');
 * ```
 * 
 * @example
 * ```javascript
 * // Multiple signals
 * class DataHandler {}
 * addSignals(DataHandler, 
 *     'dataLoaded',
 *     'dataProcessed',
 *     'error'
 * );
 * 
 * const handler = new DataHandler();
 * handler.addEvent('dataLoaded', (data) => {
 *     console.log('Data loaded:', data);
 * });
 * ```
 */
function addSignals(proto, ...signals) {
	if (!proto.prototype.allSignals)
			proto.prototype.allSignals = [];
	proto.prototype.allSignals = [...proto.prototype.allSignals, ...signals];

	/**
	 * Methods added to the prototype
	 */
	/**
	 * Initializes the signals system for an instance.
	 * Creates the signals storage object and populates it with empty arrays
	 * for each registered signal type.
	 * 
	 * @memberof SignalHandler
	 * @instance
	 * @private
	 */
	proto.prototype.initSignals = function () {
			this.signals = Object.fromEntries(this.allSignals.map(s => [s, []]));
	}

	/**
	 * Registers a callback function for a specific event.
	 * 
	 * @memberof SignalHandler
	 * @instance
	 * @param {string} event - The event name to listen for
	 * @param {Function} callback - Function to be called when event is emitted
	 * @throws {Error} Implicitly if event doesn't exist
	 * 
	 * @example
	 * ```javascript
	 * obj.addEvent('update', (param1, param2) => {
	 *     console.log('Update occurred with:', param1, param2);
	 * });
	 * ```
	 */
	proto.prototype.addEvent = function (event, callback) {
			if (!this.signals)
					this.initSignals();
			this.signals[event].push(callback);
	}

	/**
	 * Adds a one-time event listener that will be automatically removed after first execution.
	 * Once the event is emitted, the listener is automatically removed before the callback
	 * is executed.
	 * 
	 * @memberof SignalHandler
	 * @instance
	 * @param {string} event - The event name to listen for once
	 * @param {Function} callback - Function to be called once when event is emitted
	 * @throws {Error} Implicitly if event doesn't exist or callback is not a function
	 * 
	 * @example
	 * ```javascript
	 * obj.once('update', (param) => {
	 *     console.log('This will only run once:', param);
	 * });
	 * ```
	 */
	proto.prototype.once = function (event, callback) {
			if (!callback || typeof callback !== 'function') {
					console.error('Callback must be a function');
					return;
			}

			const wrappedCallback = (...args) => {
					// Remove the listener before calling the callback
					// to prevent recursion if the callback emits the same event
					this.removeEvent(event, wrappedCallback);
					callback.apply(this, args);
			};

			this.addEvent(event, wrappedCallback);
	}

	/**
	 * Removes an event callback or all callbacks for a specific event.
	 * If no callback is provided, all callbacks for the event are removed.
	 * If a callback is provided, only that specific callback is removed.
	 * 
	 * @memberof SignalHandler
	 * @instance
	 * @param {string} event - The event name to remove callback(s) from
	 * @param {Function} [callback] - Optional specific callback function to remove
	 * @returns {boolean} True if callback(s) were removed, false if event or callback not found
	 * @throws {Error} Implicitly if event doesn't exist
	 * 
	 * @example
	 * ```javascript
	 * // Remove specific callback
	 * const callback = (data) => console.log(data);
	 * obj.addEvent('update', callback);
	 * obj.removeEvent('update', callback);
	 * 
	 * // Remove all callbacks for an event
	 * obj.removeEvent('update');
	 * ```
	 */
	proto.prototype.removeEvent = function (event, callback) {
			if (!this.signals) {
					this.initSignals();
					return false;
			}

			if (!this.signals[event]) {
					return false;
			}

			if (callback === undefined) {
					// Remove all callbacks for this event
					const hadCallbacks = this.signals[event].length > 0;
					this.signals[event] = [];
					return hadCallbacks;
			}

			// Find and remove specific callback
			const initialLength = this.signals[event].length;
			this.signals[event] = this.signals[event].filter(cb => cb !== callback);
			return initialLength > this.signals[event].length;
	}

	/**
	 * Emits an event, triggering all registered callbacks.
	 * Callbacks are executed in the order they were registered.
	 * Creates a copy of the callbacks array before iteration to prevent
	 * issues if callbacks modify the listeners during emission.
	 * 
	 * @memberof SignalHandler
	 * @instance
	 * @param {string} event - The event name to emit
	 * @param {...*} parameters - Parameters to pass to the callback functions
	 * 
	 * @example
	 * ```javascript
	 * obj.emit('update', 'param1', 42);
	 * ```
	 */
	proto.prototype.emit = function (event, ...parameters) {
			if (!this.signals)
					this.initSignals();
			// Create a copy of the callbacks array to safely iterate even if
			// callbacks modify the listeners
			const callbacks = [...this.signals[event]];
			for (let r of callbacks)
					r(...parameters);
	}
}

export { addSignals }