/*
 *  Draco Econometrics
 *  Copyright (C) 2006-2007 Approximatrix, LLC
 *  http://www.approximatrix.com
 *  
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.approximatrix.draco.datamodel;

import java.util.ArrayList;
import java.lang.ArrayIndexOutOfBoundsException;
import java.util.Arrays;

import org.netlib.blas.DAXPY;
import org.netlib.blas.DNRM2;

import com.approximatrix.draco.math.CustomSort;
import com.approximatrix.draco.math.VectorSupport;

public class VariableModel implements Cloneable {

	private ArrayList data = null;
	private Class classid = null;
	
	private String name = null;
	
	private String equation = null;
	
	private String description = null;
	
	/** Creates a new variable filled with Doubles
	 * 
	 * @param size default initial size (for speed reasons)
	 * @param fillvalue value to fill variable with
	 */
	public VariableModel(String var_name, int size, double fillvalue) {
		data = new ArrayList(size);
		for(int i=0;i<size;i++) {
			data.add(new Double(fillvalue));
		}
		
		classid = Double.class;
		name = var_name;
	}
	
	/** Creates a new variable from a vector of doubles
	 * 
	 * @param values values to fill in array with
	 */
	public VariableModel(String var_name, double[] values) {
		data = new ArrayList(values.length);
		
		for(int i=0;i<values.length; i++) {
			data.add(new Double(values[i]));
		}
		classid = Double.class;
		name = var_name;
	}
	
	/** Creates a new variable from a vector of Objects
	 * 
	 * @param values Values with which to create the variable
	 */
	public VariableModel(String var_name, Object[] values) {
		data = new ArrayList(values.length);
		
		for(int i=0;i<values.length; i++) {
			data.add(values[i]);
		}
		
		resetContentsClass();
		name = var_name;
	}
	
	/** Determines if an entry is editable.  DateObjects are not
	 * editable under any circumstance.
	 * 
	 * @param index the entry's index
	 * @return true if editable, false otherwise
	 */
	public boolean isEntryEditable(int index) {
		if(index < data.size() && index >= 0) {
			if(data.get(index) == null) {
				return true;
			} else if(data.get(index).getClass() == DateObject.class) {
				return false;
			} else {
				return true;
			}
		}
		return false;
	}
	
	/** Returns a single value from the array
	 * 
	 * @param index the value to access
	 * @return the Object or null if the index is out of range
	 */
	public Object getValueAt(int index) {
		if(index < data.size() && index >= 0) {
			if(classid == DateObject.class &&
					data.get(index) != null) {
				//return ((DateObject)data.get(index)).stringValue();
			}
			return data.get(index);
		}
		return null;
	}
	
	/** Returns a single value from the array
	 * 
	 * @param index the value to access
	 * @return the Number or null if the index is out of range
	 */
	public Number getNumberAt(int index) {
		if(index < data.size() && index >= 0) {
			return (Number)data.get(index);
		}
		return null;
	}
	
	
	/** Get a range of values
	 * 
	 * @param start starting index
	 * @param end ending index
	 * @return array of stored Objects
	 */
	public Object[] getValues(int start, int end) {
		if(start > end) return null;
		
		if(start < 0 || start > data.size()) return null;
		if(end > data.size()) return null;
		
		Object[] return_data = new Object[end-start+1];
		for(int i=start;i<=end;i++) {
			return_data[i-start] = data.get(i);
		}
		return return_data;
	}
	
	/** Gets all values as Objects
	 * 
	 * @return an array of all values in the variable as Objects
	 */
	public Object[] getValues() {
		Object[] return_data = new Object[data.size()];
		for(int i=0;i<data.size();i++) {
			return_data[i] = data.get(i);
		}
		return return_data;
	}
	
	/** Gets the size of an object
	 * 
	 * @return the number of entries in the variable
	 */
	public int size() {
		return data.size();
	}
	
	/** Returns the number of non-null values stored in this variable
	 * 
	 * @return an integer representing the number of non-null rows
	 */
	public int getObservationCount() {
		// Count the number of nonnull rows
		int non_null = 0;
		for(int i=0;i<data.size();i++) {
			if(data.get(i) != null)
				non_null++;
		}
		return non_null;
	}
	
	/** Returns the class of the data in the variable
	 * 
	 * @return the class of the data in the variable
	 */
	public Class getContentsClass() {
		return classid;
	}
	
	public String getContentType() {
		return classid.toString();
	}
	
	/** Resets the Contents Class id based on the currently stored data
	 * 
	 */
	public void resetContentsClass() {
		int i=0;
		
		if(data.size() == 0) return;
		
		for(i=0;i<data.size();i++) {
			if(data.get(i) != null) break;
		}
		if(i>data.size()) return;
		
		if(i < data.size()) {
			classid = data.get(i).getClass();
		} else {
			classid = Double.class;
		}
	}
	
	/** Resets the Contents Class id to the specified value, and destroys all
	 * stored data in the process if the new class is not the same as the old
	 * class
	 * 
	 * @param id the new class
	 */
	public void resetContentsClass(Class id) {
		if(id != classid) data = new ArrayList();
		classid = id;
	}
	
	/** Sets an existing value to a new object
	 * 
	 * @param index position to update
	 * @param value the new Object
	 */
	public void setValueAt(int index, Object value) {
		if(index < 0 || index > data.size()) {
			throw new ArrayIndexOutOfBoundsException();
		}
		
		data.set(index,value);
	}
	
	public void setAllValues(Object[] values) {
		if(values.length != data.size()) {
			// Should have a new exception for this
			System.err.println("New values are not sized the same as original");
			System.err.println("Ignoring set request");
			return;
		}

		for(int i=0;i<values.length;i++) {
			data.set(i,values[i]);
		}
		resetContentsClass();
	}
	
	/** Returns the name of the variable
	 * 
	 * @return the name as a String
	 */
	public String getName() {
		return name;
	}
	
	/** Sets the variable name
	 * 
	 * @param text the new variable name
	 */
	public void setName(String text) {
		name = text;
	}
	
	/** Returns the text of the equation used to calculate the variable, null if none
	 * 
	 * @return the equation as a String
	 */
	public String getEquation() {
		return equation;
	}
	
	/** Sets the stored equation to the passed text, but does not do
	 * any calculations
	 * 
	 * @param text the text of the equation
	 */
	public void setEquation(String text) {
		equation = text;
	}
	
	/** Returns all variables as integers, but only if the
	 * stored class id is Integer.class
	 * 
	 * @return an array of all values or null if NA
	 */
	public int[] getAllint() {
		int[] return_val = new int[data.size()];
		for(int i=0;i<data.size();i++) {
			return_val[i] = ((Number)data.get(i)).intValue();
		}
		return return_val;
	}
	
	/** Returns all variables that are not nukkas doubles
	 * 
	 * @return an array of all values or null if NA
	 */
	public double[] getAllNonNulldouble() {
		int valid = 0;
		for(int i=0;i<data.size();i++) {
			if(data.get(i) != null)
				valid++;
		}
		
		double[] return_val = new double[valid];
		int j = 0;
		for(int i=0;i<data.size()&&j<return_val.length;i++) {
			if(data.get(i) != null) {
				return_val[j] = ((Number)data.get(i)).doubleValue();
				j++;
			}
		}
		return return_val;
		
	}
	
	/** Returns all variables as doubles, but only if the
	 * stored class id is Double.class
	 * 
	 * @return an array of all values or null if NA
	 */
	public double[] getAlldouble() {
		double[] return_val = new double[data.size()];
		for(int i=0;i<data.size();i++) {
			if(data.get(i) == null) {
				return_val[i] = Double.NaN;
			} else {
				return_val[i] = ((Number)data.get(i)).doubleValue();
			}
		}
		return return_val;
	}
	
	/** Returns all variable values as Objects, including null values
	 * 
	 * @return an array of all values
	 */
	public Object[] getAll() {
		return data.toArray();
	}
	
	public Object getMaximumValue() {
		if(classid == Integer.class) {
			return getMaximumIntegerValue();
		} else if(classid == Double.class) {
			return getMaximumDoubleValue();
		} else {
			// Default to doubles
			return getMaximumDoubleValue();
			
			//System.err.println("Error using getMaximumValue() - Bad type");
			//System.err.println(classid.toString());
			//return null;
		}
	}
	
	public Object getMinimumValue() {
		if(classid == Integer.class) {
			return getMinimumIntegerValue();
		} else if(classid == Double.class) {
			return getMinimumDoubleValue();
		} else {
			// Default to doubles
			return getMinimumDoubleValue();
			
			//System.err.println("Error using getMaximumValue() - Bad type");
			//System.err.println(classid.toString());
			//return null;
		}
	}
	
	/** Gets the largest integer value
	 * 
	 * @return the largest value as an Integer
	 */
	private Integer getMaximumIntegerValue() {
		int largest = Integer.MIN_VALUE;
		for(int i=0;i<data.size();i++) {
			Number one_entry = (Number)data.get(i);
			if(one_entry != null)
				largest = Math.max(largest,one_entry.intValue());
		}
		return new Integer(largest);
	}
	
	/** Gets the smallest integer value
	 * 
	 * @return the smallest value as an Integer
	 */
	private Integer getMinimumIntegerValue() {
		int largest = Integer.MAX_VALUE;
		for(int i=0;i<data.size();i++) {
			Number one_entry = (Number)data.get(i);
			if(one_entry != null)
				largest = Math.min(largest,one_entry.intValue());
		}
		return new Integer(largest);
	}
	
	/** Gets the largest integer value
	 * 
	 * @return the largest value as an Double
	 */
	private Double getMaximumDoubleValue() {
		double largest = Double.MIN_VALUE;
		for(int i=0;i<data.size();i++) {
			Number one_entry = (Number)data.get(i);
			if(one_entry != null)
				largest = Math.max(largest,one_entry.doubleValue());
		}
		return new Double(largest);
	}
	
	/** Gets the smallest double value
	 * 
	 * @return the smallest value as an Double
	 */
	private Double getMinimumDoubleValue() {
		double largest = Double.MAX_VALUE;
		for(int i=0;i<data.size();i++) {
			Double one_entry = (Double)data.get(i);
			if(one_entry != null)
				largest = Math.min(largest,one_entry.doubleValue());
		}
		return new Double(largest);
	}
	
	/** Returns the mean value as a Double
	 * 
	 * @return the mean value
	 */
	public Double getMean() {
		double[] all_values = getAllNonNulldouble();
		if(all_values == null) return null;
		
		return new Double(VectorSupport.Mean(all_values));
	}
	
	/** Returns the median value in the variable
	 * 
	 * @return the median value as a Double
	 */
	public Double getMedian() {
		double[] all_values = getAllNonNulldouble();
		if(all_values == null) return null;
		
		Arrays.sort(all_values);
		int half_count = all_values.length/2-1;
		if(half_count < 0 || half_count >= all_values.length)
			half_count = 0;
		
		return new Double(all_values[half_count]);
	}
	
	/** Returns the standard deviation as a Double
	 * 
	 * @return the standard deviation
	 */
	public Double getColumnStandardDeviation() {
		double[] all_values = getAllNonNulldouble();
		
		if(all_values == null) return null;
		
		double std = VectorSupport.StandardDeviation(all_values);
		
		return new Double(std);
	}
	
	/** Overrides the Object.clone() method in a public form
	 * 
	 * @return a new VariableModel object
	 */
	public VariableModel clone() {
		
		// For dates, the bas must be rememebered
		DateObject base = null;
		
		Object[] values = new Object[data.size()];
		for(int i=0; i<data.size(); i++) {
			if(data.get(i) == null) {
				values[i] = null;
			} else {
				if(classid == Double.class) {
					values[i] = new Double(((Double)data.get(i)).doubleValue());
				} else if(classid == Integer.class) {
					values[i] = new Integer(((Integer)data.get(i)).intValue());
				} else if(classid == DateObject.class) {
					DateObject current = (DateObject)data.get(i);
					if(base == null) {
						base = new DateObject(current.getType(),
								              current.getYear(),
								              current.getQuarter(),
								              current.getMonth(),
								              current.getMonthHalf(),
								              current.getWeek(),
								              current.getDay());
						values[i] = base;
					} else {
						values[i] = new DateObject(base,current.getCountFrom(base));
					}
				} else {
						System.err.print("Cloning failed - unhandled class: ");
						System.err.println(data.get(i).getClass().toString());
						values[i] = null;
				}
			}
		}
		
		String new_name = new String(name);
		
		return new VariableModel(new_name,values); 
	}
	
	/** Removes an entry from the variable
	 * 
	 * @param i the index to remove
	 */
	public void remove(int i) {
		if(i < 0 || i >= data.size()) return;
		data.remove(i);
	}
	
	/** Adds an entry to the variable
	 * 
	 * @param i the index to add before
	 * @param value the new Object
	 */
	public void add(int i, Object value) {
		if(i < 0 || i >= data.size()) return;
		data.add(i,value);
	}
	
	/** Adds an entry to the variable
	 * 
	 * @param value the new Object to be appended
	 */
	public void add(Object value) {
		data.add(value);
	}
	
	/** Returns a description of the variable
	 * 
	 * @return a String representing the variable description, or null if none exists
	 */
	public String getDescription() {
		return description;
	}
	
	/**  Sets the description of the variable
	 * 
	 * @param describe the Sting to describe the variable, or null to erase
	 */
	public void setDescription(String describe) {
		description = describe;
	}
	
	public int[] sortAscending() {
		Object[] new_array = this.getAll();
		int[] placement = CustomSort.QuickSort(new_array);
		this.setAllValues(new_array);
		return placement;
	}
	
	public int[] sortDescending() {
		int[] placement = this.sortAscending();
		Object[] new_array = this.getAll();
		
		int top = placement.length-1;
		
		for(int i=0;i<placement.length/2;i++) {
			int temp = placement[i]; placement[i] = placement[top-i]; placement[top-i] = temp;
			Object temp2 = new_array[i]; new_array[i] = new_array[top-i]; new_array[top-i] = temp2;
		}
		
		this.setAllValues(new_array);
		return placement;
	}
	
	public void setNewOrder(int[] new_rows) {
		Object[] new_array = new Object[this.size()];
		for(int i=0;i<this.size();i++) {
			new_array[i] = this.getValueAt(new_rows[i]);
		}
		this.setAllValues(new_array);
	}

}
