/**
 * Mnemonist FixedDeque
 * =====================
 *
 * Fixed capacity double-ended queue implemented as ring deque.
 */
var iterables = require('./utils/iterables.js'),
    Iterator = require('obliterator/iterator');

/**
 * FixedDeque.
 *
 * @constructor
 */
function FixedDeque(ArrayClass, capacity) {

  if (arguments.length < 2)
    throw new Error('mnemonist/fixed-deque: expecting an Array class and a capacity.');

  if (typeof capacity !== 'number' || capacity <= 0)
    throw new Error('mnemonist/fixed-deque: `capacity` should be a positive number.');

  this.ArrayClass = ArrayClass;
  this.capacity = capacity;
  this.items = new ArrayClass(this.capacity);
  this.clear();
}

/**
 * Method used to clear the structure.
 *
 * @return {undefined}
 */
FixedDeque.prototype.clear = function() {

  // Properties
  this.start = 0;
  this.size = 0;
};

/**
 * Method used to append a value to the deque.
 *
 * @param  {any}    item - Item to append.
 * @return {number}      - Returns the new size of the deque.
 */
FixedDeque.prototype.push = function(item) {
  if (this.size === this.capacity)
    throw new Error('mnemonist/fixed-deque.push: deque capacity (' + this.capacity + ') exceeded!');

  var index = this.start + this.size;

  if (index >= this.capacity)
    index -= this.capacity;

  this.items[index] = item;

  return ++this.size;
};

/**
 * Method used to prepend a value to the deque.
 *
 * @param  {any}    item - Item to prepend.
 * @return {number}      - Returns the new size of the deque.
 */
FixedDeque.prototype.unshift = function(item) {
  if (this.size === this.capacity)
    throw new Error('mnemonist/fixed-deque.unshift: deque capacity (' + this.capacity + ') exceeded!');

  var index = this.start - 1;

  if (this.start === 0)
    index = this.capacity - 1;

  this.items[index] = item;
  this.start = index;

  return ++this.size;
};

/**
 * Method used to pop the deque.
 *
 * @return {any} - Returns the popped item.
 */
FixedDeque.prototype.pop = function() {
  if (this.size === 0)
    return;

  this.size--;

  var index = this.start + this.size;

  if (index >= this.capacity)
    index -= this.capacity;

  return this.items[index];
};

/**
 * Method used to shift the deque.
 *
 * @return {any} - Returns the shifted item.
 */
FixedDeque.prototype.shift = function() {
  if (this.size === 0)
    return;

  var index = this.start;

  this.size--;
  this.start++;

  if (this.start === this.capacity)
    this.start = 0;

  return this.items[index];
};

/**
 * Method used to peek the first value of the deque.
 *
 * @return {any}
 */
FixedDeque.prototype.peekFirst = function() {
  if (this.size === 0)
    return;

  return this.items[this.start];
};

/**
 * Method used to peek the last value of the deque.
 *
 * @return {any}
 */
FixedDeque.prototype.peekLast = function() {
  if (this.size === 0)
    return;

  var index = this.start + this.size - 1;

  if (index >= this.capacity)
    index -= this.capacity;

  return this.items[index];
};

/**
 * Method used to get the desired value of the deque.
 *
 * @param  {number} index
 * @return {any}
 */
FixedDeque.prototype.get = function(index) {
  if (this.size === 0 || index >= this.capacity)
    return;

  index = this.start + index;

  if (index >= this.capacity)
    index -= this.capacity;

  return this.items[index];
};

/**
 * Method used to iterate over the deque.
 *
 * @param  {function}  callback - Function to call for each item.
 * @param  {object}    scope    - Optional scope.
 * @return {undefined}
 */
FixedDeque.prototype.forEach = function(callback, scope) {
  scope = arguments.length > 1 ? scope : this;

  var c = this.capacity,
      l = this.size,
      i = this.start,
      j = 0;

  while (j < l) {
    callback.call(scope, this.items[i], j, this);
    i++;
    j++;

    if (i === c)
      i = 0;
  }
};

/**
 * Method used to convert the deque to a JavaScript array.
 *
 * @return {array}
 */
// TODO: optional array class as argument?
FixedDeque.prototype.toArray = function() {

  // Optimization
  var offset = this.start + this.size;

  if (offset < this.capacity)
    return this.items.slice(this.start, offset);

  var array = new this.ArrayClass(this.size),
      c = this.capacity,
      l = this.size,
      i = this.start,
      j = 0;

  while (j < l) {
    array[j] = this.items[i];
    i++;
    j++;

    if (i === c)
      i = 0;
  }

  return array;
};

/**
 * Method used to create an iterator over the deque's values.
 *
 * @return {Iterator}
 */
FixedDeque.prototype.values = function() {
  var items = this.items,
      c = this.capacity,
      l = this.size,
      i = this.start,
      j = 0;

  return new Iterator(function() {
    if (j >= l)
      return {
        done: true
      };

    var value = items[i];

    i++;
    j++;

    if (i === c)
      i = 0;

    return {
      value: value,
      done: false
    };
  });
};

/**
 * Method used to create an iterator over the deque's entries.
 *
 * @return {Iterator}
 */
FixedDeque.prototype.entries = function() {
  var items = this.items,
      c = this.capacity,
      l = this.size,
      i = this.start,
      j = 0;

  return new Iterator(function() {
    if (j >= l)
      return {
        done: true
      };

    var value = items[i];

    i++;

    if (i === c)
      i = 0;

    return {
      value: [j++, value],
      done: false
    };
  });
};

/**
 * Attaching the #.values method to Symbol.iterator if possible.
 */
if (typeof Symbol !== 'undefined')
  FixedDeque.prototype[Symbol.iterator] = FixedDeque.prototype.values;

/**
 * Convenience known methods.
 */
FixedDeque.prototype.inspect = function() {
  var array = this.toArray();

  array.type = this.ArrayClass.name;
  array.capacity = this.capacity;

  // Trick so that node displays the name of the constructor
  Object.defineProperty(array, 'constructor', {
    value: FixedDeque,
    enumerable: false
  });

  return array;
};

if (typeof Symbol !== 'undefined')
  FixedDeque.prototype[Symbol.for('nodejs.util.inspect.custom')] = FixedDeque.prototype.inspect;

/**
 * Static @.from function taking an arbitrary iterable & converting it into
 * a deque.
 *
 * @param  {Iterable} iterable   - Target iterable.
 * @param  {function} ArrayClass - Array class to use.
 * @param  {number}   capacity   - Desired capacity.
 * @return {FiniteStack}
 */
FixedDeque.from = function(iterable, ArrayClass, capacity) {
  if (arguments.length < 3) {
    capacity = iterables.guessLength(iterable);

    if (typeof capacity !== 'number')
      throw new Error('mnemonist/fixed-deque.from: could not guess iterable length. Please provide desired capacity as last argument.');
  }

  var deque = new FixedDeque(ArrayClass, capacity);

  if (iterables.isArrayLike(iterable)) {
    var i, l;

    for (i = 0, l = iterable.length; i < l; i++)
      deque.items[i] = iterable[i];

    deque.size = l;

    return deque;
  }

  iterables.forEach(iterable, function(value) {
    deque.push(value);
  });

  return deque;
};

/**
 * Exporting.
 */
module.exports = FixedDeque;
