import { assert } from "../../assert.js";

/**
 * Min-Heap implementation with a score function.
 * Elements are removed in ascending order by their score, meaning that the item with the lowest score is removed first.
 * The data structure is a binary heap where elements are removed in order defined by scoring function.
 *
 * The most common use case for this structure is for a priority queue.
 *
 * @template T
 * @see Uint32Heap
 */
class BinaryHeap {
    /**
     * @private
     * @type {T[]}
     */
    data = [];

    /**
     * @private
     * @type {number}
     */
    length = 0;

    /**
     * @template T
     * @param {function(el:T):number} min_score_function
     * @constructor
     */
    constructor(min_score_function) {

        assert.isFunction(min_score_function, "scoreFunction");

        /**
         *
         * @type {function(T): number}
         */
        this.scoreFunction = min_score_function;

    }

    /**
     * @private
     * @param {number} pos
     */
    bubbleUp(pos) {
        assert.isNumber(pos, "pos");

        const data = this.data;

        let parentIndex;

        let index = pos;

        const value = data[index];
        const score = this.scoreFunction(value);

        assert.isNumber(score, "score");

        while (index > 0) {

            parentIndex = (index - 1) >> 1;

            const parentValue = data[parentIndex];
            const parentScore = this.scoreFunction(parentValue);

            assert.isNumber(parentScore, "parentScore");

            if (score < parentScore) {
                //swap

                data[parentIndex] = value;
                data[index] = parentValue;

                index = parentIndex;
            } else {
                break;
            }
        }
    }

    /**
     * @private
     * @param {number} pos
     */
    bubbleDown(pos) {
        assert.isNonNegativeInteger(pos, 'pos');

        const data = this.data;
        const length = this.length;

        let index = pos;

        let minIndex = index;

        for (; ;) {
            const left = (index << 1) + 1;

            if (left >= length) {
                //index is a leaf node
                break;
            }

            const right = left + 1;

            let dataMin = data[minIndex];
            const scoreMin = this.scoreFunction(dataMin);

            const dataLeft = data[left];
            const scoreLeft = this.scoreFunction(dataLeft);

            if (right >= length) {
                //right node doesn't exist

                if (scoreLeft >= scoreMin) {
                    break;
                } else {
                    minIndex = left;
                    dataMin = dataLeft;
                }

            } else {

                //both left and right nodes exist
                const dataRight = data[right];
                const scoreRight = this.scoreFunction(dataRight);

                if (scoreLeft <= scoreRight) {
                    if (scoreLeft >= scoreMin) {
                        break;
                    } else {
                        minIndex = left;
                        dataMin = dataLeft;
                    }
                } else {
                    if (scoreRight >= scoreMin) {
                        break;
                    } else {
                        minIndex = right;
                        dataMin = dataRight;
                    }
                }

            }

            //swap positions
            data[minIndex] = data[index];
            data[index] = dataMin;

            index = minIndex;

        }
    }

    /**
     *
     * @return {T}
     */
    pop() {

        this.length--;

        const new_length = this.length;

        const last = this.data.pop();

        if (new_length === 0) {

            // this was the last element in the heap

            return last;

        }

        const ret = this.data[0];

        this.data[0] = last;

        this.bubbleDown(0);

        return ret;

    }

    /**
     *
     * @returns {T|undefined}
     */
    peek() {
        return this.data[0];
    }

    /**
     *
     * @param {T} node
     * @returns {boolean}
     */
    delete(node) {
        const i = this.data.indexOf(node);

        if (i === -1) {
            return false;
        }

        this.deleteByIndex(i);

        return true;
    }

    /**
     * NOTE: Unsafe method, use at your own risk
     * @param {number} i
     */
    deleteByIndex(i) {

        this.data.splice(i, 1);

        this.length--;
    }

    /**
     * Remove all the data from the heap
     */
    clear() {
        this.data = [];
        this.length = 0;
    }

    /**
     *
     * @param {T} node
     * @returns {boolean}
     */
    contains(node) {
        const index = this.data.indexOf(node);
        return index !== -1;
    }

    /**
     *
     * @returns {boolean}
     */
    isEmpty() {
        return this.length === 0;
    }

    /**
     *
     * @returns {number}
     */
    size() {
        return this.length;
    }

    /**
     *
     * @param {T} node
     * @returns {boolean} false if element is not found, true otherwise
     */
    updateElementScore(node) {
        const index = this.data.indexOf(node);

        if (index === -1) {
            return false;
        }

        this.bubbleDown(index);
        return true;
    }

    /**
     *
     * @param {T} el
     */
    push(el) {
        this.data.push(el);

        const position = this.length;

        this.length = position + 1;

        this.bubbleUp(position);
    }
}

/**
 * Useful for type checks
 * @readonly
 * @type {boolean}
 */
BinaryHeap.prototype.isBinaryHeap = true;

export default BinaryHeap;
