/*
 *  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.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Vector;
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 extends Vector<Number> implements Cloneable {
	
	private Class classid = null;
	
	private String name = null;
	
	private String equation = null;
	
	private String description = null;
	
	private DecimalFormat decimalFormat = null;
	
	/** Creates a new variable filled with Doubles
	 * 
	 * @param var_name the variable's name
	 * @param size default initial size (for speed reasons)
	 */
	public VariableModel(String var_name, int size) {
		super(size);
		classid = Double.class;
		name = var_name;
		decimalFormat = 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) {
		this(var_name,size);
		for(int i=0;i<size;i++) {
			this.add(new Double(fillvalue));
		}
	}
	
	/** Creates a new variable from a vector of doubles
	 * 
	 * @param values values to fill in array with
	 */
	public VariableModel(String var_name, double[] values) {
		super(values.length);
				
		for(int i=0;i<values.length; i++) {
			super.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) {
		super(values.length);
		
		for(int i=0;i<values.length; i++) {
			if(values[i] != null && values[i] instanceof Number)
				super.add((Number)values[i]);
			else
				super.add(null);
		}
		
		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) {
		try {
			Number value = get(index);
			if(value == null)
				return true;
			else if(value instanceof DateObject)
				return false;
			else
				return true;
		} catch(ArrayIndexOutOfBoundsException ab) {
			return false;
		}
	}
	
	/** 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 getValueAt(int index) {
		try {
			return super.get(index);
		}catch(ArrayIndexOutOfBoundsException ab) {
			return null;
		}
	}
	
	/** Get a range of values
	 * 
	 * @param start starting index
	 * @param end ending index
	 * @return array of stored Objects
	 */
	public Number[] getValues(int start, int end) {
		if(start > end) return null;
		
		if(start < 0 || start > super.size()) return null;
		if(end > super.size()) return null;
		
		Number[] return_data = new Number[end-start+1];
		for(int i=start;i<=end;i++) {
			return_data[i-start] = super.get(i);
		}
		return return_data;
	}
	
	/** Gets all values
	 * 
	 * @return an array of all values in the variable as Objects
	 */
	public Number[] getValues() {
		return this.toArray(new Number[0]);
	}
	
	/** 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<super.size();i++) {
			if(super.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(super.size() == 0) return;
		
		classid = Integer.class;
		for(Number entry : this) {
		    if(entry==null) continue;
		    
		    if(entry instanceof DateObject) {
		        classid = DateObject.class;
		        break;
		    } else if(entry instanceof Double) {
		        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) {
		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) throws ArrayIndexOutOfBoundsException {
		if(value instanceof Number)
			super.set(index, (Number)value);
		else
			super.set(index, null);
	}
	
	public void setAllValues(Object[] values) throws ArrayIndexOutOfBoundsException {
		if(values.length != super.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++) {
			if(values[i] instanceof Number)
				super.set(i,(Number)values[i]);
			else
				super.set(i,null);
		}
		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[super.size()];
		for(int i=0;i<super.size();i++) {
			try {
				return_val[i] = super.get(i).intValue();
			} catch(NullPointerException npe) {
				return_val[i] = 0;
			}
		}
		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<super.size();i++) {
			if(this.get(i) != null)
				valid++;
		}
		
		double[] return_val = new double[valid];
		int j = 0;
		for(int i=0;i<super.size()&&j<return_val.length;i++) {
			if(this.get(i) != null) {
				return_val[j] = this.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[super.size()];
		for(int i=0;i<super.size();i++) {
			try {
				return_val[i] = super.get(i).doubleValue();
			} catch(NullPointerException npe) {
				return_val[i] = Double.NaN;
			}
		}
		return return_val;
	}
	
	public Number getMaximumValue() {
		double max = -1.0*Double.MAX_VALUE;
		for(Number val : this) {
			try {
				if(!Double.isNaN(val.doubleValue()))
					max = Math.max(max, val.doubleValue());
			} catch(NullPointerException npe) {
				// pass
			}
		}
		if(max != -1.0*Double.MAX_VALUE)
			return new Double(max);
		else
			return null;
	}
	
	public Number getMinimumValue() {
		double min = Double.MAX_VALUE;
		for(Number val : this) {
			try {
				if(!Double.isNaN(val.doubleValue()))
					min = Math.min(min, val.doubleValue());
			} catch(NullPointerException npe) {
				// pass
			}
		}
		if(min != Double.MAX_VALUE)
			return new Double(min);
		else
			return null;
	}
	
	/** Returns the mean value as a Double
	 * 
	 * @return the mean value
	 */
	public Double getMean() {
		double[] all_values = getAllNonNulldouble();
		if(all_values == null) return null;
		if(all_values.length == 0) 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;
		if(all_values.length == 0) return null;
		
		Arrays.sort(all_values);
		int half_count = all_values.length/2;
		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;
		if(all_values.length == 0) 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;

		VariableModel new_var = new VariableModel(name,this.size()); 
		
		for(Number val : this)
			new_var.add(val);
		
		new_var.resetContentsClass(this.classid);
		return new_var;
	}
	
	/** 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;
	}
	
	/** Sets the decimal format pattern string for displaying
	 * numbers in formatted form.
	 * @param format a string conforming to Java's decimal format patterns
	 * @return true if successful
	 */ 
	public boolean setDecimalFormatting(String format) {
		if(format == null || this.getContentsClass() == DateObject.class) {
			decimalFormat = null;
			return true;
		}
		
		if(decimalFormat == null)
			decimalFormat = new DecimalFormat();
		
		try {
			decimalFormat.applyPattern(format);
			return true;
		} catch(IllegalArgumentException iae) {
			return false;
		}
	}
	
	/** Returns the current decimal formatting
	 * 
	 * @return a DecimalFormat object or null when approppriate
	 */
	public DecimalFormat getDecimalFormatting() {
		return decimalFormat;
	}
	
	public int[] sortAscending() {
		Object[] new_array = this.getValues();
		int[] placement = CustomSort.QuickSort(new_array);
		this.setAllValues(new_array);
		return placement;
	}
	
	public int[] sortDescending() {
		int[] placement = this.sortAscending();
		Object[] new_array = this.getValues();
		
		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);
	}

}
