import { assert } from "../../assert.js";
import { array_push_if_unique } from "../../collection/array/array_push_if_unique.js";
import Signal from "../../events/signal/Signal.js";

import { noop } from "../../function/noop.js";
import ObservedInteger from "../../model/ObservedInteger.js";
import { TaskSignal } from "./TaskSignal.js";
import TaskState from "./TaskState.js";

/**
 *
 * @type {number}
 */
let id_counter = 0;

/**
 * Represents a computation task, where computation is performed in cycles.
 * Each cycle is intended to complete very quickly, and be executed by {@link ConcurrentExecutor}
 */
export class Task {
    /**
     * @readonly
     * @type {number}
     */
    id = id_counter++;

    /**
     * @readonly
     */
    on = {
        /**
         * @readonly
         * @type {Signal}
         */
        started: new Signal(),
        /**
         * @readonly
         * @type {Signal}
         */
        completed: new Signal(),
        /**
         * @readonly
         * @type {Signal}
         */
        failed: new Signal(),
    };

    /**
     *
     * @type {ObservedInteger}
     */
    state = new ObservedInteger(TaskState.INITIAL);

    /**
     * amount of time spent running this task in milliseconds
     * @type {number}
     * @public
     */
    __executedCpuTime = 0;
    /**
     * number of time task's cycle function was executed
     * @type {number}
     * @public
     */
    __executedCycleCount = 0;

    /**
     *
     * @param {string} [name] useful for identifying the task later on, for various UI and debug purposes
     * @param {function(Task, executor:ConcurrentExecutor)} [initializer] function to be executed just before task starts
     * @param {function():TaskSignal} cycleFunction
     * @param {function():number} [computeProgress]
     * @param {Task[]} [dependencies=[]]
     * @param {number} [estimatedDuration=1] in seconds
     * @constructor
     */
    constructor(
        {
            name = "Unnamed",
            initializer = noop,
            cycleFunction,
            computeProgress,
            dependencies = [],
            estimatedDuration = 1
        }
    ) {
        assert.isString(name, 'name');

        assert.isFunction(cycleFunction, 'cycleFunction');
        assert.isFunction(initializer, 'initializer');
        assert.isNumber(estimatedDuration, 'estimatedDuration');

        /**
         *
         * @type {Task[]}
         */
        this.dependencies = dependencies;

        /**
         *
         * @type {number}
         */
        this.estimatedDuration = estimatedDuration;

        /**
         *
         * @type {string}
         */
        this.name = name;

        /**
         *
         * @type {function(): TaskSignal}
         */
        this.cycle = cycleFunction;

        /**
         *
         * @type {function(Task, executor:ConcurrentExecutor)}
         */
        this.initialize = initializer;

        if (computeProgress !== undefined) {

            // override progress function
            this.computeProgress = computeProgress;

        }

    }

    computeProgress() {

        const cycles = this.__executedCycleCount;

        if (cycles === 0) {
            return 0;
        }

        // inverse logarithmic progression, never reaches 1
        return 1 - (1 / cycles);
    }

    /**
     * Time in milliseconds that the task has been executing for, suspended time does not count
     * @returns {number}
     */
    getExecutedCpuTime() {
        return this.__executedCpuTime;
    }

    getEstimatedDuration() {
        return this.estimatedDuration;
    }

    /**
     *
     * @param {Task|TaskGroup} task
     * @returns Task
     */
    addDependency(task) {
        assert.notEqual(task, undefined, 'task is undefined');
        assert.notEqual(task, null, 'task is null');


        if (task.isTaskGroup) {

            //is a task group, add all children instead
            this.addDependencies(task.children);

        } else if (task.isTask) {

            //check that the dependency is not registered yet
            array_push_if_unique(this.dependencies, task);

        } else {
            throw new Error('Expected a Task or a TaskGroup, got something else');
        }

        return this;
    }

    /**
     *
     * @param {Array<(Task|TaskGroup)>} tasks
     */
    addDependencies(tasks) {
        assert.isArray(tasks, 'tasks');

        const task_count = tasks.length;

        for (let i = 0; i < task_count; i++) {
            const task = tasks[i];
            this.addDependency(task);
        }
    }

    toString() {
        return `Task{name:'${this.name}'}`;
    }

    /**
     *
     * @param {function} resolve
     * @param {function} reject
     */
    join(resolve, reject) {
        Task.join(this, resolve, reject);
    }

    /**
     * Run entire task synchronously to completion
     */
    executeSync() {
        this.initialize(this, null);

        this.on.started.send0();

        let s = this.cycle();

        for (; s !== TaskSignal.EndSuccess && s !== TaskSignal.EndFailure; s = this.cycle()) {
            //keep running
        }

        if (s === TaskSignal.EndSuccess) {
            this.on.completed.send0();
        } else if (s === TaskSignal.EndFailure) {
            this.on.failed.send0();
        }

        return s;
    }

    /**
     *
     * @returns {Promise}
     */
    promise() {
        return Task.promise(this);
    }

    /**
     *
     * @param {Array<(Task|TaskGroup)>} tasks
     * @return {Promise}
     */
    static promiseAll(tasks) {
        const promises = tasks.map(Task.promise);

        return Promise.all(promises);
    }

    /**
     *
     * @param {Task|TaskGroup} task
     */
    static promise(task) {
        return new Promise((resolve, reject) => Task.join(task, resolve, reject));
    }

    /**
     *
     * @param {Task} task
     * @param {function} resolve
     * @param {function} reject
     */
    static join(task, resolve, reject) {
        assert.isFunction(resolve, 'resolve');
        assert.isFunction(reject, 'reject');

        const state = task.state.getValue();
        if (state === TaskState.SUCCEEDED) {
            resolve();
        } else if (state === TaskState.FAILED) {
            if (reject !== undefined) {
                reject();
            }
        } else {
            task.on.completed.addOne(resolve);
            if (reject !== undefined) {
                task.on.failed.addOne(reject);
            }
        }
    }

    /**
     *
     * @param {Task[]} tasks
     * @param {function} resolve
     * @param {function} reject
     */
    static joinAll(tasks, resolve, reject) {
        let liveCount = tasks.length;

        if (liveCount === 0) {
            //empty input
            resolve();
            return;
        }

        let failedDispatched = false;

        function cbOK() {
            liveCount--;
            if (liveCount <= 0 && !failedDispatched) {
                resolve();
            }
        }

        function cbFailed() {
            if (!failedDispatched) {
                failedDispatched = true;
                reject(arguments);
            }
        }

        for (let i = 0; i < tasks.length; i++) {
            Task.join(tasks[i], cbOK, cbFailed);
        }
    }
}

/**
 * @readonly
 * @type {boolean}
 */
Task.prototype.isTask = true;


export default Task;
