/**
 * 
 */
package dspl.intermediate.variable;

import dspl.frontend.ParserTokensTokenTypes;

/**
 * Variable is the base abstract class used as the parent of all
 * other abstract Variable classes
 * 
 * @author David
 */
public abstract class Variable implements ParserTokensTokenTypes {
	/**
	 * Enumeration types of possible values. Note this value only indicates the type of data
	 * but not the # of elements. Both an Array of Integers and a single integer would return 
	 * DATA_TYPE_INT . 
	 */
	public final static int DATA_TYPE_FLOAT = 0;
	public final static int DATA_TYPE_INT   = 1; 
	public final static int DATA_TYPE_UINT  = 2; 
	public final static int DATA_TYPE_BYTE  = 3;
	public final static int DATA_TYPE_UBYTE = 4;
	public final static int DATA_TYPE_COMPLEX = 5; 
	public final static int DATA_TYPE_STRING = 6; 
	
	/*
	 * Enumeration representing the size of the Variable.
	 * All elements in an array must have the same data type correspnding to the Variable's dataType. 
	 */
	/**
	 * Describes any single elements (integer, float, etc)
	 */
	public final static int SIZE_TYPE_ATOM   = 0; 
	
	/**
	 * Describes any arra of single elements (array of ints, array of floats, etc)
	 */
	public final static int SIZE_TYPE_ARRAY  = 1; 
	
	/**
	 * Stores the value of the object (Integer, Float, Array of Integers, Array of Floats, etc)
	 */
	public Object value = null; 
	
	/**
	 * Stores the data type, using one of the possible DATA_TYPE_xxx values above 
	 */
	protected int dataType = -1; 
	
	/**
	 * Stores the size type, using one of the possible SIZE_TYPE_xxx above
	 */
	protected int sizeType = -1; 
	
	/**
	 * Stores the size (in bytes) of the Variable
	 * For primitives:
	 * float,int,uint = 4 bytes
	 * byte,ubyte  = 1 byte
	 * complex = 8 bytes (2 floats)
	 * for arrays = # of elements * size of array's primitive type
	 */
	protected int byteSize = -1; 
	
	/**
	 * The number of elements in an array<p>
	 * For Variables declared which have sizeType = SIZE_TYPE_ARRAY this value must be >=1<br>
	 * For SIZE_TYPE_ATOM it should remain -1
	 */
	protected int arraySize = -1; 
	
	/**
	 * Refers to whether the Variable is a constant, meaning the actual value is known at compile time.<p>
	 * Example: "int x=5;" in this case the Variable created for 5 would have isConstant = true, since its initial value 
	 * is known at compile time. This is useful for generating assmebly code when one needs to decide whether to pass
	 * an actual variable or a immediate constant within the asm instruction. 
	 */
	protected boolean isConstant = false; 
	
	/**
	 * Refers to whether a Variable represents an element within either an array a Variable
	 * Example: Within "int_array x[5]; x[0] = 1;", "x[0]" should create an array element variable to pass into the IR
	 */
	protected boolean isElement = false; 
	
	/**
	 * If the variable is an element of a larger variable (either array or complex), element index stores either:<br>
	 * the index into the array, in the case where elementsParentVar is an array<br>
	 * the flag indicating either the real or imaginary component of a complex, in the case where elementsParentVar is a DSPLComplex 
	 */
	protected Variable elementIndex = null; 
	
	/**
	 * For Atomic Elements of Arrays/Complex, elementsParentVar contains the parent variable (either the array or complex variable)<P>
	 * Should only be set in the case where isElement is true. 
	 */
	protected Variable elementsParentVar = null; 
	
	/**
	 * Contains the identifier used within the actual source program<br>
	 * For example, "complex_array a;" would create a Variable of data type complex_array with the identifier set to "a";<br><br>
	 * In the case of constants, the identifier is equal to the string representation of the constant. <br>
	 * For example, "int b; b = 5;", would create two Variables, one for b and one for 5. The identifier for 5 would be "5"; 
	 */
	protected String identifier = null; 
	
	/**
	 * Contains the line within the source program where the Variable was declared
	 */
	protected int lineNumDecl = -1; 
	
	/**
	 * Returns whether the Variable is a constant variable or not
	 * @return true if the Variable is a constant expression, false otherwise
	 */
	public boolean isConstant() { 
		return isConstant; 
	}
	
	
	/**
	 * Returns the Variable's value 
	 * @return
	 */
	public Object getValue() { 
		if (value==null) { 
			throw new Error("getValue called on variable which does not have value set");
		}
		return value; 
	}
	
	/**
	 * Retruns whether the Variable's value has been set<br>
	 * The function has been included so that variables can have
	 * their initial values set implicitly when they are declared
	 * within the .data section of the ASM file instead of requiring
	 * a "mov" instruction to set it in the begining<br>
	 * This feature is specificaly required for floating point values
	 * because floating point operations do not accept immediate operands. 
	 * @return
	 */
	public boolean isValueSet() { 
		return (value!=null); 
	}
	
	/**
	 * Used to set the value of a Variable. <bR>
	 * Should only be used for creating float constants
	 * which cannot be true constants. 
	 * @param valueStr
	 */
	public void setValue(String valueStr) { 
		if (isArray()) { 
			throw new Error("not supported"); 
		}
		if (isConstant()) {
			throw new Error("invalid setting value of constant");
		}
		if (isValueSet()) { 
			throw new Error("Set value called on Variable whose value already set"); 
		}
		this.value = convertToObject(valueStr); 
	}
	
	/**
	 * 
	 * @return the size type (SIZE_TYPE_ARRAY or SIZE_TYPE_ATOMIC)
	 */
	public int getSizeType() {
		if (sizeType==-1) { 
			throw new Error("Error: size type has not been defined"); 
		}
		return sizeType; 
	}
	
	/**
	 * Returns the data type of the Variable (either atomic or array)
	 * @return the data type (DATA_SIZE_xxx)
	 */
	//have all classes utilize getVariableDataType instead
	/*
	public int getDataType() { 
		if (dataType==-1) { 
			throw new Error("Error: data type has not been defined"); 
		}
		return dataType; 
	}
	*/
	
	public int getArraySize() { 
		if (arraySize==-1) 	{
			throw new Error("Error: array size not defined"); 
		}
		return arraySize; 
	}
	
	/**
	 * Called to determine if the variable is an array or not
	 * @return true if sizeType == SIZE_TYPE_ARRAY, false otherwise
	 */
	public boolean isArray() { 
		return (getSizeType()==Variable.SIZE_TYPE_ARRAY);
	}
	
	/**
	 * 
	 * @return true if Variable is element of another Variable, false otherwise
	 */
	public boolean isElement() { 
		return this.isElement; 
	}
	
	/**
	 * Called to determine if the variable is an atomic or not<P>
	 * Atomic types include int, uint, float, byte, ubyte, complex, and string. </p>
	 * Non-atomic (array-types) include int_array, uint_array, ubyte_array, byte_array, and complex_array. <br>
	 * @return true if sizeType == SIZE_TYPE_ATOM, false otherwise
	 */
	public boolean isAtomic() {
		return (getSizeType()==Variable.SIZE_TYPE_ATOM); 
	}
	
	/**
	 * Gets the identifier for the Variable<br>
	 * Throws an error if the variable has not yet been set.<br>
	 * @return the identifier of the variable
	 */
	public String getIdentifier() { 
		if (identifier==null) {
			throw new Error("Error: get identifier called when identifier is null!");
		}
		return identifier; 
	}
	
	/**
	 * 
	 * Note that this function returns true so long as their type is the same, it does not
	 * take into account whether the sizes are the same. Therefore an array of integers 
	 * compared against a single integer will have the same data type. <br>
	 * @param otherVar: the Variable to compare this to 
	 * @return true if otherVar and this Variable have the same Data Type, false if not
	 */
	public boolean isSameDataType(Variable otherVar) {
		return (this.getDataType()==otherVar.getDataType()); 
	}
	
	/**
	 * Numeric types are defiend in the context of the allowable operations performable on them.
	 * It is assumed that numeric types can undergo single and dual operand arithmatic operations, but non-numeric types cannot.
	 * For example, it is possible to add an integer constant to an array of integers, since both are numeric, but it is not possible
	 * to add a string to an integer, or add any numeric type to a string. 
	 * @return true if the Variable is a numeric type (int, float, array of numeric types, etc) and false if it is not (string)
	 */
	public abstract boolean isNumeric(); 
	
	/**
	 * Natural numbers are ints, uints, chars, and uchars. <br>
	 * This function is useful when there are different arithmatic machine
	 * instructions depending on whether the number is an integer or floating point type. 
	 * @return
	 */
	public abstract boolean isNaturalNum();
	
	/**
	 * 
	 * @return element variable's parent
	 */
	public Variable getParentVariable() { 
		if (!isElement()) { 
			throw new Error("called on non-atomic element"); 
		}
		else if (this.elementsParentVar==null) { 
			throw new Error("elementsParentVar is null"); 
		}
		else { 
			return elementsParentVar; 
		}
	}
	
	/**
	 * 
	 * @return returns an element variable's parent
	 */
	public Variable getIndexVariable() { 
		if (!isElement()) { 
			throw new Error("called on non-atomic element"); 
		}
		else if (this.elementIndex==null) { 
			throw new Error("elementIndex is null"); 
		}
		else { 
			return elementIndex; 
		}
	}
	
	/**
	 * Returns the size of the Variable type's byte size (for a single element)
	 * @return the size of bytes of the variable 
	 */
	public abstract int getDataTypeByteSize(); 
	
	/**
	 * Returns the size of the Variable (size = (size of type) * (num of elements)
	 * @return
	 */
	public int getByteSize() { 
		return byteSize; 
	}
	
	/**
	 * Sets the identifier of the Variable. <B>
	 * This method was added solely for the SymbolTable to call when renaming Strings from
	 * their actual text to their label used in the data section of the ASM File. 
	 * @param newID String to set identifier to 
	 */
	public void setIdentifier(String newID) { 
		this.identifier = newID;
	}
	
	/**
	 * 
	 * @return the data tyep of the variable, must be one of the Variable.DATA_TYPE_xxx values
	 */
	public abstract int getDataType();
	
	/**
	 * Method responsible for taking a string from the source code and converting it into the 
	 * appriopriate Java Object for the primitive.<br>
	 * For all DSPL primitives except complex, the native java objects are used (String for string, Integer for int/uint, Float for float, etc). <br>
	 * Note: Invoking the single string convertToObject on a complex throws an Error since it is expecting two strings.
	 * @param strValue the string expresion for the variable (i.e. "hello", "5", "3.4"); 
	 * @return
	 */
	public abstract Object convertToObject(String strValue); 
	
	/**
	 * Method responsible for taking two strings from the source code and converting it into the 
	 * appriopriate Java Object for the primitive.<br>
	 * At time of writing, this is only required for Complex, which takes two strings (real and imag).<br>
	 * For complex, the dspl.intermediate.variable.primitive.Complex object is used <br>
	 * Note invoking the double string convertToObject on any type besides complex throws an error. 
	 * @param strValue1 the string expresion for the first variable
	 * @param strValue2 the string expression for the second variable
	 * @return
	 */
	public abstract Object convertToObject(String strValue1, String strValue2); 
	
	
	/**
	 * 
	 * @return the size type of the variable, must be one of the Variable.SIZE_TYPE_xxx values
	 */
	public int getVariableSizeType() { 
		if (sizeType==-1) { 
			throw new Error("getVariablesSizeType called on Variable whose size type has not been set"); 
		}
		return sizeType; 
	}
	
	/**
	 * 
	 * @return the data type of the variable (one of DATA_TYPE_xxx)
	 */
	/*
	public int getDataType() { 
		if (dataType==-1) { 
			throw new Error("getDataType called on Variable whose data type has not been set"); 
		}
		return this.dataType; 
	}*/
	
	/**
	 * The purpose of this function is to have a single function that all derived classes can
	 * call to perform the initialization steps of setting the byte size, data type, and size type
	 * While this could be done in the constructor, by putting it here and then relying on the 
	 * necessary get calls that all descended classes must implement, this makes it easier 
	 * to write correct code which fills in the proper values as one need only remember to call
	 * initVariables in the constructor, rather than remember to write out all of the fields below. 
	 *
	 */
	public void initVariable(int numElements) { 
		if (numElements<1) { 
			throw new Error("zero or negative number of elements specified"); 
		}
		else if (numElements==1) { 
			this.sizeType = SIZE_TYPE_ATOM; 
		}
		else { 
			this.sizeType = SIZE_TYPE_ARRAY; 
		}
		
		this.dataType = this.getDataType(); 
		//this.dataType = this.getDataType() * numElements; 
	}
	
	/**
	 * Constructor immediately invoked by all derived classes of Variable in their atomic or array constructor 
	 * @param numElements number of elements to be created, numElements=1 implies ATOMIC, numElements>1 implies ARRAY
	 */
	public Variable(int numElements) { 
		this.initVariable(numElements); 
	}; 
	
	/**
	 * Called to create a new array Variable of the appriopriate size based on the provided parserTokenID
	 * @param parserTokenID parser token id corresponding to wha tis listed in DSPLParserTokenTypes automatically generated by antler
	 * @param identifier the textual representation used to declare this variable within the source code of the program
	 * @param arraySize the size of the array
	 * @param line in the source file on which the array is declared
	 */
	public static Variable getNewArray(int parserTokenID, String identifier, int arraySize, int lineDecl) { 
		Variable v; 
		switch (parserTokenID) {
		case LITERAL_complex_array:
			v = new DSPLComplex(arraySize); 
			break;
		case LITERAL_int_array:
			v = new DSPLInteger(arraySize); 
			break;
		case LITERAL_uint_array:
			v = new DSPLUInteger(arraySize); 
			break;
		case LITERAL_byte_array:
			v = new DSPLByte(arraySize); 
			break;
		case LITERAL_ubyte_array:
			v = new DSPLUByte(arraySize); 
			break;
		case LITERAL_float_array:
			v = new DSPLFloat(arraySize); 
			break;
		default:
			throw new Error("Invalid token type passed to getNewArray: " + parserTokenID); 
		}
		v.arraySize = arraySize; 
		v.identifier = identifier; 
		v.lineNumDecl = lineDecl;
		v.byteSize = v.getDataTypeByteSize() * arraySize;
		return v; 
	}
	
	/**
	 * Creates a new Atomic Variable.<p>
	 * if isConstant is true, then the variable is a constant literal (i.e. "5")<br>
	 * else if isConstant is false, then a "normal" Variable is created (i.e. "int x;")
	 * which will need a location in memory to be stored. 
	 * @param parserTokenID
	 * @param identifier
	 * @param lineDecl
	 * @param isConstant
	 */
	// TODO: (Dave) need seperate getNewAtomic which accepts two strings, for complex numbers, or convert to an array-based implementation
	public static Variable getNewAtomic(int parserTokenID, String identifier, int lineDecl, boolean isConstant) {
		if (identifier==null) {
			throw new Error("Error: getNewAtomic passed null identifier"); 
		}
		
		Variable v; 
		switch (parserTokenID) {
		case LITERAL_string:
			v = new DSPLString(1);
			break;
		case LITERAL_float:
			v = new DSPLFloat(1);
			break;
		case LITERAL_complex:
			v = new DSPLComplex(1);
			break;
		case LITERAL_int:
			v = new DSPLInteger(1); 
			break;
		case LITERAL_uint:
			v = new DSPLUInteger(1);
			break;
		case LITERAL_byte:
			v = new DSPLByte(1);
			break;
		case LITERAL_ubyte:
			v = new DSPLUByte(1);
			break;
		default:
			throw new Error("Invalid token type passed to getNewAtomic: " + parserTokenID); 
		}
		v.dataType = v.getDataType(); 
		v.identifier = identifier; 
		v.lineNumDecl = lineDecl;
		
		//if constant identifer, then set the value of the variable to the identifier (i.e. "5")
		if (isConstant) { 
			v.value = v.convertToObject(identifier); 
			v.isConstant = true; 
		}
		
		//else it is not a constant
		else { 
			v.isConstant = false; 
		}
		
		v.byteSize = v.getDataTypeByteSize() * 1;
		
		return v; 
	}
	
	/**
	 * Returns the Antlr constant for a variable type based on the DSPL variable type.<p>
	 * @param varType
	 * @author jcropsey
	 */
	
	public static int getAntlrTypeFromVarType(int varType)
	{
		switch (varType) {
		case DATA_TYPE_STRING:
			return LITERAL_string;
		case DATA_TYPE_FLOAT:
			return LITERAL_float;
		case DATA_TYPE_COMPLEX:
			return LITERAL_complex;
		case DATA_TYPE_INT:
			return LITERAL_int;
		case DATA_TYPE_UINT:
			return LITERAL_uint;
		case DATA_TYPE_BYTE:
			return LITERAL_byte;
		case DATA_TYPE_UBYTE:
			return LITERAL_ubyte;
		default:
			throw new Error("Invalid DSPL Variable Type"); 
		}
	}
	
	public static int getAntlrTypeFromVarTypeArray(int varType)
	{
		switch (varType) {
		case DATA_TYPE_FLOAT:
			return LITERAL_float_array;
		case DATA_TYPE_COMPLEX:
			return LITERAL_complex_array;
		case DATA_TYPE_INT:
			return LITERAL_int_array;
		case DATA_TYPE_UINT:
			return LITERAL_uint_array;
		case DATA_TYPE_BYTE:
			return LITERAL_byte_array;
		case DATA_TYPE_UBYTE:
			return LITERAL_ubyte_array;
		default:
			throw new Error("Invalid DSPL Variable Type"); 
		}
	}
	
	/**
	 * Returns a new Atomic variable representing an element within an array or complex<P>
	 * @param parentVar parent variable array or complex
	 * @param elementIndex 0-based index into the array or flag indicating real or imaginary component of Complex
	 * @param lineDecl line number where where array element is being used
	 * @return Variable referring to element within an array
	 * @throws Exception if elementIndex is outside of array bounds (0 --> (arraysize-1))
	 */
	public static Variable getNewAtomicElement(Variable parentVar, Variable elementIndex, int lineDecl) throws Exception {
		//XXX: getNewAtomicElement may change to handle .real/.imag, and may require different check besides isArray (isComplex?)
		if (!parentVar.isArray()) {
			throw new Error("Error: getNewArrayElement called on non-array element"); 
		}
		if (parentVar.getArraySize()==-1) {
			throw new Error("Error: getNewArrayElement passed parentVar with unset arraysize"); 
		}
		//if the element index is a constant, then get the integer value and make sure the index is within the bounds of the array
		if (parentVar.isArray() && elementIndex.isConstant()) { 
			int indexVal = ((Long)elementIndex.getValue()).intValue(); //@author vkm had to change type from Int to Long because of storage type changes
			
			if (indexVal<0 || indexVal>=parentVar.getArraySize()) { 
				throw new Exception("Syntax Error: index (" + elementIndex + ") is outside of array size (0->" + (parentVar.getArraySize()-1) +")"); 
			}
		}
		Variable elementVar; 
		
		switch (parentVar.getDataType()) { 
		case DATA_TYPE_FLOAT:
			elementVar = new DSPLFloat(1); 
			break;
		case DATA_TYPE_INT:
			elementVar = new DSPLInteger(1); 
			break;
		case DATA_TYPE_UINT:
			elementVar = new DSPLUInteger(1); 
			break;
		case DATA_TYPE_BYTE:
			elementVar = new DSPLByte(1); 
			break;
		case DATA_TYPE_UBYTE:
			elementVar = new DSPLUByte(1); 
			break;
		case DATA_TYPE_COMPLEX:
			elementVar = new DSPLFloat(1);  //note how in this one case, it creates a variable that is not the same type as the parent
			break;
		case DATA_TYPE_STRING:
			throw new Exception("Syntax Error: Cannot access elements of a String");
		default:
			throw new Error("Error: invalid data type of parent variable passed to getNewArrayElement"); 
		}
		
		//initialize element-type specific properties 
		elementVar.isElement = true; 
		elementVar.elementsParentVar = parentVar; 
		elementVar.elementIndex = elementIndex; 
		
		//initalize generic Variable properties
		if (parentVar.isArray()) {
			elementVar.dataType = parentVar.getDataType();  //only true for arrays
		}
		else if (parentVar.getDataType()==Variable.DATA_TYPE_COMPLEX) { 
			elementVar.dataType = Variable.DATA_TYPE_FLOAT;
		}
		elementVar.lineNumDecl = lineDecl;
		elementVar.isConstant = false; 
		elementVar.byteSize = elementVar.getDataTypeByteSize();
		
		return elementVar; 
	}
}
