package miningmart.storedProcedures;
import java.sql.SQLException;
import java.util.Vector;

public class WMF {

    public static final int AVERAGE = 0;
    public static final int MODAL   = 1;
    public static final int MEDIAN  = 2;

    private final DbConnector connector;
    private final Windowing   window; // for the input
    private final OutputColumnset out; // for the output

    private final double[] weights; // table that holds the weights
    private final int method;

    private static final int INDEXONATTRIB = 2;


    protected Windowing getWindow() {
	return this.window;
    }

    protected OutputColumnset getOutputColumnset() {
	return this.out;
    }


    /**
     * Constructor
     * @param dbc source of database connection if not used as stored procedure
     * @param source source table
     * @param time   time column of source table
     * @param column value column of source table
     * @param target name of the target table to be created
     * @param timeStartOut name of the interval start attribute
     * for the output table
     * @param timeEndOut name of the interval end attribute
     * for the output table
     * @param columnOut name of the output value attribute
     * @param weights a vector of weights, also specifies the window size
     * @param distance step size for windowing
     * @param method - 0: AVERAGE, 1: MODAL, 2: MEDIAN
     * @throws SQLException
     * @throws TimeOperatorException
    */    
    public WMF(BusinessDbConnectionSource dbc,
	    	   String source, String time, String column, String target,
		       String timeStartOut, String timeEndOut, String columnOut,
		       double[] weights, int distance, int method)
		throws SQLException, TimeOperatorException
    {
		this.weights = weights;
		this.method = method;

		this.connector = new DbConnector(dbc);
		final short timeDT   = DataType.getColumnDataType(source, time, this.connector);
		final short columnDT = DataType.getColumnDataType(source, column, this.connector);

		// Input:
		final Attribute timeAttr   = new Attribute(time,   timeDT,   timeDT);
		final Attribute columnAttr = new Attribute(column, columnDT, DataType.NUMBER);
	
		if (!timeAttr.isConvertable()) {
	    	DataType.wrongDataType(target, timeAttr);
		}
		
		if (!columnAttr.isConvertable()) {
		    DataType.wrongDataType(target, columnAttr);
		}

		// Output:
		final Attribute timeStartAttr = new Attribute(timeStartOut, timeDT,  timeDT);
		final Attribute timeEndAttr   = new Attribute(timeEndOut,   timeDT,  timeDT);
		final Attribute columnOutAttr = new Attribute(columnOut,  columnDT,  DataType.NUMBER);
		Attribute[] allAttributes = new Attribute[] { timeStartAttr, timeEndAttr, columnOutAttr};
		this.out = new OutputColumnset(target, allAttributes, this.connector.getConnection());

		// Prepare Windowing:
		this.window = new Windowing(weights.length, distance, this.connector);
		if (!window.open("SELECT " + columnAttr.getConvertedInput() + ", " +
			 timeAttr.getConvertedInput() + " FROM " + source))
	    {
			DbConnector.infoOutput("Error while opening " + source);
			throw new TimeOperatorException(
				"Could not read from table '" + source + "'!"
			);
	    }
    }

    /**
     * calculation function, if the average method should be used
     * @param window the data window to process
     * @param weights an array that holds the weights
     * @return the calculated average
     */
    private double processAverage(String[] window, double[] weights) {
		double sum = 0;
		for (int i = 0; i < window.length; i++) {
	    	sum+= Double.parseDouble(window[i]) * weights[i];
		}	
		return (sum);
    }
    
    /**
     * calculation function, if the modal method should be used
     * @param window the data window to process
     * @param weights an array that holds the weights
     * @return the calculated average
     * */
    private double processModal(String[] window, double[] weights) {
		double sum = 0;
	
		// Convert the array to double for sorting
		double[] dblWnd = new double[window.length];
		for (int i = 0; i < window.length; i++) {
	    	dblWnd[i] = Double.parseDouble(window[i]);
		}
	
		// sort the array
		java.util.Arrays.sort(dblWnd);

		double[] cnt = new double[dblWnd.length];
		java.util.Arrays.fill(cnt,0);
		int i = 0;
		while (i < dblWnd.length - 2) {
		    int cntIndex = i;
	    	while (dblWnd[i] == dblWnd[i + 1]) {
				cnt[cntIndex]++;
				i++;
	    	}
		    i++;
		}
	
		Quicksort.sort(cnt,dblWnd,0,dblWnd.length - 1);
	
		for (i = 0; i < dblWnd.length; i++) {
		    sum+= dblWnd[i] * weights[i];
		}

		return (sum);
    }

    /**
     * calculation function, if the median method should be used
     * @param window: the data window to process
     * @param weights: an array that holds the weights
     * @return the calculated average
     * */
	private double processMedian(String[] window, double[] weights) {
		double sum = 0;

		// Convert the array to double for sorting
		double[] dblWnd = new double[window.length];
		for (int i = 0; i < window.length; i++) {
			dblWnd[i] = Double.parseDouble(window[i]);
		}
	
		// sort the array
		java.util.Arrays.sort(dblWnd);
		double median = dblWnd[dblWnd.length / 2];
		double[] space = new double[dblWnd.length];
		for (int i = 0; i < dblWnd.length; i++) {
			space[i] = Math.abs(median - dblWnd[i]);
		}

		// sort the spaces
		Quicksort.sort(space, dblWnd, 0, dblWnd.length - 1);
		for (int i = 0; i < dblWnd.length; i++) {
			sum += dblWnd[i] * weights[i];
		}
		return (sum);
	}
	
    /* moves all values in an array one index left */
    private void shiftArray(double[] array) {
		for (int i = array.length - 1; i > 0; i--) {
		    array[i - 1] = array[i];
		}
    }
    
    /** 
     * Method calc() reads from the column(s) specified in the constructor
     * and writes to the specified target, using windowing.
     * <!-- Finally an index is created on the target table. -->
     */
    public void calc() throws TimeOperatorException {
		String[] ret = null;
		do {
		    ret = this.getWindow().getNextWnd();
		    String startTime = this.getWindow().getTimeFirstEntry();
	    	String endTime = this.getWindow().getTimeLastEntry();
		    if (ret != null) {
				Double value = null;
				switch (method) {
				case AVERAGE :
			    	value = new Double(processAverage(ret, weights));
			    break;
				case MODAL :
				    value = new Double(processModal(ret, weights));
			    break;
				case MEDIAN :
		    		value = new Double(processMedian(ret, weights));
			    break;
				}
				this.getOutputColumnset().insert(new Object[] {startTime, endTime, value});
	    	}
		} while (ret != null);

		// this.getOutputColumnset().createIndexOn(INDEXONATTRIB);
		// *** No index is created in this compiler version! ***

		this.getWindow().close();
		this.connector.close();
    }



    /*  dbWMA( InputColumnSet     VARCHAR2,
     *         TimeAttribIn       VARCHAR2,
     *         ValueAttribIn      VARCHAR2,
     *         TargetColumnSet    VARCHAR2,
     *         TimeStartAttribOut VARCHAR2,
     *         TimeEndAttribOut   VARCHAR2,
     *         ValueAttribOut     VARCHAR2,
     *         weights            VARCHAR2,
     *         distance           NUMBER)   */

    /**
     * This function has to be used in the database as a stored
     * procedure for calculating an WMF.
     */
    public static void dbWMA(Object dbc,
    						 String source, String time, String column, String target,
						     String timeStartOut, String timeEndOut,
						     String columnOut, String weights, int distance)
		throws SQLException, TimeOperatorException
    {
    	BusinessDbConnectionSource dbcCasted =
			(dbc instanceof BusinessDbConnectionSource) ?
				((BusinessDbConnectionSource) dbc) : null;
		
    	
		double[] numWeights = parseWeightList(weights);
		if ((numWeights == null) || (numWeights.length == 0)) {
	    	return;
		}
		WMF wmf = new WMF(dbcCasted, source, time, column, target,
						  timeStartOut, timeEndOut, columnOut,
						  numWeights, distance, WMF.AVERAGE);
		wmf.calc();
		wmf = null;
    }

	private static double[] parseWeightList(String s) {
		if (s == null)
			return null;
		Vector values = new Vector();
		int stop = 0;
		for (int from = 0; from < s.length(); from = stop) {
			from = nextNonWhitespace(s, from);
			if (from == s.length())
				break;
			stop = nextWhitespace(s, from);
			String sub = s.substring(from, stop);
			double d = Double.parseDouble(sub);
			values.add(new Double(d));
		}
		double weights[] = new double[values.size()];
		for (int arg = 0; arg < weights.length; arg++)
			weights[arg] = ((Double) (values.elementAt(arg))).doubleValue();
		return (weights);
	}
	
    private static int nextNonWhitespace(String s, int index) {
		for (;((index<s.length())&&(Character.isWhitespace(s.charAt(index))));index++);
		return (index);
    }

    private static int nextWhitespace(String s, int index) {
		for (;((index<s.length())&&(!Character.isWhitespace(s.charAt(index))));index++);
		return (index);
    }

}
