import { createExternallyResolvablePromise, Cancellation, } from "@intelligentgraphics/ig.utilities";
import { IGXE } from "@intelligentgraphics/igd.core";
var OperationPriority;
(function (OperationPriority) {
    OperationPriority[OperationPriority["Normal"] = 0] = "Normal";
    OperationPriority[OperationPriority["Parent"] = 1] = "Parent";
    OperationPriority[OperationPriority["Final"] = 2] = "Final";
})(OperationPriority || (OperationPriority = {}));
const withCleanup = (func, cleanup) => (...args) => func(...args)
    .then(res => {
    cleanup();
    return res;
})
    .catch(err => {
    cleanup();
    return Promise.reject(err);
});
export const createScheduler = (articleQuery) => {
    const comIdQueue = new Map();
    const comIdStatus = new Map();
    const next = (comId) => {
        const queue = comIdQueue.get(comId);
        if (queue === undefined || queue.length === 0) {
            comIdQueue.delete(comId);
            comIdStatus.delete(comId);
            return;
        }
        comIdStatus.set(comId, true);
        let nextIndex = queue.findIndex(item => item.priority === OperationPriority.Final);
        if (nextIndex === -1) {
            nextIndex = queue.findIndex(item => item.priority === OperationPriority.Parent);
        }
        if (nextIndex === -1) {
            nextIndex = 0;
        }
        const nextItem = queue.splice(nextIndex, 1)[0];
        nextItem.callback();
    };
    const addQueueItem = (comId, type, data, priority, callback) => new Promise((resolve, reject) => {
        const cleanup = () => {
            ownOperationDone.resolve();
            next(comId);
        };
        const article = articleQuery.getArticleByComId(comId);
        if (article === undefined) {
            return reject(new Error(`Refusing to queue an operation for unknown article "${comId}"`));
        }
        const ownOperationDone = createExternallyResolvablePromise();
        const children = articleQuery.getDeepArticleChildren(comId);
        const childReadyPromise = Promise.all(children.map(comId => new Promise((resolve, reject) => addQueueItem(comId, type, undefined, OperationPriority.Parent, () => {
            resolve();
            return ownOperationDone.promise;
        }).then(resolve, resolve))));
        const processItem = withCleanup((err) => err !== undefined
            ? Promise.reject(err).catch(reject)
            : childReadyPromise
                .then(callback)
                .then(resolve, reject), cleanup);
        const item = {
            comId,
            data,
            type,
            priority,
            callback: processItem,
        };
        let queue = comIdQueue.get(item.comId);
        if (queue === undefined) {
            queue = [];
            comIdQueue.set(item.comId, queue);
        }
        queue.push(item);
        if (comIdStatus.get(comId)) {
            return;
        }
        next(item.comId);
    });
    const queueOperation = (comId, callback) => addQueueItem(comId, "any", undefined, OperationPriority.Normal, callback);
    const queueConfigureOperation = (comId, changes, callback) => addQueueItem(comId, "configuration", changes, OperationPriority.Normal, () => {
        let accumulatedChanges = changes;
        const skippedConfigurations = [];
        const queue = comIdQueue.get(comId);
        if (queue !== undefined && queue.length > 0) {
            let skipped = 0;
            const currentQueueLength = queue.length;
            for (let index = 0; index < currentQueueLength; index++) {
                const entry = queue[index - skipped];
                if (entry.type === "configuration") {
                    skippedConfigurations.push(entry.callback);
                    queue.splice(index - skipped, 1);
                    skipped++;
                    accumulatedChanges = IGXE.mergePropertyValueChanges(accumulatedChanges, entry.data);
                }
            }
        }
        return callback(accumulatedChanges).then(result => {
            for (const entry of skippedConfigurations) {
                entry(Cancellation.createCancelError(`Configuration request was swallowed by another configuration request`));
            }
            return result;
        });
    });
    const queueDeleteOperation = (comId, callback) => addQueueItem(comId, "deletion", undefined, OperationPriority.Final, () => callback().then(res => {
        const queue = comIdQueue.get(comId);
        if (queue !== undefined) {
            for (const entry of queue) {
                entry.callback(Cancellation.createCancelError(`Operation has been canceled because of article deletion`));
            }
        }
        comIdQueue.delete(comId);
        return res;
    }));
    const queueGfxOperation = (comId, callback) => addQueueItem(comId, "gfx", undefined, OperationPriority.Normal, callback);
    return {
        queueOperation,
        queueConfigureOperation,
        queueDeleteOperation,
        queueGfxOperation,
    };
};
