/*
 * 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.uima.cas.impl;

/**
 * Encapsulate 8, 16, and 64 bit storage for the CAS.
 */
abstract class CommonAuxHeap {
  
  private static final boolean debugLogShrink = false;
//  static {
//    debugLogShrink = System.getProperty("uima.debug.ihs") != null;
//  }
  
  // cannot be 0 because it grows by multiplying growth_factor
  protected static final int DEFAULT_HEAP_BASE_SIZE = 16;

  // Jira: https://issues.apache.org/jira/browse/UIMA-2385, 
  // https://issues.apache.org/jira/browse/UIMA-4279
  protected static final int DEFAULT_HEAP_MULT_LIMIT = 1024 * 1024 * 16; 

  protected static final int MIN_HEAP_BASE_SIZE = 16;

  protected static final int GROWTH_FACTOR = 2;

  protected static final int NULL = 0;

  // start pos
  protected static final int FIRST_CELL_REF = 1;

  protected final int heapBaseSize;

  protected final int heapMultLimit;

  protected int heapPos = FIRST_CELL_REF;
  
  private final int[] shrinkableCount = new int[1];

  CommonAuxHeap() {
    this(DEFAULT_HEAP_BASE_SIZE, DEFAULT_HEAP_MULT_LIMIT);
  }

  CommonAuxHeap(int heapBaseSize, int heapMultLimit) {
    super();
    this.heapBaseSize = Math.max(heapBaseSize, MIN_HEAP_BASE_SIZE);
    this.heapMultLimit = Math.max(heapMultLimit, DEFAULT_HEAP_MULT_LIMIT);
    initMemory();
  }

  abstract void initMemory();
  
  abstract void initMemory(int size);

  abstract void resetToZeros();

  abstract void growHeapIfNeeded();

  void reset() {
    this.reset(false);
  }

  /**
   * Logic for shrinking:
   * 
   *   Based on a short history of the capacity needed to hold the larger of the previous 2 sizes
   *     (Note: can be overridden by calling reset() multiple times in a row)
   *   Never shrink below initialSize
   *   
   *   Shrink in exact reverse sequence of growth - using the subtraction method 
   *   and then (for small enough sizes) the dividing method
   *   
   *   Shrink by one jump if that is large enough to hold the larger of the prev 2 sizes

   * @param doFullReset true means reallocate from scratch
   */
  void reset(boolean doFullReset) {
    if (doFullReset) {
      if (debugLogShrink) System.out.format("Debug shrink CommonAux full reset from %,d to %,d for %s%n",
          getCapacity(), heapBaseSize, this.getClass().getSimpleName());
      this.initMemory();
    } else {
      final int curCapacity = getCapacity();
      final int curSize = getSize();
      int newSize = computeShrunkArraySize(
          curCapacity, curSize, GROWTH_FACTOR, heapMultLimit, heapBaseSize, shrinkableCount);
      if (newSize == getCapacity()) { // means didn't shrink
        resetToZeros();
      } else {
        if (debugLogShrink) System.out.format("Debug shrink CommonAux from %,d to %,d for %s%n",
            curCapacity, newSize, this.getClass().getSimpleName());
        initMemory(newSize);
      }
    }
    this.heapPos = FIRST_CELL_REF;
  }

  int reserve(int numCells) {
    int cellRef = this.heapPos;
    this.heapPos += numCells;
    growHeapIfNeeded();
    return cellRef;
  }

  int computeNewArraySize(int size, int needed_size, int growth_factor, int multiplication_limit) {
    do {
      if (size < multiplication_limit) {
        size *= growth_factor;
      } else {
        size += multiplication_limit;
      }
    } while (size < needed_size);
    return size;
  }
  
  /**
   * This routine is used to compute the capacity an internal expandable array 
   * should be reallocated to, upon reset.
   * 
   * It returns at most a 1 increment shrink, based on the doubling up to multiplication
   * limit, then addition thereafter.
   * 
   * It maintains a shrinkableCount - the number of consecutive times this could be shrunk.
   * This is reset to 0 if current size requires current capacity,
   *   that is, no shrinkage is possible for current size.
   * Otherwise, the count is incremented.
   * 
   * If the shrinkableCount is incremented to exceed 20, 
   * the capacity is allowed to drop by 1 allocation unit.
   * 
   * This guarantees the shrinkages are delayed until 20 shrinkable sizes 
   * are found (with no intervening non-shrinkable ones).
   * When a shrinkage happens, the count is reset to 16; this delays subsequent
   * shrinkages to happen only every (20 - 16) 4 resets. 
   * 
   * @param capacity the current capacity
   * @param size_used the maximum number of used entries, <= current capacity
   * @param growth_factor is 2
   * @param multiplication_limit the point where we start adding this limit, vs using the growth factor
   * @param shrinkableCount a pass-by-reference int reflecting the number of times it was shrinkable
   * @return the capacity shrink down by one step, if that will still hold the size_used number of entries, 
   *   minimum limited to min_size.
   */
  static int computeShrunkArraySize(
      int capacity, 
      int size_used, 
      int growth_factor, 
      int multiplication_limit,
      int min_size, 
      int[] shrinkableCount) {  // pass by reference

    if (capacity < size_used) {
      throw new IllegalArgumentException("The Capacity " + capacity + " must be >= sized_used " + size_used);
    }
    
    // this if for shrinking down 1 step if possible
    int oneSizeLowerCapacity = ((capacity - multiplication_limit) < multiplication_limit) ?
        // the last expansion was by multiplying; the next expansion would be by adding
        (capacity / growth_factor) :
        (capacity - multiplication_limit);

    if (oneSizeLowerCapacity < min_size) {
      return capacity;
    }
    
    boolean isShrink = (size_used < oneSizeLowerCapacity);
    if (isShrink) {
      shrinkableCount[0] ++;
      if (shrinkableCount[0] > 20) {
        shrinkableCount[0] = 16;
        return oneSizeLowerCapacity;
      }
      return capacity;
    }
    shrinkableCount[0] = 0;
    return capacity;
  }
    
  int getSize() {
    return this.heapPos;
  }
  
  abstract int getCapacity();

}
