/*
 *     The Peacock Project - a HITMAN server replacement.
 *     Copyright (C) 2021-2023 The Peacock Project Team
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU Affero General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU Affero General Public License for more details.
 *
 *     You should have received a copy of the GNU Affero General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

/**
 * A HookMap is a helper class for a Map with Hooks.
 *
 * @example
 * const myHookMap = new HookMap(key => new SyncHook())
 *
 * @example
 * hookMap.for("some-key").tap("MyPlugin", (arg) => { })
 *
 * @example
 * const hook = hookMap.for("some-key")
 * hook.call("some value", 123456)
 */
export class HookMap<Hook> {
    private readonly _map: Map<string, Hook>

    public constructor(private readonly _createFunc: (key: string) => Hook) {
        this._map = new Map()
    }

    /**
     * Get a hook for the given key.
     *
     * @param key The hook to get.
     * @returns The hook.
     */
    public for(key: string): Hook {
        if (this._map.has(key)) {
            return this._map.get(key)!
        }

        const hook = this._createFunc(key)
        this._map.set(key, hook)
        return hook
    }
}

/**
 * The options for a hook. Will either be just the name (as a string), or an object containing the additional options.
 */
export type TapOptions = string | { name: string; context: boolean }

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AsArray<T> = T extends any[] ? T : [T]

/**
 * An internal interface containing the properties held by a single taps' container object.
 */
interface Tap<T, R> {
    name: string
    func: (...args: AsArray<T>) => R
    enableContext: boolean
}

/**
 * The structure of an intercept.
 *
 * @see name
 * @see call
 * @see tap
 */
export interface Intercept<Params, Return> {
    /**
     * The name of the intercept.
     */
    name: string

    /**
     * A function called just after the hook is called, and before all taps run.
     *
     * @param context The context object. Can be modified.
     * @param params The parameters that the taps will get. Can be modified.
     */
    call(context, ...params: AsArray<Params>): void

    /**
     * A function called when the hook is tapped. Note that it will not be called when an interceptor is registered, since that doesn't count as a tap.
     *
     * @param name The name of the tap.
     * @param func The tap's function.
     */
    tap(name: string, func: (...args: AsArray<Params>) => Return): void
}

/**
 * The base for a hook, including {@link tap} and {@link intercept} functionality.
 *
 * @see SyncHook
 * @see SyncBailHook
 * @see AsyncSeriesHook
 */
export abstract class BaseImpl<Params, Return = void> {
    protected _intercepts: Intercept<Params, Return>[]
    protected _taps: Tap<Params, Return>[]

    /**
     * Register an interceptor.
     * Interceptors can listen for certain events, and control things like context for them.
     *
     * @param intercept An object containing the intercept.
     * @see Intercept
     */
    public intercept(intercept: Intercept<Params, Return>): void {
        this._intercepts.push(intercept)
    }

    /**
     * Tap the hook.
     *
     * @param nameOrOptions A string containing the tap's name, or an object containing the tap's details.
     * @param consumer The function that will be called when the hook is.
     * @see TapOptions
     */
    public tap(
        nameOrOptions: TapOptions,
        consumer: (...args: AsArray<Params>) => Return,
    ): void {
        const name =
            typeof nameOrOptions === "string"
                ? nameOrOptions
                : nameOrOptions.name
        const enableContext =
            typeof nameOrOptions === "string" ? false : nameOrOptions.context

        for (const intercept of this._intercepts) {
            if (intercept.tap) {
                intercept.tap(name, consumer)
            }
        }

        this._taps.push({
            name,
            func: consumer,
            enableContext,
        })
    }

    public get allTapNames(): string[] {
        return this._taps.map((t) => t.name)
    }
}

/**
 * A hook that runs each tap one-by-one.
 */
export class SyncHook<Params> extends BaseImpl<Params> {
    public constructor() {
        super()
        this._taps = []
        this._intercepts = []
    }

    public call(...params: AsArray<Params>): void {
        const context = {}

        for (const intercept of this._intercepts) {
            if (intercept.call) {
                intercept.call(context, ...params)
            }
        }

        for (const tap of this._taps) {
            const args = tap.enableContext ? [context, ...params] : [...params]

            // @ts-expect-error TypeScript things.
            tap.func(...args)
        }

        return
    }
}

/**
 * A hook that runs each tap one-by-one until one returns a result.
 */
export class SyncBailHook<Params, Return> extends BaseImpl<Params, Return> {
    public constructor() {
        super()
        this._taps = []
        this._intercepts = []
    }

    public call(...params: AsArray<Params>): Return | null {
        const context = {}

        for (const intercept of this._intercepts) {
            if (intercept.call) {
                intercept.call(context, ...params)
            }
        }

        for (const tap of this._taps) {
            const args = tap.enableContext ? [context, ...params] : [...params]

            // @ts-expect-error TypeScript things.
            const result = tap.func(...args)

            if (result) {
                return result
            }
        }

        return null
    }
}

/**
 * A hook that runs each tap, one-by-one, in an async context (each tap may be an async function).
 */
export class AsyncSeriesHook<Params> extends BaseImpl<Params, Promise<void>> {
    public constructor() {
        super()
        this._taps = []
        this._intercepts = []
    }

    /**
     * Async hooks cannot be called from a sync function - use {@link callAsync} instead!
     * This function will only throw an error.
     *
     * @throws {Error} Always throws an error, see the note above.
     * @deprecated
     */
    public call(): Promise<void> {
        throw new Error("Can't call an async hook with the sync method.")
    }

    public async callAsync(...params: AsArray<Params>): Promise<void> {
        const context = {}

        for (const intercept of this._intercepts) {
            if (intercept.call) {
                await intercept.call(context, ...params)
            }
        }

        for (const tap of this._taps) {
            const args = tap.enableContext ? [context, ...params] : [...params]

            // @ts-expect-error TypeScript things.
            await tap.func(...args)
        }
    }
}