/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.util;

/**
 * Acts like forever growing T[], but internally uses a circular buffer to reuse instances of T.
 *
 * @lucene.internal
 */
public abstract class RollingBuffer<T extends RollingBuffer.Resettable> {

  /** Implement to reset an instance */
  public static interface Resettable {
    public void reset();
  }

  @SuppressWarnings("unchecked")
  private T[] buffer = (T[]) new RollingBuffer.Resettable[8];

  // Next array index to write to:
  private int nextWrite;

  // Next position to write:
  private int nextPos;

  // How many valid Position are held in the
  // array:
  private int count;

  public RollingBuffer() {
    for (int idx = 0; idx < buffer.length; idx++) {
      buffer[idx] = newInstance();
    }
  }

  protected abstract T newInstance();

  public void reset() {
    nextWrite--;
    while (count > 0) {
      if (nextWrite == -1) {
        nextWrite = buffer.length - 1;
      }
      buffer[nextWrite--].reset();
      count--;
    }
    nextWrite = 0;
    nextPos = 0;
    count = 0;
  }

  // For assert:
  private boolean inBounds(int pos) {
    return pos < nextPos && pos >= nextPos - count;
  }

  private int getIndex(int pos) {
    int index = nextWrite - (nextPos - pos);
    if (index < 0) {
      index += buffer.length;
    }
    return index;
  }

  /**
   * Get T instance for this absolute position; this is allowed to be arbitrarily far "in the
   * future" but cannot be before the last freeBefore.
   */
  public T get(int pos) {
    // System.out.println("RA.get pos=" + pos + " nextPos=" + nextPos + " nextWrite=" + nextWrite +
    // " count=" + count);
    while (pos >= nextPos) {
      if (count == buffer.length) {
        @SuppressWarnings("unchecked")
        T[] newBuffer =
            (T[])
                new Resettable
                    [ArrayUtil.oversize(1 + count, RamUsageEstimator.NUM_BYTES_OBJECT_REF)];
        // System.out.println("  grow length=" + newBuffer.length);
        System.arraycopy(buffer, nextWrite, newBuffer, 0, buffer.length - nextWrite);
        System.arraycopy(buffer, 0, newBuffer, buffer.length - nextWrite, nextWrite);
        for (int i = buffer.length; i < newBuffer.length; i++) {
          newBuffer[i] = newInstance();
        }
        nextWrite = buffer.length;
        buffer = newBuffer;
      }
      if (nextWrite == buffer.length) {
        nextWrite = 0;
      }
      // Should have already been reset:
      nextWrite++;
      nextPos++;
      count++;
    }
    assert inBounds(pos) : "pos=" + pos + " nextPos=" + nextPos + " count=" + count;
    final int index = getIndex(pos);
    // System.out.println("  pos=" + pos + " nextPos=" + nextPos + " -> index=" + index);
    // assert buffer[index].pos == pos;
    return buffer[index];
  }

  /**
   * Returns the maximum position looked up, or -1 if no position has been looked up since
   * reset/init.
   */
  public int getMaxPos() {
    return nextPos - 1;
  }

  /** Returns how many active positions are in the buffer. */
  public int getBufferSize() {
    return count;
  }

  public void freeBefore(int pos) {
    final int toFree = count - (nextPos - pos);
    assert toFree >= 0;
    assert toFree <= count : "toFree=" + toFree + " count=" + count;
    int index = nextWrite - count;
    if (index < 0) {
      index += buffer.length;
    }
    for (int i = 0; i < toFree; i++) {
      if (index == buffer.length) {
        index = 0;
      }
      // System.out.println("  fb idx=" + index);
      buffer[index].reset();
      index++;
    }
    count -= toFree;
  }
}
