window.RING_BUFF_CONSECUTIVE_SET = 0;
window.RING_BUFF_NON_CONSECUTIVE_SET = 1;
window.RING_BUFF_FAILED_TO_SET = 2;

var RingBuffer = function(capacity) {
  this.ed = 0; // write index, open index
  this.st = 0; // read index, closed index
  this.edFrameId = 0;
  this.stFrameId = 0;
  this.n = capacity;
  this.cnt = 0; // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods
  this.eles = new Array(capacity).fill(null);
};

RingBuffer.prototype.put = function(item) {
  let firstPopped = null;
  while (0 < this.cnt && this.cnt >= this.n) {
    // Make room for the new element
    const popped = this.pop();
    if (null == firstPopped)
      firstPopped = popped;
  }
  this.eles[this.ed] = item
  this.edFrameId++;
  this.cnt++;
  this.ed++;
  if (this.ed >= this.n) {
    this.ed -= this.n; // Deliberately not using "%" operator for performance concern
  }
  return firstPopped;
};

RingBuffer.prototype.pop = function() {
  if (0 == this.cnt) {
    return null;
  }
  const item = this.eles[this.st];
  this.stFrameId++;
  this.cnt--;
  this.st++;
  if (this.st >= this.n) {
    this.st -= this.n;
  }
  return item;
};

RingBuffer.prototype.getArrIdxByOffset = function(offsetFromSt) {
  if (0 > offsetFromSt || 0 == this.cnt) {
    return null;
  }
  let arrIdx = this.st + offsetFromSt;
  if (this.st < this.ed) {
    // case#1: 0...st...ed...n-1
    if (this.st <= arrIdx && arrIdx < this.ed) {
      return arrIdx;
    }
  } else {
    // if this.st >= this.sd
    // case#2: 0...ed...st...n-1
    if (arrIdx >= this.n) {
      arrIdx -= this.n
    }
    if (arrIdx >= this.st || arrIdx < this.ed) {
      return arrIdx;
    }
  }

  return null;
};

RingBuffer.prototype.getByFrameId = function(frameId) {
  if (frameId >= this.edFrameId || frameId < this.stFrameId) return null;
  const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
  return (null == arrIdx ? null : this.eles[arrIdx]);
};

// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. 
RingBuffer.prototype.setByFrameId = function(item, frameId) {
  const oldStFrameId = this.stFrameId,
    oldEdFrameId = this.edFrameId;
  if (frameId < oldStFrameId) {
    return [window.RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId];
  }
  // By now "this.stFrameId <= frameId"

  if (oldEdFrameId > frameId) {
    const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId);
    if (null != arrIdx) {
      this.eles[arrIdx] = item;
      return [window.RING_BUFF_CONSECUTIVE_SET, oldStFrameId, oldEdFrameId];
    }
  }

  // By now "this.edFrameId <= frameId"
  let ret = window.RING_BUFF_CONSECUTIVE_SET;
  if (oldEdFrameId < frameId) {
    this.st = this.ed = 0;
    this.stFrameId = this.edFrameId = frameId;
    this.cnt = 0;
    ret = window.RING_BUFF_NON_CONSECUTIVE_SET;
  }

  // By now "this.edFrameId == frameId" 
  this.put(item);

  return [ret, oldStFrameId, oldEdFrameId];
};

module.exports = RingBuffer;
