/**
 * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import { internalSymbol } from "../constants.mjs";
import { Base } from "../types/base.mjs";
import { getGlobalFunction } from "../types/global.mjs";
import { isFunction, isInteger } from "../types/is.mjs";
import { Queue } from "../types/queue.mjs";
import { validateFunction, validateInteger } from "../types/validate.mjs";

export { Processing };

/**
 * @private
 */
class Callback {
    /**
     *
     * @param {function} callback
     * @param {int|undefined} time
     * @throws {TypeError} value is not a function
     * @throws {TypeError} value is not an integer
     * @private
     */
    constructor(callback, time) {
        this[internalSymbol] = {
            callback: validateFunction(callback),
            time: validateInteger(time ?? 0),
        };
    }

    /**
     * @private
     * @param  {*} data
     * @return {Promise}
     */
    run(data) {
        const self = this;
        return new Promise((resolve, reject) => {
            getGlobalFunction("setTimeout")(() => {
                try {
                    resolve(self[internalSymbol].callback(data));
                } catch (e) {
                    reject(e);
                }
            }, self[internalSymbol].time);
        });
    }
}

/**
 * This class allows to execute several functions in order.
 *
 * Functions and timeouts can be passed. If a timeout is passed, it applies to all further functions.
 * In the example
 *
 * `timeout1, function1, function2, function3, timeout2, function4`
 *
 * the timeout1 is valid for the functions 1, 2 and 3 and the timeout2 for the function4.
 *
 * So the execution time is timeout1+timeout1+timeout1+timeout2
 *
 * The result of `run()` is a promise.
 *
 * @externalExample ../../example/util/processing.mjs
 * @copyright schukai GmbH
 * @license AGPLv3
 * @since 1.21.0
 * @memberOf Monster.Util
 * @summary Class to be able to execute function chains
 */
class Processing extends Base {
    /**
     * Create new Processing
     *
     * Functions and timeouts can be passed. If a timeout is passed, it applies to all further functions.
     * In the example
     *
     * `timeout1, function1, function2, function3, timeout2, function4`
     *
     * the timeout1 is valid for the functions 1, 2 and 3 and the timeout2 for the function4.
     *
     * So the execution time is timeout1+timeout1+timeout1+timeout2
     *
     * @param {int} timeout Timeout
     * @param {function} callback Callback
     * @throw {TypeError} the arguments must be either integer or functions
     */
    constructor(...args) {
        super();

        this[internalSymbol] = {
            queue: new Queue(),
        };

        let time = 0;

        if (typeof args !== "object" || args[0] === null) {
            throw new TypeError("the arguments must be either integer or functions");
        }

        for (const [, arg] of Object.entries(args)) {
            if (isInteger(arg) && arg >= 0) {
                time = arg;
            } else if (isFunction(arg)) {
                this[internalSymbol].queue.add(new Callback(arg, time));
            } else {
                throw new TypeError("the arguments must be either integer or functions");
            }
        }
    }

    /**
     * Adds a function with the desired timeout
     * If no timeout is specified, the timeout of the previous function is used.
     *
     * @param {function} callback
     * @param {int|undefined} time
     * @throws {TypeError} value is not a function
     * @throws {TypeError} value is not an integer
     */
    add(callback, time) {
        this[internalSymbol].queue.add(new Callback(callback, time));
        return this;
    }

    /**
     * Executes the defined functions in order.
     *
     * @param {*} data
     * @return {Promise}
     */
    run(data) {
        const self = this;
        if (this[internalSymbol].queue.isEmpty()) {
            return Promise.resolve(data);
        }

        return this[internalSymbol].queue
            .poll()
            .run(data)
            .then((result) => {
                return self.run(result);
            });
    }
}