/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { Base } from "./base.mjs";
import { isObject } from "./is.mjs";
import { TokenList } from "./tokenlist.mjs";
import { instanceSymbol } from "../constants.mjs";
export { Observer };

/**
 * Manages a callback function that is executed asynchronously when updated.
 *
 * The `update` method is called with the subject object as its `this` context.
 * For this reason, the callback should be a regular function, not an arrow function,
 * if you need access to the subject via `this`.
 *
 * @example
 * // Basic usage with a regular function to access the subject
 * const observer = new Observer(function() {
 * console.log("Subject updated:", this); // `this` refers to `mySubject`
 * });
 *
 * // Usage with arguments
 * const greeter = new Observer(function(greeting) {
 * console.log(greeting, this.name); // "Hello", "World"
 * }, "Hello");
 *
 * const mySubject = { name: "World" };
 * observer.update(mySubject);
 * greeter.update(mySubject);
 *
 * @fragments /fragments/libraries/types/observer/
 *
 * @example /examples/libraries/types/observer/simple/ Schedule a deduplicated observer update
 * @license AGPLv3
 * @since 1.0.0
 * @summary An asynchronous observer that batches updates per subject and callback.
 */
class Observer extends Base {
	/**
	 * Stores promises for updates that are scheduled but not yet executed.
	 * This prevents multiple executions for the same subject within one microtask cycle.
	 * @type {Map<object, Promise<*>>}
	 */
	#pendingUpdates = new Map();
	#callback;
	#arguments;
	#tags = new TokenList();

	/**
	 * @param {function} callback The function to execute on update.
	 * @param {...*} args Additional arguments to pass to the callback.
	 */
	constructor(callback, ...args) {
		super();

		if (typeof callback !== "function") {
			throw new Error("Observer callback must be a function.");
		}

		this.#callback = callback;
		this.#arguments = args;
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/types/observer");
	}

	// Getter for properties for cleaner access if needed
	get callback() {
		return this.#callback;
	}

	get arguments() {
		return this.#arguments;
	}

	/**
	 * Schedules the callback for execution with the given subject.
	 * If multiple updates for the same subject are requested in the same event loop tick,
	 * the callback is only executed once. All callers will receive the same promise.
	 *
	 * @param {object} subject The subject object that triggered the update.
	 * @returns {Promise<*>} A promise that resolves with the return value of the callback,
	 * or rejects if the callback throws an error.
	 */
	update(subject) {
		if (!isObject(subject)) {
			return Promise.reject(new Error("Subject must be an object."));
		}

		if (this.#pendingUpdates.has(subject)) {
			return this.#pendingUpdates.get(subject);
		}

		const promise = new Promise((resolve, reject) => {
			queueMicrotask(async () => {
				try {
					const result = await this.#callback.apply(subject, this.#arguments);
					resolve(result);
				} catch (e) {
					reject(e);
				} finally {
					this.#pendingUpdates.delete(subject);
				}
			});
		});

		this.#pendingUpdates.set(subject, promise);

		return promise;
	}

	/**
	 * @param {string} tag
	 * @returns {Observer}
	 */
	addTag(tag) {
		this.#tags.add(tag);
		return this;
	}

	/**
	 * @param {string} tag
	 * @returns {Observer}
	 */
	removeTag(tag) {
		this.#tags.remove(tag);
		return this;
	}

	/**
	 * @returns {string[]}
	 */
	getTags() {
		return this.#tags.entries();
	}

	/**
	 * @param {string} tag
	 * @returns {boolean}
	 */
	hasTag(tag) {
		return this.#tags.contains(tag);
	}
}
