import { assert } from "../../core/assert.js";
import BinaryHeap from "../../core/collection/heap/BinaryHeap.js";
import { noop } from "../../core/function/noop.js";

/**
 * @readonly
 * @type {{high: number, auto: number, low: number}}
 */
const priority_value_map = {
    "high": 10,
    "low": -1,
    "auto": 0
};

/**
 *
 * @param {string|undefined} p
 * @return {number}
 */
function extract_priority_from_resource(p) {

    let v = 0;

    if (typeof p === "string") {
        v = priority_value_map[p];

        if (v === undefined) {
            console.warn(`Unsupported request priority value '${p}', defaulting to 'auto'`);
            v = priority_value_map['auto'];
        }
    }

    return v;

}

class FetchRequest {
    /**
     *
     * @param {Request|string} resource
     * @param {{}} options
     */
    constructor(resource, options) {
        let priority = 0;

        this.__resource = resource;
        this.__options = options;

        if (typeof this.__resource === "object" && typeof this.__resource.priority !== "undefined") {
            priority = extract_priority_from_resource(this.__resource.priority);
        }

        this.__priority = priority;

        this.fulfillment = {
            resolve: null, reject: null
        };

        this.promise = new Promise((resolve, reject) => {
            this.fulfillment.resolve = resolve;
            this.fulfillment.reject = reject;
        });
    }
}

/**
 *
 * @param {FetchRequest} request
 * @returns {number}
 */
function get_priority_score(request) {
    return -request.__priority;
}

/**
 * @readonly
 * @type {number}
 */
const DEFAULT_CONCURRENCY = Infinity;

/**
 * Wraps {@link fetch} api and adds concurrency management to it by queueing requests,
 * as well as prioritization in that queue
 */
export class PriorityFetch {
    constructor({
                    adapter = noop,
                    concurrency = DEFAULT_CONCURRENCY
                }) {
        /**
         *
         * @type {number}
         * @private
         */
        this.__concurrency = concurrency;

        /**
         *
         * @type {BinaryHeap<FetchRequest>}
         * @private
         */
        this.__queue = new BinaryHeap(get_priority_score);

        /**
         *
         * @type {Set<FetchRequest>}
         * @private
         */
        this.__pending_set = new Set();

        /**
         *
         * @type {null}
         * @private
         */
        this.__adapter = adapter;
    }

    /**
     *
     * @param {function} v
     */
    set adapter(v) {
        this.__adapter = v;
    }

    /**
     *
     * @param {number} v
     */
    set concurrency(v) {
        assert.isNumber(v, 'v');
        assert.notNaN(v, 'v');
        assert.greaterThan(v, 0, 'concurrency must be greater than 0');

        this.__concurrency = v;
    }

    /**
     *
     * @return {number}
     */
    get concurrency() {
        return this.__concurrency;
    }

    /**
     *
     * @param {FetchRequest} req
     * @private
     */
    __dispatch(req) {
        this.__pending_set.add(req);

        const promise = this.__adapter(req.__resource, req.__options);

        promise.finally(() => {
            this.__pending_set.delete(req);

            this.__prod();
        });

        promise
            .then(
                (v) => {
                    req.fulfillment.resolve(v);
                },
                (reason) => {
                    req.fulfillment.reject(reason);
                }
            );
    }

    __prod() {
        if (this.__pending_set.size < this.__concurrency && !this.__queue.isEmpty()) {
            const req = this.__queue.pop();

            this.__dispatch(req);
        }
    }

    fetch(resource, options) {
        const req = new FetchRequest(resource, options);

        if (this.__pending_set.size < this.__concurrency) {
            this.__dispatch(req);
        } else {
            this.__queue.push(req);
        }

        return req.promise;
    }
}
