/*
 * Copyright (c) 2002 - 2006 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.ssa;

import com.ibm.wala.util.collections.HashMapFactory;
import com.ibm.wala.util.debug.Assertions;
import java.util.Arrays;
import java.util.HashMap;

/**
 * A symbol table which associates information with each variable (value number) in an SSA IR.
 *
 * <p>By convention, symbol numbers start at 1 ... the "this" parameter will be symbol number 1 in a
 * virtual method.
 *
 * <p>This class is used heavily during SSA construction by {@link SSABuilder}.
 */
public class SymbolTable implements Cloneable {

  private static final int MAX_VALUE_NUMBER = Integer.MAX_VALUE / 4;

  /** value numbers for parameters to this method */
  private final int[] parameters;

  /** Mapping from Constant -&gt; value number */
  private HashMap<ConstantValue, Integer> constants = HashMapFactory.make(10);

  private boolean copy = false;

  /** @param numberOfParameters in the IR .. should be ir.getNumberOfParameters() */
  public SymbolTable(int numberOfParameters) {
    if (numberOfParameters < 0) {
      throw new IllegalArgumentException("Illegal numberOfParameters: " + numberOfParameters);
    }
    parameters = new int[numberOfParameters];
    for (int i = 0; i < parameters.length; i++) {
      parameters[i] = getNewValueNumber();
    }
  }

  /**
   * Values. Note: this class must maintain the following invariant: values.length &gt;
   * nextFreeValueNumber.
   */
  private Value[] values = new Value[5];

  private int nextFreeValueNumber = 1;

  /**
   * Method newSymbol.
   *
   * @return int
   */
  public int newSymbol() {
    return getNewValueNumber();
  }

  int findOrCreateConstant(Object o) {
    return findOrCreateConstant(o, false);
  }

  /**
   * Common part of getConstant functions.
   *
   * @param o instance of a Java 'boxed-primitive' class, String or NULL.
   * @return value number for constant.
   */
  int findOrCreateConstant(Object o, boolean isDefault) {
    ConstantValue v = new ConstantValue(o);
    Integer result = constants.get(v);
    if (result == null) {
      assert !(copy && !isDefault) : "making value for " + o;
      int r = getNewValueNumber();
      result = r;
      constants.put(v, result);
      assert r < nextFreeValueNumber;
      values[r] = v;
    } else {
      assert values[result] instanceof ConstantValue;
    }
    return result;
  }

  public void setConstantValue(int vn, ConstantValue val) {
    try {
      assert vn < nextFreeValueNumber;
      values[vn] = val;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid vn: " + vn, e);
    }
  }

  private Object[] defaultValues;

  /**
   * Set the default value for a value number. The notion of a default value is for use by languages
   * that do not require variables to be defined before they are used. In this situation, SSA
   * conversion can fail since it depends on the assumption that values are always defined when
   * used. The default value is the constant to be used in cases when a given value is used without
   * having been defined. Currently, this is used only by CAst front ends for languages with this
   * "feature".
   */
  public void setDefaultValue(int vn, final Object defaultValue) {
    assert vn < nextFreeValueNumber;

    if (defaultValues == null) {
      defaultValues = new Object[vn * 2 + 1];
    }

    if (defaultValues.length <= vn) {
      defaultValues = Arrays.copyOf(defaultValues, vn * 2 + 1);
    }

    defaultValues[vn] = defaultValue;
  }

  public int getDefaultValue(int vn) {
    return findOrCreateConstant(defaultValues[vn], true);
  }

  public int getNullConstant() {
    return findOrCreateConstant(null);
  }

  public int getConstant(boolean b) {
    return findOrCreateConstant(b);
  }

  public int getConstant(int i) {
    return findOrCreateConstant(i);
  }

  public int getConstant(long l) {
    return findOrCreateConstant(l);
  }

  public int getConstant(float f) {
    return findOrCreateConstant(f);
  }

  public int getConstant(double d) {
    return findOrCreateConstant(d);
  }

  public int getOtherConstant(Object v) {
    return findOrCreateConstant(v);
  }

  public int getConstant(String s) {
    return findOrCreateConstant(s);
  }

  /**
   * Return the value number of the ith parameter
   *
   * <p>By convention, for a non-static method, the 0th parameter is 'this'
   */
  public int getParameter(int i) throws IllegalArgumentException {
    try {
      return parameters[i];
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid i: " + i, e);
    }
  }

  private void expandForNewValueNumber(int vn) {
    if (vn >= values.length) {
      values = Arrays.copyOf(values, 2 * vn);
    }
  }

  private int getNewValueNumber() {
    int result = nextFreeValueNumber++;
    expandForNewValueNumber(result);
    return result;
  }

  /**
   * ensure that the symbol table has allocated space for the particular value number
   *
   * @param i a value number
   */
  public void ensureSymbol(int i) {
    if (i < 0 || i > MAX_VALUE_NUMBER) {
      throw new IllegalArgumentException("Illegal i: " + i);
    }
    try {
      if (i != -1) {
        if (i >= values.length || values[i] == null) {
          if (nextFreeValueNumber <= i) {
            nextFreeValueNumber = i + 1;
          }
          expandForNewValueNumber(i);
        }
      }
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid i: " + i, e);
    }
  }

  public String getValueString(int valueNumber) {
    if (valueNumber < 0
        || valueNumber > getMaxValueNumber()
        || values[valueNumber] == null
        || values[valueNumber] instanceof PhiValue) {
      return "v" + valueNumber;
    } else {
      return "v" + valueNumber + ':' + values[valueNumber].toString();
    }
  }

  public boolean isConstant(int v) {
    try {
      return v < values.length && values[v] instanceof ConstantValue;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isZero(int v) {
    try {
      return (values[v] instanceof ConstantValue) && ((ConstantValue) values[v]).isZeroConstant();
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isOne(int v) {
    try {
      return (values[v] instanceof ConstantValue) && ((ConstantValue) values[v]).isOneConstant();
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isTrue(int v) {
    try {
      return (values[v] instanceof ConstantValue) && ((ConstantValue) values[v]).isTrueConstant();
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isZeroOrFalse(int v) {
    return isZero(v) || isFalse(v);
  }

  public boolean isOneOrTrue(int v) {
    return isOne(v) || isTrue(v);
  }

  public boolean isFalse(int v) {
    try {
      return (values[v] instanceof ConstantValue) && ((ConstantValue) values[v]).isFalseConstant();
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isBooleanOrZeroOneConstant(int v) {
    return isBooleanConstant(v) || isZero(v) || isOne(v);
  }

  public boolean isBooleanConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && ((ConstantValue) values[v]).getValue() instanceof Boolean;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isIntegerConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && (((ConstantValue) values[v]).getValue() instanceof Integer);
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isLongConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && (((ConstantValue) values[v]).getValue() instanceof Long);
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isFloatConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && ((ConstantValue) values[v]).getValue() instanceof Float;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isDoubleConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && ((ConstantValue) values[v]).getValue() instanceof Double;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isNumberConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && ((ConstantValue) values[v]).getValue() instanceof Number;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isStringConstant(int v) {
    try {
      return (values[v] instanceof ConstantValue)
          && ((ConstantValue) values[v]).getValue() instanceof String;
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  public boolean isNullConstant(int v) {
    try {
      return (values.length > v)
          && (values[v] instanceof ConstantValue)
          && (((ConstantValue) values[v]).getValue() == null);
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new IllegalArgumentException("invalid v: " + v, e);
    }
  }

  /** @throws IllegalArgumentException if rhs is null */
  public int newPhi(int[] rhs) throws IllegalArgumentException {
    if (rhs == null) {
      throw new IllegalArgumentException("rhs is null");
    }
    int result = getNewValueNumber();
    SSAPhiInstruction phi = new SSAPhiInstruction(SSAInstruction.NO_INDEX, result, rhs.clone());
    assert result < nextFreeValueNumber;
    values[result] = new PhiValue(phi);
    return result;
  }

  /** Return the PhiValue that is associated with a given value number */
  public PhiValue getPhiValue(int valueNumber) {
    try {
      return (PhiValue) values[valueNumber];
    } catch (ArrayIndexOutOfBoundsException | ClassCastException e) {
      throw new IllegalArgumentException("invalid valueNumber: " + valueNumber, e);
    }
  }

  public int getMaxValueNumber() {
    // return values.length - 1;
    return nextFreeValueNumber - 1;
  }

  public int[] getParameterValueNumbers() {
    return parameters;
  }

  public int getNumberOfParameters() {
    return parameters.length;
  }

  public String getStringValue(int v) throws IllegalArgumentException {
    if (!isStringConstant(v)) {
      throw new IllegalArgumentException("not a string constant: value number " + v);
    }

    return (String) ((ConstantValue) values[v]).getValue();
  }

  public float getFloatValue(int v) throws IllegalArgumentException {
    if (!isNumberConstant(v)) {
      throw new IllegalArgumentException("value number " + v + " is not a numeric constant.");
    }
    return ((Number) ((ConstantValue) values[v]).getValue()).floatValue();
  }

  public double getDoubleValue(int v) throws IllegalArgumentException {
    if (!isNumberConstant(v)) {
      throw new IllegalArgumentException("value number " + v + " is not a numeric constant.");
    }
    return ((Number) ((ConstantValue) values[v]).getValue()).doubleValue();
  }

  public int getIntValue(int v) throws IllegalArgumentException {
    if (!isNumberConstant(v)) {
      throw new IllegalArgumentException("value number " + v + " is not a numeric constant.");
    }
    return ((Number) ((ConstantValue) values[v]).getValue()).intValue();
  }

  public long getLongValue(int v) throws IllegalArgumentException {
    if (!isNumberConstant(v)) {
      throw new IllegalArgumentException("value number " + v + " is not a numeric constant.");
    }
    return ((Number) ((ConstantValue) values[v]).getValue()).longValue();
  }

  public Object getConstantValue(int v) throws IllegalArgumentException {
    if (!isConstant(v)) {
      throw new IllegalArgumentException("value number " + v + " is not a constant.");
    }

    Object value = ((ConstantValue) values[v]).getValue();
    if (value == null) {
      return null;
    } else {
      return value;
    }
  }

  /**
   * @return the Value object for given value number or null if we have no special information about
   *     the value
   */
  public Value getValue(int valueNumber) {
    if (valueNumber < 1 || valueNumber >= values.length) {
      throw new IllegalArgumentException("Invalid value number " + valueNumber);
    }
    return values[valueNumber];
  }

  /** @return true iff this valueNumber is a parameter */
  public boolean isParameter(int valueNumber) {
    return valueNumber <= getNumberOfParameters();
  }

  public SymbolTable copy() {
    try {
      SymbolTable nt = (SymbolTable) clone();
      nt.values = this.values.clone();
      if (this.defaultValues != null) {
        nt.defaultValues = this.defaultValues.clone();
      }
      nt.constants = HashMapFactory.make(this.constants);
      nt.copy = true;
      return nt;
    } catch (CloneNotSupportedException e) {
      Assertions.UNREACHABLE();
      return null;
    }
  }
}
