goog.provide('ol.structs.IntegerSet');

goog.require('goog.asserts');



/**
 * A set of integers represented as a set of integer ranges.
 * This implementation is designed for the case when the number of distinct
 * integer ranges is small.
 * @constructor
 * @param {Array.<number>=} opt_arr Array.
 */
ol.structs.IntegerSet = function(opt_arr) {

  /**
   * @private
   * @type {Array.<number>}
   */
  this.arr_ = goog.isDef(opt_arr) ? opt_arr : [];

  if (goog.DEBUG) {
    this.assertValid();
  }

};


/**
 * @param {number} addStart Start.
 * @param {number} addStop Stop.
 */
ol.structs.IntegerSet.prototype.addRange = function(addStart, addStop) {
  goog.asserts.assert(addStart <= addStop);
  if (addStart == addStop) {
    return;
  }
  var arr = this.arr_;
  var n = arr.length;
  var i;
  for (i = 0; i < n; i += 2) {
    if (addStart <= arr[i]) {
      // FIXME check if splice is really needed
      arr.splice(i, 0, addStart, addStop);
      this.compactRanges_();
      return;
    }
  }
  arr.push(addStart, addStop);
  this.compactRanges_();
};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.IntegerSet.prototype.assertValid = function() {
  var arr = this.arr_;
  var n = arr.length;
  goog.asserts.assert(n % 2 === 0);
  var i;
  for (i = 1; i < n; ++i) {
    goog.asserts.assert(arr[i] > arr[i - 1]);
  }
};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.IntegerSet.prototype.clear = function() {
  this.arr_.length = 0;
};


/**
 * @private
 */
ol.structs.IntegerSet.prototype.compactRanges_ = function() {
  var arr = this.arr_;
  var n = arr.length;
  var rangeIndex = 0;
  var i;
  for (i = 0; i < n; i += 2) {
    if (arr[i] == arr[i + 1]) {
      // pass
    } else if (rangeIndex > 0 &&
               arr[rangeIndex - 2] <= arr[i] &&
               arr[i] <= arr[rangeIndex - 1]) {
      arr[rangeIndex - 1] = Math.max(arr[rangeIndex - 1], arr[i + 1]);
    } else {
      arr[rangeIndex++] = arr[i];
      arr[rangeIndex++] = arr[i + 1];
    }
  }
  arr.length = rangeIndex;
};


/**
 * Finds the start of smallest range that is at least of length minSize, or -1
 * if no such range exists.
 * @param {number} minSize Minimum size.
 * @return {number} Index.
 */
ol.structs.IntegerSet.prototype.findRange = function(minSize) {
  goog.asserts.assert(minSize > 0);
  var arr = this.arr_;
  var n = arr.length;
  var bestIndex = -1;
  var bestSize, i, size;
  for (i = 0; i < n; i += 2) {
    size = arr[i + 1] - arr[i];
    if (size == minSize) {
      return arr[i];
    } else if (size > minSize && (bestIndex == -1 || size < bestSize)) {
      bestIndex = arr[i];
      bestSize = size;
    }
  }
  return bestIndex;
};


/**
 * Calls f with each integer range.
 * @param {function(this: T, number, number)} f Callback.
 * @param {T=} opt_obj The object to be used as the value of 'this' within f.
 * @template T
 */
ol.structs.IntegerSet.prototype.forEachRange = function(f, opt_obj) {
  var arr = this.arr_;
  var n = arr.length;
  var i;
  for (i = 0; i < n; i += 2) {
    f.call(opt_obj, arr[i], arr[i + 1]);
  }
};


/**
 * Calls f with each integer range not in [start, stop) - 'this'.
 * @param {number} start Start.
 * @param {number} stop Stop.
 * @param {function(this: T, number, number)} f Callback.
 * @param {T=} opt_obj The object to be used as the value of 'this' within f.
 * @template T
 */
ol.structs.IntegerSet.prototype.forEachRangeInverted =
    function(start, stop, f, opt_obj) {
  goog.asserts.assert(start < stop);
  var arr = this.arr_;
  var n = arr.length;
  if (n === 0) {
    f.call(opt_obj, start, stop);
  } else {
    if (start < arr[0]) {
      f.call(opt_obj, start, arr[0]);
    }
    var i;
    for (i = 1; i < n - 1; i += 2) {
      f.call(opt_obj, arr[i], arr[i + 1]);
    }
    if (arr[n - 1] < stop) {
      f.call(opt_obj, arr[n - 1], stop);
    }
  }
};


/**
 * @return {Array.<number>} Array.
 */
ol.structs.IntegerSet.prototype.getArray = function() {
  return this.arr_;
};


/**
 * Returns the first element in the set, or -1 if the set is empty.
 * @return {number} Start.
 */
ol.structs.IntegerSet.prototype.getFirst = function() {
  return this.arr_.length === 0 ? -1 : this.arr_[0];
};


/**
 * Returns the first integer after the last element in the set, or -1 if the
 * set is empty.
 * @return {number} Last.
 */
ol.structs.IntegerSet.prototype.getLast = function() {
  var n = this.arr_.length;
  return n === 0 ? -1 : this.arr_[n - 1];
};


/**
 * Returns the number of integers in the set.
 * @return {number} Size.
 */
ol.structs.IntegerSet.prototype.getSize = function() {
  var arr = this.arr_;
  var n = arr.length;
  var size = 0;
  var i;
  for (i = 0; i < n; i += 2) {
    size += arr[i + 1] - arr[i];
  }
  return size;
};


/**
 * @param {number} start Start.
 * @param {number} stop Stop.
 * @return {boolean} Intersects range.
 */
ol.structs.IntegerSet.prototype.intersectsRange = function(start, stop) {
  goog.asserts.assert(start <= stop);
  if (start == stop) {
    return false;
  } else {
    var arr = this.arr_;
    var n = arr.length;
    var i = 0;
    for (i = 0; i < n; i += 2) {
      if (arr[i] <= start && start < arr[i + 1] ||
          arr[i] < stop && stop - 1 < arr[i + 1] ||
          start < arr[i] && arr[i + 1] <= stop) {
        return true;
      }
    }
    return false;
  }
};


/**
 * @return {boolean} Is empty.
 */
ol.structs.IntegerSet.prototype.isEmpty = function() {
  return this.arr_.length === 0;
};


/**
 * @return {Array.<number>} Array.
 */
ol.structs.IntegerSet.prototype.pack = function() {
  return this.arr_;
};


/**
 * @param {number} removeStart Start.
 * @param {number} removeStop Stop.
 */
ol.structs.IntegerSet.prototype.removeRange =
    function(removeStart, removeStop) {
  // FIXME this could be more efficient
  goog.asserts.assert(removeStart <= removeStop);
  var arr = this.arr_;
  var n = arr.length;
  var i;
  for (i = 0; i < n; i += 2) {
    if (removeStop < arr[i] || arr[i + 1] < removeStart) {
      continue;
    } else if (arr[i] > removeStop) {
      break;
    }
    if (removeStart < arr[i]) {
      if (removeStop == arr[i]) {
        break;
      } else if (removeStop < arr[i + 1]) {
        arr[i] = Math.max(arr[i], removeStop);
        break;
      } else {
        arr.splice(i, 2);
        i -= 2;
        n -= 2;
      }
    } else if (removeStart == arr[i]) {
      if (removeStop < arr[i + 1]) {
        arr[i] = removeStop;
        break;
      } else if (removeStop == arr[i + 1]) {
        arr.splice(i, 2);
        break;
      } else {
        arr.splice(i, 2);
        i -= 2;
        n -= 2;
      }
    } else {
      if (removeStop < arr[i + 1]) {
        arr.splice(i, 2, arr[i], removeStart, removeStop, arr[i + 1]);
        break;
      } else if (removeStop == arr[i + 1]) {
        arr[i + 1] = removeStart;
        break;
      } else {
        arr[i + 1] = removeStart;
      }
    }
  }
  this.compactRanges_();
};


if (goog.DEBUG) {

  /**
   * @return {string} String.
   */
  ol.structs.IntegerSet.prototype.toString = function() {
    var arr = this.arr_;
    var n = arr.length;
    var result = new Array(n / 2);
    var resultIndex = 0;
    var i;
    for (i = 0; i < n; i += 2) {
      result[resultIndex++] = arr[i] + '-' + arr[i + 1];
    }
    return result.join(', ');
  };

}
