import { ReferenceException } from "../exceptions/index.js";

import type { Callback, CallbackMap, InternalsEventsMap, WildcardEventsMap } from "./types.js";

type P = InternalsEventsMap;
type S = WildcardEventsMap & InternalsEventsMap;

/**
 * A class implementing the
 * {@link https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern|Publish-subscribe} pattern.
 *
 * It can be used to create a simple event system where objects can subscribe
 * to events and receive notifications when the events are published.  
 * It's a simple and efficient way to decouple the objects and make them communicate with each other.
 *
 * Using generics, it's also possible to define the type of the events and the callbacks that can be subscribed to them.
 *
 * ---
 *
 * @example
 * ```ts
 * interface EventsMap
 * {
 *     "player:spawn": (evt: SpawnEvent) => void;
 *     "player:move": ({ x, y }: Point) => void;
 *     "player:death": () => void;
 * }
 *
 * const publisher = new Publisher<EventsMap>();
 *
 * let unsubscribe: () => void;
 * publisher.subscribe("player:death", unsubscribe);
 * publisher.subscribe("player:spawn", (evt) =>
 * {
 *     unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] });
 * });
 * ```
 *
 * ---
 *
 * @template T
 * A map containing the names of the emittable events and the
 * related callback signatures that can be subscribed to them.  
 * Default is `Record<string, (...args: unknown[]) => unknown>`.
 */
export default class Publisher<T extends CallbackMap<T> = CallbackMap>
{
    /**
     * A map containing all the subscribers for each event.
     *
     * The keys are the names of the events they are subscribed to.  
     * The values are the arrays of the subscribers themselves.
     */
    protected readonly _subscribers: Map<string, Callback<unknown[], unknown>[]>;

    /**
     * Initializes a new instance of the {@link Publisher} class.
     *
     * ---
     *
     * @example
     * ```ts
     * const publisher = new Publisher();
     * ```
     */
    public constructor()
    {
        this._subscribers = new Map();
    }

    /**
     * Creates a new scoped instance of the {@link Publisher} class,
     * which can be used to publish and subscribe events within a specific context.
     *
     * It can receive all events published to the parent publisher while also allowing
     * the scoped publisher to handle its own events independently.  
     * In fact, events published to the scoped publisher won't be propagated back to the parent publisher.
     *
     * ---
     *
     * @example
     * ```ts
     * const publisher = new Publisher();
     * const context = publisher.createScope();
     *
     * publisher.subscribe("player:death", () => console.log("Player has died."));
     * context.subscribe("player:spawn", () => console.log("Player has spawned."));
     *
     * publisher.publish("player:spawn"); // "Player has spawned."
     * context.publish("player:death");   // * no output *
     * ```
     *
     * ---
     *
     * @template U
     * A map containing the additional names of the emittable events and
     * the related callback signatures that can be subscribed to them.
     * Default is `{ }`.
     *
     * @return
     * A new instance of the {@link Publisher} class that can be
     * used to publish and subscribe events within a specific context.
     */

    // eslint-disable-next-line @typescript-eslint/no-empty-object-type
    public createScope<U extends CallbackMap<U> = { }>(): Publisher<U & T>
    {
        const scope = new Publisher();

        this.subscribe("__internals__:clear", () => scope.clear());
        this.subscribe("*", (event, ...args): void => { scope.publish(event, ...args); });

        return scope;
    }

    /**
     * Publishes an event to all the subscribers.  
     * This events will trigger the wildcard listeners as well, if any.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("player:move", (coords) => { [...] });
     * publisher.subscribe("player:move", ({ x, y }) => { [...] });
     * publisher.subscribe("player:move", (evt) => { [...] });
     *
     * publisher.publish("player:move", { x: 10, y: 20 });
     * ```
     *
     * ---
     *
     * @template K The key of the map containing the callback signature to publish.
     *
     * @param event The name of the event to publish.
     * @param args The arguments to pass to the subscribers.
     *
     * @returns An array containing the return values of all the subscribers.
     */
    public publish<K extends keyof T>(event: K & string, ...args: Parameters<T[K]>): ReturnType<T[K]>[];

    /**
     * Publishes an internal event to all the subscribers.
     *
     * Internal events follow the pattern `__${string}__:${string}` and are used for internal
     * communication within the publisher system. These events won't trigger wildcard listeners.  
     * Please note to use this method only if you know what you are doing.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("__internals__:clear", () => console.log("Clearing..."));
     * publisher.publish("__internals__:clear"); // "Clearing..."
     * ```
     *
     * ---
     *
     * @template K The key of the internal events map containing the callback signature to publish.
     *
     * @param event The name of the internal event to publish.
     * @param args The arguments to pass to the subscribers.
     *
     * @returns An array containing the return values of all the subscribers.
     */
    public publish<K extends keyof P>(event: K & string, ...args: Parameters<P[K]>): ReturnType<P[K]>[];
    public publish(event: string, ...args: unknown[]): unknown[]
    {
        let results: unknown[];
        let subscribers = this._subscribers.get(event);
        if (subscribers)
        {
            const _subscribers = subscribers.slice();
            const _length = _subscribers.length;

            results = new Array<unknown>(_length);
            for (let i = 0; i < _length; i += 1)
            {
                results[i] = _subscribers[i](...args);
            }
        }
        else { results = []; }

        if (!(event.startsWith("__")))
        {
            subscribers = this._subscribers.get("*");
            if (subscribers)
            {
                subscribers.slice()
                    .forEach((subscriber) => subscriber(event, ...args));
            }
        }

        return results;
    }

    /**
     * Subscribes to an event and adds a subscriber to be executed when the event is published.
     *
     * ---
     *
     * @example
     * ```ts
     * let unsubscribe: () => void;
     * publisher.subscribe("player:death", unsubscribe);
     * publisher.subscribe("player:spawn", (evt) =>
     * {
     *     unsubscribe = publisher.subscribe("player:move", ({ x, y }) => { [...] });
     * });
     * ```
     *
     * ---
     *
     * @template K The key of the map containing the callback signature to subscribe.
     *
     * @param event The name of the event to subscribe to.
     * @param subscriber The subscriber to execute when the event is published.
     *
     * @returns A function that can be used to unsubscribe the subscriber from the event.
     */
    public subscribe<K extends keyof T>(event: K & string, subscriber: T[K]): Callback;

    /**
     * Subscribes to the wildcard event to listen to all published events.
     *
     * The wildcard subscriber will be called for every event published, receiving
     * the event type as the first parameter followed by all event arguments.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("*", (type, ...args) =>
     * {
     *     console.log(`Event "${type}" was fired with args:`, args);
     * });
     * ```
     *
     * ---
     *
     * @template K The key of the wildcard events map (always `*`).
     *
     * @param event The wildcard event name (`*`).
     * @param subscriber The subscriber to execute for all published events.
     *
     * @returns A function that can be used to unsubscribe the subscriber from the wildcard event.
     */
    public subscribe<K extends keyof S>(event: K & string, subscriber: S[K]): Callback;
    public subscribe(event: string, subscriber: Callback<unknown[], unknown>): Callback
    {
        const subscribers = this._subscribers.get(event) ?? [];
        subscribers.push(subscriber);

        this._subscribers.set(event, subscribers);

        return () =>
        {
            const index = subscribers.indexOf(subscriber);
            if (index < 0)
            {
                throw new ReferenceException("Unable to unsubscribe the required subscriber. " +
                    "The subscription was already unsubscribed.");
            }

            subscribers.splice(index, 1);
        };
    }

    /**
     * Unsubscribes from an event and removes a subscriber from being executed when the event is published.
     *
     * ---
     *
     * @example
     * ```ts
     * const onPlayerMove = ({ x, y }: Point) => { [...] };
     *
     * publisher.subscribe("player:spawn", (evt) => publisher.subscribe("player:move", onPlayerMove));
     * publisher.subscribe("player:death", () => publisher.unsubscribe("player:move", onPlayerMove));
     * ```
     *
     * ---
     *
     * @template K The key of the map containing the callback signature to unsubscribe.
     *
     * @param event The name of the event to unsubscribe from.
     * @param subscriber The subscriber to remove from the event.
     */
    public unsubscribe<K extends keyof T>(event: K & string, subscriber: T[K]): void;

    /**
     * Unsubscribes from the wildcard event and removes a subscriber from being executed for all events.
     *
     * This removes a previously registered wildcard listener that was capturing all published events.
     *
     * ---
     *
     * @example
     * ```ts
     * const wildcardHandler = (type: string, ...args: unknown[]) => console.log(type, args);
     * 
     * publisher.subscribe("*", wildcardHandler);
     * publisher.unsubscribe("*", wildcardHandler);
     * ```
     *
     * ---
     *
     * @template K The key of the wildcard events map (always `*`).
     *
     * @param event The wildcard event name (`*`).
     * @param subscriber The wildcard subscriber to remove.
     */
    public unsubscribe<K extends keyof S>(event: K & string, subscriber: S[K]): void;
    public unsubscribe(event: string, subscriber: Callback<unknown[], unknown>): void
    {
        const subscribers = this._subscribers.get(event);
        if (!(subscribers))
        {
            throw new ReferenceException("Unable to unsubscribe the required subscriber. " +
                "The subscription was already unsubscribed or was never subscribed.");
        }

        const index = subscribers.indexOf(subscriber);
        if (index < 0)
        {
            throw new ReferenceException("Unable to unsubscribe the required subscriber. " +
                "The subscription was already unsubscribed or was never subscribed.");
        }

        subscribers.splice(index, 1);
        if (subscribers.length === 0) { this._subscribers.delete(event); }
    }

    /**
     * Unsubscribes all subscribers from a specific event and removes
     * them from being executed when the event is published.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("player:spawn", (evt) => { [...] });
     * publisher.subscribe("player:move", (coords) => { [...] });
     * publisher.subscribe("player:move", () => { [...] });
     * publisher.subscribe("player:move", ({ x, y }) => { [...] });
     * publisher.subscribe("player:death", () => { [...] });
     *
     * // All these subscribers are working fine...
     *
     * publisher.unsubscribeAll("player:move");
     *
     * // ... but now "player:move" subscribers are gone!
     * ```
     *
     * ---
     *
     * @template K The key of the map containing the event to clear.
     *
     * @param event The name of the event to unsubscribe all subscribers from.
     */
    public unsubscribeAll<K extends keyof T>(event: K & string): void;

    /**
     * Unsubscribes all subscribers from the wildcard event and removes
     * them from being executed for all published events.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("player:spawn", (evt) => { [...] });
     * publisher.subscribe("*", (type, ...args) => { [...] });
     * publisher.subscribe("*", (type, arg1, arg2, arg3) => { [...] });
     * publisher.subscribe("*", (_, arg, ...rest) => { [...] });
     * publisher.subscribe("player:death", () => { [...] });
     *
     * // All these subscribers are working fine...
     *
     * publisher.unsubscribeAll("*");
     *
     * // ... but now wildcard subscribers are gone!
     * ```
     *
     * ---
     *
     * @template K The key of the wildcard events map (`*`).
     *
     * @param event The wildcard event name (`*`).
     */
    public unsubscribeAll<K extends keyof S>(event: K & string): void;
    public unsubscribeAll(event: string): void
    {
        this._subscribers.delete(event);
    }

    /**
     * Unsubscribes all the subscribers from all the events.
     *
     * ---
     *
     * @example
     * ```ts
     * publisher.subscribe("player:spawn", (evt) => { [...] });
     * publisher.subscribe("player:move", (coords) => { [...] });
     * publisher.subscribe("*", () => { [...] });
     * publisher.subscribe("player:move", ({ x, y }) => { [...] });
     * publisher.subscribe("player:death", () => { [...] });
     *
     * // All these subscribers are working fine...
     *
     * publisher.clear();
     *
     * // ... but now they're all gone!
     * ```
     */
    public clear(): void
    {
        this.publish("__internals__:clear");

        this._subscribers.clear();
    }

    public readonly [Symbol.toStringTag]: string = "Publisher";
}
