export { PriorityQueue };

/**
 * A min-priority queue data structure. This algorithm is derived from Cormen,
 * et al., "Introduction to Algorithms". The basic idea of a min-priority
 * queue is that you can efficiently (in O(1) time) get the smallest key in
 * the queue. Adding and removing elements takes O(log n) time. A key can
 * have its priority decreased in O(log n) time.
 */
class PriorityQueue {
  constructor() {
    /**
     * @private
     * @type {Array<{key: string, priority: number}>}
     */
    this._arr = [];
    /**
     * @private
     * @type {Record<string, number>}
     */
    this._keyIndices = {};
  }
  /**
   * @returns {number} the number of elements in the queue.
   * @remarks Takes `O(1)` time.
   */
  size() {
    return this._arr.length;
  }
  /**
   * @returns {string[]} the keys that are in the queue.
   * @remarks Takes `O(n)` time.
   */
  keys() {
    return this._arr.map(function (x) {
      return x.key;
    });
  }
  /**
   * @param {Object} key - The key to check for presence in the queue.
   * @returns {boolean} `true` if **key** is in the queue and `false` if not.
   */
  has(key) {
    return Object.prototype.hasOwnProperty.call(this._keyIndices, key);
  }
  /**
   * @param {Object} key - The key to get the priority for.
   * @returns {number | undefined} the priority for **key**.
   * If **key** is not present in the queue then this function returns `undefined`.
   * @remarks Takes `O(1)` time.
   */
  priority(key) {
    var index = this._keyIndices[key];
    if (index !== undefined) {
      return this._arr[index].priority;
    }
  }
  /**
   * @returns {string} the key for the minimum element in this queue.
   * @throws {Error} if the queue is empty.
   * @remarks Takes `O(1)` time.
   */
  min() {
    if (this.size() === 0) {
      throw new Error('Queue underflow');
    }
    return this._arr[0].key;
  }
  /**
   * Inserts a new key into the priority queue.
   *
   * @remarks Takes `O(n)` time.
   *
   * @param {Object} key the key to add. This will be coerced to a `string`.
   * @param {Number} priority the initial priority for the key
   * @returns {boolean} `true` if the key was added and `false` if it was already
   * present in the queue.
   */
  add(key, priority) {
    var keyIndices = this._keyIndices;
    key = String(key);
    if (!Object.prototype.hasOwnProperty.call(keyIndices, key)) {
      var arr = this._arr;
      var index = arr.length;
      keyIndices[key] = index;
      arr.push({ key: key, priority: priority });
      this._decrease(index);
      return true;
    }
    return false;
  }
  /**
   * Removes and returns the smallest key in the queue.
   * @returns {string} the key with the smallest priority
   * @remarks Takes `O(log n)` time.
   */
  removeMin() {
    this._swap(0, this._arr.length - 1);
    var min = this._arr.pop();
    delete this._keyIndices[min.key];
    this._heapify(0);
    return min.key;
  }
  /**
   * Decreases the priority for **key** to **priority**.
   *
   * @param {Object} key the key for which to raise priority
   * @param {Number} priority the new priority for the key
   * @throws {Error} if the new priority is  greater than the previous priority.
   */
  decrease(key, priority) {
    var index = this._keyIndices[key];
    if (priority > this._arr[index].priority) {
      throw new Error(
        'New priority is greater than current priority. ' +
          'Key: ' +
          key +
          ' Old: ' +
          this._arr[index].priority +
          ' New: ' +
          priority,
      );
    }
    this._arr[index].priority = priority;
    this._decrease(index);
  }
  /**
   * @param {number} i - Lower index.
   * @private
   */
  _heapify(i) {
    var arr = this._arr;
    var l = 2 * i;
    var r = l + 1;
    var largest = i;
    if (l < arr.length) {
      largest = arr[l].priority < arr[largest].priority ? l : largest;
      if (r < arr.length) {
        largest = arr[r].priority < arr[largest].priority ? r : largest;
      }
      if (largest !== i) {
        this._swap(i, largest);
        this._heapify(largest);
      }
    }
  }
  /**
   * @param {number} index - Index to decrease.
   * @private
   */
  _decrease(index) {
    var arr = this._arr;
    var priority = arr[index].priority;
    var parent;
    while (index !== 0) {
      parent = index >> 1;
      if (arr[parent].priority < priority) {
        break;
      }
      this._swap(index, parent);
      index = parent;
    }
  }
  /**
   * @param {number} i - First index
   * @param {number} j - Second index
   * @private
   */
  _swap(i, j) {
    var arr = this._arr;
    var keyIndices = this._keyIndices;
    var origArrI = arr[i];
    var origArrJ = arr[j];
    arr[i] = origArrJ;
    arr[j] = origArrI;
    keyIndices[origArrJ.key] = i;
    keyIndices[origArrI.key] = j;
  }
}
