package org.apache.lucene.util.packed;

/*
 * 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.
 */

import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.LongValues;
import org.apache.lucene.util.RamUsageEstimator;

import java.util.Arrays;

import static org.apache.lucene.util.packed.PackedInts.checkBlockSize;

/** Common functionality shared by {@link AppendingDeltaPackedLongBuffer} and {@link MonotonicAppendingLongBuffer}. */
abstract class AbstractAppendingLongBuffer extends LongValues {

  static final int MIN_PAGE_SIZE = 64;
  // More than 1M doesn't really makes sense with these appending buffers
  // since their goal is to try to have small numbers of bits per value
  static final int MAX_PAGE_SIZE = 1 << 20;

  final int pageShift, pageMask;
  PackedInts.Reader[] values;
  private long valuesBytes;
  int valuesOff;
  long[] pending;
  int pendingOff;
  float acceptableOverheadRatio;

  AbstractAppendingLongBuffer(int initialBlockCount, int pageSize, float acceptableOverheadRatio) {
    values = new PackedInts.Reader[initialBlockCount];
    pending = new long[pageSize];
    pageShift = checkBlockSize(pageSize, MIN_PAGE_SIZE, MAX_PAGE_SIZE);
    pageMask = pageSize - 1;
    valuesOff = 0;
    pendingOff = 0;
    this.acceptableOverheadRatio = acceptableOverheadRatio;
  }

  final int pageSize() {
    return pageMask + 1;
  }

  /** Get the number of values that have been added to the buffer. */
  public final long size() {
    long size = pendingOff;
    if (valuesOff > 0) {
      size += values[valuesOff - 1].size();
    }
    if (valuesOff > 1) {
      size += (long) (valuesOff - 1) * pageSize();
    }
    return size;
  }

  /** Append a value to this buffer. */
  public final void add(long l) {
    if (pending == null) {
      throw new IllegalStateException("This buffer is frozen");
    }
    if (pendingOff == pending.length) {
      // check size
      if (values.length == valuesOff) {
        final int newLength = ArrayUtil.oversize(valuesOff + 1, 8);
        grow(newLength);
      }
      packPendingValues();
      valuesBytes += values[valuesOff].ramBytesUsed();
      ++valuesOff;
      // reset pending buffer
      pendingOff = 0;
    }
    pending[pendingOff++] = l;
  }

  void grow(int newBlockCount) {
    values = Arrays.copyOf(values, newBlockCount);
  }

  abstract void packPendingValues();

  @Override
  public final long get(long index) {
    assert index >= 0 && index < size();
    final int block = (int) (index >> pageShift);
    final int element = (int) (index & pageMask);
    return get(block, element);
  }

  /**
   * Bulk get: read at least one and at most <code>len</code> longs starting
   * from <code>index</code> into <code>arr[off:off+len]</code> and return
   * the actual number of values that have been read.
   */
  public final int get(long index, long[] arr, int off, int len) {
    assert len > 0 : "len must be > 0 (got " + len + ")";
    assert index >= 0 && index < size();
    assert off + len <= arr.length;

    int block = (int) (index >> pageShift);
    int element = (int) (index & pageMask);
    return get(block, element, arr, off, len);
  }


  abstract long get(int block, int element);

  abstract int get(int block, int element, long[] arr, int off, int len);


  /** Return an iterator over the values of this buffer. */
  public Iterator iterator() {
    return new Iterator();
  }

  final public class Iterator {

    long[] currentValues;
    int vOff, pOff;
    int currentCount; // number of entries of the current page

    Iterator() {
      vOff = pOff = 0;
      if (valuesOff == 0) {
        currentValues = pending;
        currentCount = pendingOff;
      } else {
        currentValues = new long[values[0].size()];
        fillValues();
      }
    }

    void fillValues() {
      if (vOff == valuesOff) {
        currentValues = pending;
        currentCount = pendingOff;
      } else {
        currentCount = values[vOff].size();
        for (int k = 0; k < currentCount; ) {
          k += get(vOff, k, currentValues, k, currentCount - k);
        }
      }
    }

    /** Whether or not there are remaining values. */
    public final boolean hasNext() {
      return pOff < currentCount;
    }

    /** Return the next long in the buffer. */
    public final long next() {
      assert hasNext();
      long result = currentValues[pOff++];
      if (pOff == currentCount) {
        vOff += 1;
        pOff = 0;
        if (vOff <= valuesOff) {
          fillValues();
        } else {
          currentCount = 0;
        }
      }
      return result;
    }

  }

  long baseRamBytesUsed() {
    return RamUsageEstimator.NUM_BYTES_OBJECT_HEADER
        + 2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF // the 2 arrays
        + 2 * RamUsageEstimator.NUM_BYTES_INT // the 2 offsets
        + 2 * RamUsageEstimator.NUM_BYTES_INT // pageShift, pageMask
        + RamUsageEstimator.NUM_BYTES_FLOAT   // acceptable overhead
        + RamUsageEstimator.NUM_BYTES_LONG; // valuesBytes
  }

  /** Return the number of bytes used by this instance. */
  public long ramBytesUsed() {
    // TODO: this is called per-doc-per-norms/dv-field, can we optimize this?
    long bytesUsed = RamUsageEstimator.alignObjectSize(baseRamBytesUsed())
        + (pending != null ? RamUsageEstimator.sizeOf(pending) : 0L)
        + RamUsageEstimator.alignObjectSize(RamUsageEstimator.NUM_BYTES_ARRAY_HEADER + (long) RamUsageEstimator.NUM_BYTES_OBJECT_REF * values.length); // values

    return bytesUsed + valuesBytes;
  }

  /** Pack all pending values in this buffer. Subsequent calls to {@link #add(long)} will fail. */
  public void freeze() {
    if (pendingOff > 0) {
      if (values.length == valuesOff) {
        grow(valuesOff + 1); // don't oversize!
      }
      packPendingValues();
      valuesBytes += values[valuesOff].ramBytesUsed();
      ++valuesOff;
      pendingOff = 0;
    }
    pending = null;
  }

}
