// SPDX-License-Identifier: LGPL-2.1-or-later
import { is_function, is_object } from './common';

/* ------------------------------------------------------------------------------------
 * An ordered queue of functions that should be called later.
 */

let later_queue = [];
let later_timeout = null;

function later_drain() {
    const queue = later_queue;
    later_timeout = null;
    later_queue = [];
    for (;;) {
        const func = queue.shift();
        if (!func)
            break;
        func();
    }
}

export function later_invoke(func) {
    if (func)
        later_queue.push(func);
    if (later_timeout === null)
        later_timeout = window.setTimeout(later_drain, 0);
}

/* ------------------------------------------------------------------------------------
 * Promises.
 * Based on Q and angular promises, with some jQuery compatibility. See the angular
 * license in COPYING.node for license lineage. There are some key differences with
 * both Q and jQuery.
 *
 *  * Exceptions thrown in handlers are not treated as rejections or failures.
 *    Exceptions remain actual exceptions.
 *  * Unlike jQuery callbacks added to an already completed promise don't execute
 *    immediately. Wait until control is returned to the browser.
 */

function promise_then(state, fulfilled, rejected, updated) {
    if (fulfilled === undefined && rejected === undefined && updated === undefined)
        return null;
    const result = new Deferred();
    state.pending = state.pending || [];
    state.pending.push([result, fulfilled, rejected, updated]);
    if (state.status > 0)
        schedule_process_queue(state);
    return result.promise;
}

function create_promise(state) {
    /* Like jQuery the promise object is callable */
    const self = function Promise(target) {
        if (target) {
            Object.assign(target, self);
            return target;
        }
        return self;
    };

    state.status = 0;

    self.then = function then(fulfilled, rejected, updated) {
        return promise_then(state, fulfilled, rejected, updated) || self;
    };

    self.catch = function catch_(callback) {
        return promise_then(state, null, callback) || self;
    };

    self.finally = function finally_(callback, updated) {
        return promise_then(state, function() {
            return handle_callback(arguments, true, callback);
        }, function() {
            return handle_callback(arguments, false, callback);
        }, updated) || self;
    };

    /* Basic jQuery Promise compatibility */
    self.done = function done(fulfilled) {
        promise_then(state, fulfilled);
        return self;
    };

    self.fail = function fail(rejected) {
        promise_then(state, null, rejected);
        return self;
    };

    self.always = function always(callback) {
        promise_then(state, callback, callback);
        return self;
    };

    self.progress = function progress(updated) {
        promise_then(state, null, null, updated);
        return self;
    };

    self.state = function state_() {
        if (state.status == 1)
            return "resolved";
        if (state.status == 2)
            return "rejected";
        return "pending";
    };

    /* Promises are recursive like jQuery */
    self.promise = self;

    return self;
}

function process_queue(state) {
    const pending = state.pending;
    state.process_scheduled = false;
    state.pending = undefined;
    for (let i = 0, ii = pending.length; i < ii; ++i) {
        state.pur = true;
        const deferred = pending[i][0];
        const fn = pending[i][state.status];
        if (is_function(fn)) {
            deferred.resolve(fn.apply(state.promise, state.values));
        } else if (state.status === 1) {
            deferred.resolve.apply(deferred.resolve, state.values);
        } else {
            deferred.reject.apply(deferred.reject, state.values);
        }
    }
}

function schedule_process_queue(state) {
    if (state.process_scheduled || !state.pending)
        return;
    state.process_scheduled = true;
    later_invoke(function() { process_queue(state) });
}

function deferred_resolve(state, values) {
    let then;
    let done = false;
    if (is_object(values[0]) || is_function(values[0]))
        then = values[0]?.then;
    if (is_function(then)) {
        state.status = -1;
        then.call(values[0], function(/* ... */) {
            if (done)
                return;
            done = true;
            deferred_resolve(state, arguments);
        }, function(/* ... */) {
            if (done)
                return;
            done = true;
            deferred_reject(state, arguments);
        }, function(/* ... */) {
            deferred_notify(state, arguments);
        });
    } else {
        state.values = values;
        state.status = 1;
        schedule_process_queue(state);
    }
}

function deferred_reject(state, values) {
    state.values = values;
    state.status = 2;
    schedule_process_queue(state);
}

function deferred_notify(state, values) {
    const callbacks = state.pending;
    if ((state.status <= 0) && callbacks?.length) {
        later_invoke(function() {
            for (let i = 0, ii = callbacks.length; i < ii; i++) {
                const result = callbacks[i][0];
                const callback = callbacks[i][3];
                if (is_function(callback))
                    result.notify(callback.apply(state.promise, values));
                else
                    result.notify.apply(result, values);
            }
        });
    }
}

export function Deferred() {
    const self = this;
    const state = { };
    self.promise = state.promise = create_promise(state);

    self.resolve = function resolve(/* ... */) {
        if (arguments[0] === state.promise)
            throw new Error("Expected promise to be resolved with other value than itself");
        if (!state.status)
            deferred_resolve(state, arguments);
        return self;
    };

    self.reject = function reject(/* ... */) {
        if (state.status)
            return;
        deferred_reject(state, arguments);
        return self;
    };

    self.notify = function notify(/* ... */) {
        deferred_notify(state, arguments);
        return self;
    };
}

function prep_promise(values, resolved) {
    const result = new Deferred();
    if (resolved)
        result.resolve.apply(result, values);
    else
        result.reject.apply(result, values);
    return result.promise;
}

function handle_callback(values, is_resolved, callback) {
    let callback_output = null;
    if (is_function(callback))
        callback_output = callback();
    if (callback_output && is_function(callback_output.then)) {
        return callback_output.then(function() {
            return prep_promise(values, is_resolved);
        }, function() {
            return prep_promise(arguments, false);
        });
    } else {
        return prep_promise(values, is_resolved);
    }
}
