/*
 * Copyright (c) 2009 IBM Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 */
package com.ibm.wala.util.intset;

import java.util.Arrays;

/**
 * an implementation of {@link IntVector} that uses a mix of backing arrays of type int, char, and
 * byte array, in an attempt to save space for common data structures.
 */
public class MultiModalIntVector implements IntVector {

  private static final float INITIAL_GROWTH_FACTOR = 1.5f;

  private static final float MINIMUM_GROWTH_FACTOR = 1.1f;

  private static final float DIFF_GROWTH_FACTOR = INITIAL_GROWTH_FACTOR - MINIMUM_GROWTH_FACTOR;

  private float currentGrowthRate = INITIAL_GROWTH_FACTOR;

  private static final int MAX_SIZE = 10000;

  private static final int INITIAL_SIZE = 1;

  int maxIndex = -1;

  private int[] intStore = new int[0];

  private short[] shortStore = new short[0];

  private byte[] byteStore = new byte[0];

  final int defaultValue;

  public MultiModalIntVector(int defaultValue) {
    this.defaultValue = defaultValue;
    init(getInitialSize(), defaultValue);
  }

  private void init(int initialSize, int defaultValue) {
    if (NumberUtility.isByte(defaultValue)) {
      byteStore = new byte[initialSize];
      byteStore[0] = (byte) defaultValue;
    } else if (NumberUtility.isShort(defaultValue)) {
      shortStore = new short[initialSize];
      shortStore[0] = (short) defaultValue;
    } else {
      intStore = new int[initialSize];
      intStore[0] = defaultValue;
    }
  }

  public MultiModalIntVector(int defaultValue, int initialSize) {
    if (initialSize <= 0) {
      throw new IllegalArgumentException("invalid initialSize: " + initialSize);
    }
    this.defaultValue = defaultValue;
    init(initialSize, defaultValue);
  }

  int getInitialSize() {
    return INITIAL_SIZE;
  }

  float getGrowthFactor() {
    return INITIAL_GROWTH_FACTOR;
  }

  /**
   * Will determine a dynamic growth factor that depends on the current size of the array
   *
   * @return the new growth factor
   */
  float getGrowthFactor(int size) {
    if (currentGrowthRate >= MINIMUM_GROWTH_FACTOR) {
      float val =
          (float) (1 / (1 + Math.pow(Math.E, (-1) * (((double) size / MAX_SIZE) * 12.0 - 6.0))));
      currentGrowthRate = INITIAL_GROWTH_FACTOR - DIFF_GROWTH_FACTOR * val;
    }
    return currentGrowthRate;
  }

  /*
   * @see com.ibm.wala.util.intset.IntVector#get(int)
   */
  @Override
  public int get(int x) {
    if (x < 0) {
      throw new IllegalArgumentException("illegal x: " + x);
    }
    int index = x;
    if (index < byteStore.length) {
      return byteStore[index];
    }
    index -= byteStore.length;
    if (index < shortStore.length) {
      return shortStore[index];
    }
    index -= shortStore.length;
    if (index < intStore.length) {
      return intStore[index];
    }
    return defaultValue;
  }

  private int getStoreLength() {
    return shortStore.length + byteStore.length + intStore.length;
  }

  /*
   * @see com.ibm.wala.util.intset.IntVector#set(int, int)
   */
  @Override
  public void set(int x, int value) {
    if (x < 0) {
      throw new IllegalArgumentException("illegal x: " + x);
    }
    if (x > MAX_SIZE) {
      throw new IllegalArgumentException("x is too big: " + x);
    }
    maxIndex =
        Math.max(maxIndex, x); // Find out if the new position is bigger than size of the array
    handleMorph(x, value);
    if (value == defaultValue) {
      int length = getStoreLength();
      if (x >= length) {
        return;
      } else {
        add(value, x);
      }
    } else {
      ensureCapacity(x, value);
      add(value, x);
    }
  }

  private void add(int value, int index) {
    if (byteStore.length > index) {
      byteStore[index] = (byte) value;
    } else {
      index -= byteStore.length;
      if (shortStore.length > index) {
        shortStore[index] = (short) value;
      } else {
        index -= shortStore.length;
        intStore[index] = value;
      }
    }
  }

  private void handleMorph(int index, int value) {
    if (NumberUtility.isShort(value)) {
      if (index < byteStore.length) {
        int newShortSize = byteStore.length - index + shortStore.length;
        short[] newShortStore = new short[newShortSize];
        byte[] newByteStore = new byte[index];
        for (int i = index; i < byteStore.length; i++) {
          newShortStore[i - index] = byteStore[i];
        }
        System.arraycopy(byteStore, 0, newByteStore, 0, index);
        System.arraycopy(shortStore, 0, newShortStore, byteStore.length - index, shortStore.length);
        byteStore = newByteStore;
        shortStore = newShortStore;
      }
    } else if (!NumberUtility.isByte(value)) {
      if (index < byteStore.length) {
        int newShortSize = byteStore.length - index + intStore.length;
        int[] newIntStore = new int[newShortSize];
        for (int i = index; i < byteStore.length; i++) {
          newIntStore[i - index] = byteStore[i];
        }
        byte[] newByteStore = new byte[index];
        System.arraycopy(byteStore, 0, newByteStore, 0, index);
        for (int i = 0; i < shortStore.length; i++) {
          newIntStore[byteStore.length - 1 + i] = shortStore[i];
        }
        System.arraycopy(
            intStore,
            0,
            newIntStore,
            byteStore.length + shortStore.length - index,
            intStore.length);
        intStore = newIntStore;
        byteStore = newByteStore;
        shortStore = new short[0];
      } else {
        int newindex = index - byteStore.length;
        if (newindex < shortStore.length) {
          int newIntSize = shortStore.length - newindex + intStore.length;
          int[] newIntStore = new int[newIntSize];
          for (int i = newindex; i < shortStore.length; i++) {
            newIntStore[i - newindex] = shortStore[i];
          }
          short[] newShortStore = new short[newindex];
          System.arraycopy(shortStore, 0, newShortStore, 0, newindex);
          System.arraycopy(intStore, 0, newIntStore, shortStore.length - newindex, intStore.length);
          intStore = newIntStore;
          shortStore = newShortStore;
        }
      }
    }
  }

  /** make sure we can store to a particular index */
  private void ensureCapacity(int capacity, int value) {
    int length = getStoreLength();
    // Value is an int
    if (intStore.length > 0 || (!NumberUtility.isShort(value) && !NumberUtility.isByte(value))) {
      // Need to increase the capacity of the array
      if (capacity >= length) {
        // Current array size
        int[] old = intStore;
        // New array size
        int newSize =
            1 + (int) (getGrowthFactor(length) * capacity) - byteStore.length - shortStore.length;
        int[] newData = Arrays.copyOf(old, newSize);
        Arrays.fill(newData, old.length, newSize, defaultValue);
        intStore = newData;
      }
    } else if (shortStore.length > 0 || NumberUtility.isShort(value)) {
      if (capacity >= length) {
        short[] old = shortStore;
        int newSize = 1 + (int) (getGrowthFactor(length) * capacity) - byteStore.length;
        short[] newData = Arrays.copyOf(old, newSize);
        Arrays.fill(newData, old.length, newSize, (short) defaultValue);
        shortStore = newData;
      }
    } else {
      if (capacity >= length) {
        byte[] old = byteStore;
        int newSize = 1 + (int) (getGrowthFactor(length) * capacity);
        byte[] newData = Arrays.copyOf(old, newSize);
        Arrays.fill(newData, old.length, newSize, (byte) defaultValue);
        byteStore = newData;
      }
    }
  }

  @Override
  public int getMaxIndex() {
    return maxIndex;
  }

  public void print() {
    final StringBuilder str = new StringBuilder();
    for (byte element : byteStore) {
      str.append(element).append(',');
    }
    for (short element : shortStore) {
      str.append(element).append(',');
    }
    for (int element : intStore) {
      str.append(element).append(',');
    }
    System.out.println(str);
  }
}
