import { assert } from "../../assert.js";
import LineBuilder from "../../codegen/LineBuilder.js";
import { array_push_if_unique } from "../../collection/array/array_push_if_unique.js";
import Signal from "../../events/signal/Signal.js";
import { objectKeyByValue } from "../../model/object/objectKeyByValue.js";
import ObservedInteger from "../../model/ObservedInteger.js";
import Task from "./Task.js";
import TaskState from "./TaskState.js";

class TaskGroup {
    /**
     *
     * @param {(Task|TaskGroup)[]} subtasks
     * @param {string} [name]
     * @constructor
     */
    constructor(subtasks, name = 'Unnamed') {
        assert.isArray(subtasks, 'subtasks');
        assert.isString(name, 'name');

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

        /**
         *
         * @type {(Task|TaskGroup)[]}
         */
        this.children = subtasks;

        this.on = {
            started: new Signal(),
            completed: new Signal(),
            failed: new Signal()
        };

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

    /**
     * Time in milliseconds that the task has been executing for, suspended time does not count
     * @returns {number}
     */
    getExecutedCpuTime() {
        let result = 0;

        const children = this.children;
        const n = children.length;

        for (let i = 0; i < n; i++) {
            const child = children[i];

            const time = child.getExecutedCpuTime();

            result += time;
        }

        return result;
    }

    /**
     *
     * @param {Task|TaskGroup} child
     * @returns {boolean}
     */
    addChild(child) {
        return array_push_if_unique(this.children, child);
    }

    /**
     *
     * @param {(Task|TaskGroup)[]} children
     */
    addChildren(children) {
        const n = children.length;
        for (let i = 0; i < n; i++) {
            const child = children[i];
            this.addChild(child);
        }
    }

    /**
     *
     * @param {Task|TaskGroup} dependency
     */
    addDependency(dependency) {

        if (dependency.isTaskGroup) {
            this.addDependencies(dependency.children);
        } else {
            const children = this.children;

            const n = children.length;

            for (let i = 0; i < n; i++) {
                const child = children[i];

                child.addDependency(dependency);
            }
        }

    }

    addDependencies(dependencies) {

        const n = dependencies.length;

        for (let i = 0; i < n; i++) {
            const dependency = dependencies[i];

            this.addDependency(dependency);
        }

    }

    /**
     *
     * @returns {number}
     */
    getEstimatedDuration() {
        let result = 0;
        const children = this.children;
        const n = children.length;
        for (let i = 0; i < n; i++) {

            const child = children[i];

            const childDuration = child.getEstimatedDuration();

            if (!(isNaN(childDuration) || childDuration < 0)) {
                result += childDuration;
            }

        }

        return result;
    }

    /**
     * Dumps task group tree along with state of each task
     * @returns {string}
     */
    getVerboseStatusMessage() {
        const b = new LineBuilder();

        /**
         *
         * @param {Task|TaskGroup} t
         */
        function addTask(t) {
            if (t.isTaskGroup) {
                b.add(`group ['${t.name}']`)
                b.indent();
                for (let i = 0; i < t.children.length; i++) {
                    addTask(t.children[i]);
                }
                b.dedent();
            } else {
                b.add(`task ['${t.name}'] ${objectKeyByValue(TaskState, t.state.getValue())}`)
            }
        }

        addTask(this);

        return b.build();
    }

    computeProgress() {
        const children = this.children;
        const numChildren = children.length;

        let progressSum = 0;
        let progressTotal = 0;

        for (let i = 0; i < numChildren; i++) {
            /**
             *
             * @type {Task|TaskGroup}
             */
            const child = children[i];

            const estimatedDuration = child.getEstimatedDuration();

            if (isNaN(estimatedDuration)) {
                //Duration is not a number, ignore this child
                continue;
            }

            const childProgress = computeTaskProgress(child);

            assert.ok(childProgress >= 0 && childProgress <= 1, `Expected progress to be between 0 and 1, instead was '${childProgress}' for child '${child.name}'`);

            progressSum += childProgress * estimatedDuration;
            progressTotal += estimatedDuration;
        }

        if (progressTotal === 0) {
            return 0;
        } else {
            return progressSum / progressTotal;
        }

    }

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

    /**
     *
     * @returns {Promise<unknown>}
     */
    promise() {
        return new Promise((resolve, reject) => this.join(resolve, reject));
    }
}

/**
 * @readonly
 * @type {boolean}
 */
TaskGroup.prototype.isTaskGroup = true;

/**
 *
 * @param {Task|TaskGroup} child
 * @returns {number}
 */
function computeTaskProgress(child) {


    if (child.isTask) {
        const state = child.state.getValue();
        if (state === TaskState.INITIAL || state === TaskState.READY) {
            // not started yet
            return 0;
        }
    }

    return child.computeProgress();
}

export default TaskGroup;
