/*
 * OpenSeadragon - Queue
 *
 * Copyright (C) 2024 OpenSeadragon contributors (modified)
 * Copyright (C) Google Inc., The Closure Library Authors.
 * https://github.com/google/closure-library
 *
 * SPDX-License-Identifier: Apache-2.0
 */

(function($) {

/**
 * @class PriorityQueue
 * @classdesc Fast priority queue. Implemented as a Heap.
 */
$.PriorityQueue = class {

    /**
     * @param {?OpenSeadragon.PriorityQueue} optHeap Optional Heap or
     *     Object to initialize heap with.
     */
    constructor(optHeap = undefined) {
        /**
         * The nodes of the heap.
         *
         * This is a densely packed array containing all nodes of the heap, using
         * the standard flat representation of a tree as an array (i.e. element [0]
         * at the top, with [1] and [2] as the second row, [3] through [6] as the
         * third, etc). Thus, the children of element `i` are `2i+1` and `2i+2`, and
         * the parent of element `i` is `⌊(i-1)/2⌋`.
         *
         * The only invariant is that children's keys must be greater than parents'.
         *
         * @private @const {!Array<!Node>}
         */
        this.nodes_ = [];

        if (optHeap) {
            this.insertAll(optHeap);
        }
    }

    /**
     * Insert the given value into the heap with the given key.
     * @param {K} key The key.
     * @param {V} value The value.
     */
    insert(key, value) {
        this.insertNode(new Node(key, value));
    }

    /**
     * Insert node item.
     * @param node
     */
    insertNode(node) {
        const nodes = this.nodes_;
        node.index = nodes.length;
        nodes.push(node);
        this.moveUp_(node.index);
    }

    /**
     * Adds multiple key-value pairs from another Heap or Object
     * @param {?OpenSeadragon.PriorityQueue} heap Object containing the data to add.
     */
    insertAll(heap) {
        let keys, values;
        if (heap instanceof $.PriorityQueue) {
            keys = heap.getKeys();
            values = heap.getValues();

            // If it is a heap and the current heap is empty, I can rely on the fact
            // that the keys/values are in the correct order to put in the underlying
            // structure.
            if (this.getCount() <= 0) {
                const nodes = this.nodes_;
                for (let i = 0; i < keys.length; i++) {
                    const node = new Node(keys[i], values[i]);
                    node.index = nodes.length;
                    nodes.push(node);
                }
                return;
            }
        } else {
            throw "insertAll supports only OpenSeadragon.PriorityQueue object!";
        }

        for (let i = 0; i < keys.length; i++) {
            this.insert(keys[i], values[i]);
        }
    }

    /**
     * Retrieves and removes the root value of this heap.
     * @return {Node} The root node item removed from the root of the heap. Returns
     *     undefined if the heap is empty.
     */
    remove() {
        const nodes = this.nodes_;
        const count = nodes.length;
        const rootNode = nodes[0];
        if (count <= 0) {
            return undefined;
        } else if (count == 1) {  // eslint-disable-line
            nodes.length = 0;
        } else {
            nodes[0] = nodes.pop();
            if (nodes[0]) {
                nodes[0].index = 0;
            }
            this.moveDown_(0);
        }
        if (rootNode) {
            delete rootNode.index;
        }
        return rootNode;
    }

    /**
     * Retrieves but does not remove the root value of this heap.
     * @return {V} The value at the root of the heap. Returns
     *     undefined if the heap is empty.
     */
    peek() {
        const nodes = this.nodes_;
        if (nodes.length == 0) {  // eslint-disable-line
            return undefined;
        }
        return nodes[0].value;
    }

    /**
     * Retrieves but does not remove the key of the root node of this heap.
     * @return {string} The key at the root of the heap. Returns undefined if the
     *     heap is empty.
     */
    peekKey() {
        return this.nodes_[0] && this.nodes_[0].key;
    }

    /**
     * Move the node up in hierarchy
     * @param {Node} node the node
     * @param {K} key new ley, must be smaller than current key
     */
    decreaseKey(node, key) {
        if (node.index === undefined) {
            node.key = key;
            this.insertNode(node);
        } else {
            node.key = key;
            this.moveUp_(node.index);
        }
    }

    /**
     * Moves the node at the given index down to its proper place in the heap.
     * @param {number} index The index of the node to move down.
     * @private
     */
    moveDown_(index) {
        const nodes = this.nodes_;
        const count = nodes.length;

        // Save the node being moved down.
        const node = nodes[index];
        // While the current node has a child.
        while (index < (count >> 1)) {
            const leftChildIndex = this.getLeftChildIndex_(index);
            const rightChildIndex = this.getRightChildIndex_(index);

            // Determine the index of the smaller child.
            const smallerChildIndex = rightChildIndex < count &&
            nodes[rightChildIndex].key < nodes[leftChildIndex].key ?
                rightChildIndex :
                leftChildIndex;

            // If the node being moved down is smaller than its children, the node
            // has found the correct index it should be at.
            if (nodes[smallerChildIndex].key > node.key) {
                break;
            }

            // If not, then take the smaller child as the current node.
            nodes[index] = nodes[smallerChildIndex];
            nodes[index].index = index;
            index = smallerChildIndex;
        }
        nodes[index] = node;
        if (node) {
            node.index = index;
        }
    }

    /**
     * Moves the node at the given index up to its proper place in the heap.
     * @param {number} index The index of the node to move up.
     * @private
     */
    moveUp_(index) {
        const nodes = this.nodes_;
        const node = nodes[index];

        // While the node being moved up is not at the root.
        while (index > 0) {
            // If the parent is greater than the node being moved up, move the parent
            // down.
            const parentIndex = this.getParentIndex_(index);
            if (nodes[parentIndex].key > node.key) {
                nodes[index] = nodes[parentIndex];
                nodes[index].index = index;
                index = parentIndex;
            } else {
                break;
            }
        }
        nodes[index] = node;
        if (node) {
            node.index = index;
        }
    }

    /**
     * Gets the index of the left child of the node at the given index.
     * @param {number} index The index of the node to get the left child for.
     * @return {number} The index of the left child.
     * @private
     */
    getLeftChildIndex_(index) {
        return index * 2 + 1;
    }

    /**
     * Gets the index of the right child of the node at the given index.
     * @param {number} index The index of the node to get the right child for.
     * @return {number} The index of the right child.
     * @private
     */
    getRightChildIndex_(index) {
        return index * 2 + 2;
    }

    /**
     * Gets the index of the parent of the node at the given index.
     * @param {number} index The index of the node to get the parent for.
     * @return {number} The index of the parent.
     * @private
     */
    getParentIndex_(index) {
        return (index - 1) >> 1;
    }

    /**
     * Gets the values of the heap.
     * @return {!Array<*>} The values in the heap.
     */
    getValues() {
        const nodes = this.nodes_;
        const rv = [];
        const l = nodes.length;
        for (let i = 0; i < l; i++) {
            rv.push(nodes[i].value);
        }
        return rv;
    }

    /**
     * Gets the keys of the heap.
     * @return {!Array<string>} The keys in the heap.
     */
    getKeys() {
        const nodes = this.nodes_;
        const rv = [];
        const l = nodes.length;
        for (let i = 0; i < l; i++) {
            rv.push(nodes[i].key);
        }
        return rv;
    }

    /**
     * Whether the heap contains the given value.
     * @param {V} val The value to check for.
     * @return {boolean} Whether the heap contains the value.
     */
    containsValue(val) {
        return this.nodes_.some((node) => node.value == val);  // eslint-disable-line
    }

    /**
     * Whether the heap contains the given key.
     * @param {string} key The key to check for.
     * @return {boolean} Whether the heap contains the key.
     */
    containsKey(key) {
        return this.nodes_.some((node) => node.value == key);  // eslint-disable-line
    }

    /**
     * Clones a heap and returns a new heap
     * @return {!OpenSeadragon.PriorityQueue} A new Heap with the same key-value pairs.
     */
    clone() {
        return new $.PriorityQueue(this);
    }

    /**
     * The number of key-value pairs in the map
     * @return {number} The number of pairs.
     */
    getCount() {
        return this.nodes_.length;
    }

    /**
     * Returns true if this heap contains no elements.
     * @return {boolean} Whether this heap contains no elements.
     */
    isEmpty() {
        return this.nodes_.length === 0;
    }

    /**
     * Removes all elements from the heap.
     */
    clear() {
        this.nodes_.length = 0;
    }
};

$.PriorityQueue.Node = class {
    constructor(key, value) {
        /**
         * The key.
         * @private {K}
         */
        this.key = key;

        /**
         * The value.
         * @private {V}
         */
        this.value = value;

        /**
         * The node index value. Updated in the heap.
         * @type {number}
         * @private
         */
        this.index = 0;
    }

    clone() {
        return new Node(this.key, this.value);
    }
};

}(OpenSeadragon));
