import Check from "./Check.js";
import defaultValue from "./defaultValue.js";
import defined from "./defined.js";

/**
 * Array implementation of a heap.
 *
 * @alias Heap
 * @constructor
 * @private
 *
 * @param {Object} options Object with the following properties:
 * @param {Heap.ComparatorCallback} options.comparator The comparator to use for the heap. If comparator(a, b) is less than 0, sort a to a lower index than b, otherwise sort to a higher index.
 */
function Heap(options) {
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.object("options", options);
  Check.defined("options.comparator", options.comparator);
  //>>includeEnd('debug');

  this._comparator = options.comparator;
  this._array = [];
  this._length = 0;
  this._maximumLength = undefined;
}

Object.defineProperties(Heap.prototype, {
  /**
   * Gets the length of the heap.
   *
   * @memberof Heap.prototype
   *
   * @type {Number}
   * @readonly
   */
  length: {
    get: function () {
      return this._length;
    },
  },

  /**
   * Gets the internal array.
   *
   * @memberof Heap.prototype
   *
   * @type {Array}
   * @readonly
   */
  internalArray: {
    get: function () {
      return this._array;
    },
  },

  /**
   * Gets and sets the maximum length of the heap.
   *
   * @memberof Heap.prototype
   *
   * @type {Number}
   */
  maximumLength: {
    get: function () {
      return this._maximumLength;
    },
    set: function (value) {
      //>>includeStart('debug', pragmas.debug);
      Check.typeOf.number.greaterThanOrEquals("maximumLength", value, 0);
      //>>includeEnd('debug');
      var originalLength = this._length;
      if (value < originalLength) {
        var array = this._array;
        // Remove trailing references
        for (var i = value; i < originalLength; ++i) {
          array[i] = undefined;
        }
        this._length = value;
        array.length = value;
      }
      this._maximumLength = value;
    },
  },

  /**
   * The comparator to use for the heap. If comparator(a, b) is less than 0, sort a to a lower index than b, otherwise sort to a higher index.
   *
   * @memberof Heap.prototype
   *
   * @type {Heap.ComparatorCallback}
   */
  comparator: {
    get: function () {
      return this._comparator;
    },
  },
});

function swap(array, a, b) {
  var temp = array[a];
  array[a] = array[b];
  array[b] = temp;
}

/**
 * Resizes the internal array of the heap.
 *
 * @param {Number} [length] The length to resize internal array to. Defaults to the current length of the heap.
 */
Heap.prototype.reserve = function (length) {
  length = defaultValue(length, this._length);
  this._array.length = length;
};

/**
 * Update the heap so that index and all descendants satisfy the heap property.
 *
 * @param {Number} [index=0] The starting index to heapify from.
 */
Heap.prototype.heapify = function (index) {
  index = defaultValue(index, 0);
  var length = this._length;
  var comparator = this._comparator;
  var array = this._array;
  var candidate = -1;
  var inserting = true;

  while (inserting) {
    var right = 2 * (index + 1);
    var left = right - 1;

    if (left < length && comparator(array[left], array[index]) < 0) {
      candidate = left;
    } else {
      candidate = index;
    }

    if (right < length && comparator(array[right], array[candidate]) < 0) {
      candidate = right;
    }
    if (candidate !== index) {
      swap(array, candidate, index);
      index = candidate;
    } else {
      inserting = false;
    }
  }
};

/**
 * Resort the heap.
 */
Heap.prototype.resort = function () {
  var length = this._length;
  for (var i = Math.ceil(length / 2); i >= 0; --i) {
    this.heapify(i);
  }
};

/**
 * Insert an element into the heap. If the length would grow greater than maximumLength
 * of the heap, extra elements are removed.
 *
 * @param {*} element The element to insert
 *
 * @return {*} The element that was removed from the heap if the heap is at full capacity.
 */
Heap.prototype.insert = function (element) {
  //>>includeStart('debug', pragmas.debug);
  Check.defined("element", element);
  //>>includeEnd('debug');

  var array = this._array;
  var comparator = this._comparator;
  var maximumLength = this._maximumLength;

  var index = this._length++;
  if (index < array.length) {
    array[index] = element;
  } else {
    array.push(element);
  }

  while (index !== 0) {
    var parent = Math.floor((index - 1) / 2);
    if (comparator(array[index], array[parent]) < 0) {
      swap(array, index, parent);
      index = parent;
    } else {
      break;
    }
  }

  var removedElement;

  if (defined(maximumLength) && this._length > maximumLength) {
    removedElement = array[maximumLength];
    this._length = maximumLength;
  }

  return removedElement;
};

/**
 * Remove the element specified by index from the heap and return it.
 *
 * @param {Number} [index=0] The index to remove.
 * @returns {*} The specified element of the heap.
 */
Heap.prototype.pop = function (index) {
  index = defaultValue(index, 0);
  if (this._length === 0) {
    return undefined;
  }
  //>>includeStart('debug', pragmas.debug);
  Check.typeOf.number.lessThan("index", index, this._length);
  //>>includeEnd('debug');

  var array = this._array;
  var root = array[index];
  swap(array, index, --this._length);
  this.heapify(index);
  array[this._length] = undefined; // Remove trailing reference
  return root;
};

/**
 * The comparator to use for the heap.
 * @callback Heap.ComparatorCallback
 * @param {*} a An element in the heap.
 * @param {*} b An element in the heap.
 * @returns {Number} If the result of the comparison is less than 0, sort a to a lower index than b, otherwise sort to a higher index.
 */
export default Heap;
