/*
 * MiningMart Version 1.0
 * 
 * Copyright (C) 2006 Martin Scholz, Timm Euler, 
 *                    Daniel Hakenjos, Katharina Morik
 *
 * Contact: miningmart@ls8.cs.uni-dortmund.de
 *
 * A list of contributing developers (other than the copyright 
 * holders) can be found at
 * http://mmart.cs.uni-dortmund.de/downloads/download.html
 * 
 * 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 2
 * 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, see the file MM_HOME/LICENSE; if not, write
 * to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301, USA.
 */
package edu.udo.cs.miningmart.operator;

import java.util.Collection;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;

import edu.udo.cs.miningmart.db.DB;
import edu.udo.cs.miningmart.exception.M4CompilerError;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.m4.BaseAttribute;
import edu.udo.cs.miningmart.m4.Column;
import edu.udo.cs.miningmart.m4.Columnset;
import edu.udo.cs.miningmart.m4.Feature;
import edu.udo.cs.miningmart.m4.MultiColumnFeature;
import edu.udo.cs.miningmart.m4.Value;
import edu.udo.cs.miningmart.m4.RelationalDatatypes;
import edu.udo.cs.miningmart.m4.utils.Print;

/**
 * This operator denormalizes or flattens out certain attributes, which is called 
 * Pivotizing. This means that for each value that occurs in the column of 
 * one or two "index" attributes, a new attribute is created. This new attribute 
 * contains the aggregation over those values of the "pivot" attribute where 
 * the index attribute(s) took the value that corresponds to the new
 * attribute. 
 * 
 * Example where aggregation is done by summing up (I = index attribute, P = pivotised attribute):
 * 
 * Input              Output
 * 
 *  I | P          P_M | P_F
 * ------          ----------
 *  M | 4    =>     6  | 5
 *  M | 2           
 *  F | 5          
 *
 * @author Timm Euler
 * @version $Id: Pivotize.java,v 1.6 2006/05/19 16:24:05 euler Exp $
 */
public class Pivotize extends SingleCSOperator {

	// Parameter names for this Operator
	public static final String PARAMETER_GROUPBY_ATTR = "TheGroupByAttributes";
	public static final String PARAMETER_INDEX_ATTR   = "TheIndexAttributes";
	public static final String PARAMETER_PIVOT_ATTR   = "ThePivotAttribute";
	public static final String PARAMETER_VALUES       = "IndexValues";
	public static final String PARAMETER_AGGREGATION  = "AggregationOperator";
	public static final String PARAMETER_NULLORZERO   = "NullOrZero";
	
	public static final String NO_AGGREGATION_VALUE = "NONE";
		
	private Collection myAttributeValueCombinations = null;
	
	// two flags to control aggregation:
	private boolean groupByAttribsExist = false;
	private boolean aggregationFunctionExists = false; 
	
	/**
	 * @see miningmart.compiler.operator.SingleCSOperator#getTypeOfNewColumnSet()
	 */
	public String getTypeOfNewColumnSet() {
		return Columnset.CS_TYPE_VIEW;
	}

	/**
	 * @see edu.udo.cs.miningmart.operator.SingleCSOperator#generateSQLDefinition(String)
	 */
	public String generateSQLDefinition(String selectPart) throws M4CompilerError {
		if (( ! this.aggregationFunctionExists) && this.groupByAttribsExist) {
			throw new M4CompilerError("Operator Pivotize, Step '" + this.getStep().getName() + 
					"': cannot 'group by' without an aggregation function!");
		}
		try {
			String sqlDef = "(SELECT " + selectPart + 
			                " FROM " + getInputConcept().getCurrentColumnSet().getSchemaPlusName();
			if (this.groupByAttribsExist) {
				sqlDef += " GROUP BY " + this.getGroupByStatement();
			}
			sqlDef += ")";
			return sqlDef;
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator Pivotize: M4Exception caught " +
			                          "when accessing input concept/its columnset: " + m4e.getMessage());
		}
	}

	/**
	 * This method is overridden because this operator uses a mapping to fill
	 * the output BaseAttributes.
	 * 
	 * @see edu.udo.cs.miningmart.m4.operator.ConceptOperator#generateColumns(Columnset)
	 */
    protected String generateColumns(Columnset csForOutputConcept) throws M4CompilerError {
    	
		String columnExpr = ""; // to be returned
		Feature[] theGroupByFeatures = (Feature[]) this.getParameter(PARAMETER_GROUPBY_ATTR);
		Feature[] theIndexAttrs = (Feature[]) this.getParameter(PARAMETER_INDEX_ATTR);

		/*
		int noOfLoops = this.getNumberOfLoops();
		for (int i = 0; i < noOfLoops; i++) {
			theMappedBAs[i] = (BaseAttribute) this.getSingleParameter(PARAMETER_MAPPED_ATTR, i);
			if (theMappedBAs[i] == null) {
				throw new M4CompilerError("Operator PivotizeWithAggregation: Found no entry for '" +
				                          PARAMETER_MAPPED_ATTR + "' for loop number " + i + "!");
			}
			theIndexValues[i] = (Value) this.getSingleParameter(PARAMETER_VALUES, i);
			if (theIndexValues[i] == null) {
				throw new M4CompilerError("Operator PivotizeWithAggregation: Found no entry for '" +
				                          PARAMETER_VALUES + "' for loop number " + i + "!");
			}			
		}		
		*/
		try	{
			// go through the Features of the output concept:
			Collection outFeatures = this.getOutputConcept().getFeatures();
			if (outFeatures == null || outFeatures.isEmpty()) {
				throw new M4CompilerError("Operator Pivotize: No Features found in the output concept!");
			}
			Iterator it = outFeatures.iterator();
		 L:	while (it.hasNext()) {
				Feature outF = (Feature) it.next();
				
				// for debugging, check that it's not an index BA:
				for (int i = 0; i < theIndexAttrs.length; i++) {
					if (outF.correspondsTo(theIndexAttrs[i])) {
						throw new M4CompilerError("Operator Pivotize: found an Index BA in the Output Concept!");
					}
				}
				
				// check if it's a GroupBy-Feature:
				Feature inF = null;
				if (theGroupByFeatures != null && theGroupByFeatures.length > 0) {
					inF = correspondsToOneOf(outF, theGroupByFeatures);
				}
				if (inF != null) {
					// inF is a GroupBy-Feature
					this.groupByAttribsExist = true;
					columnExpr = this.createMetadata(inF, outF, csForOutputConcept, columnExpr);
				}				
				else {
					// outF must be a newly created Attribute, so find the corresponding value of the index attribute:
					if ( ! (outF instanceof BaseAttribute)) {
						throw new M4CompilerError("Pivotize ('" + this.getStep().getName() +
								"'): cannot handle MultiColumnFeatures like '" +
								outF.getName() + "!");
					}
					AttributeValueCombination theCombi = this.getCombination((BaseAttribute) outF);
					if (theCombi == null) {					
	                    this.doPrint(Print.OPERATOR, "Operator PivotizeWithAggregation: skipped OutputConcept feature '" +
	                                  outF.getName() + "' because no corresponding GroupBy- or Output Attribute was found.");
	                    continue L;
					}
					columnExpr = this.createPivotizedColumn(theCombi, (BaseAttribute) outF, csForOutputConcept, columnExpr);
				}
			
			} // end of loop through the output features
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator Pivotize: M4 Exception caught when generating metadata: " +
			                          m4e.getMessage());
		}	
		if (columnExpr.length() > 2) {
			columnExpr = columnExpr.substring(0, columnExpr.length() - 2);
		}
		else {
			throw new M4CompilerError("Operator Pivotize: No columns created for output concept!");
		}
		
		return columnExpr;		             
    }
    
	/**
	 * This method is never called.
	 * 
	 * @see miningmart.compiler.operator.ConceptOperator#mustCopyFeature(String)
	 */
	protected boolean mustCopyFeature(String nameOfFeature)	throws M4CompilerError {
		return false;
	}
	
	// checks if the given feature corresponds to a feature given in the feature array,
	// and returns the corresponding feature if yes, or NULL otherwise
	private Feature correspondsToOneOf(Feature f, Feature[] someFeatures) {
		for (int i = 0; i < someFeatures.length; i++) {
			if (someFeatures[i].correspondsTo(f)) {
				return someFeatures[i];
			}			
		}
		return null;
	}
	
	private String createPivotizedColumn( AttributeValueCombination theCombi,
										  BaseAttribute outBA,
	                                      Columnset csForOutputConcept, 
	                                      String columnExpr) throws M4CompilerError {

		// Get the pivot and index BAs and Columns:
		BaseAttribute pivotBA = (BaseAttribute) this.getSingleParameter(PARAMETER_PIVOT_ATTR);		

	    // read the optional parameter NullOrZero:
	    Value nullOrZero = (Value) this.getSingleParameter(PARAMETER_NULLORZERO);
	    String myNull;
	    if (nullOrZero == null || nullOrZero.getValue().equalsIgnoreCase("null")) { // this parameter is optional, NULL is default
	    	myNull = "NULL";
	    }
	    else if (nullOrZero.getValue().equalsIgnoreCase("zero")) {
	    	myNull = "0";
	    }
	    else throw new M4CompilerError("Operator Pivotize: Parameter '" + PARAMETER_NULLORZERO +
	                                   "' must be 'Null' or 'Zero'!");

		// get Aggregation operator:
		Value agg = (Value) this.getSingleParameter(PARAMETER_AGGREGATION);
		String aggregationOpString = "";
		if (  ! agg.getValue().equals(NO_AGGREGATION_VALUE)) {
			aggregationOpString = agg.getValue().toUpperCase();
			this.aggregationFunctionExists = true;
		}
		
		Column pivotCol, indexCol;
		String when = null;
		try {
			pivotCol = pivotBA.getCurrentColumn();
			if (pivotCol == null) { 
				throw new M4CompilerError("Operator Pivotize: Could not find column for pivot BA '" + 
				                          pivotBA.getName() + "!");
			}
			when = "";
			for (int i = 0; i < theCombi.getNoOfIndexes(); i++) {
				BaseAttribute indexBA = theCombi.getIndexBA(i);
				indexCol = indexBA.getCurrentColumn();			
				if (indexCol == null) { 
					throw new M4CompilerError("Operator Pivotize: Could not find column for index BA '" + 
												indexBA.getName() + "'!");
				}
				// get the index value:
				String indexValue = theCombi.getIndexValue(i);
				// check if the index Column is string:
				boolean indexColNominal = (indexCol.getColumnDataTypeName().equalsIgnoreCase(RelationalDatatypes.RELATIONAL_DATATYPE_STRING));
				when += indexCol.getSQLPlusLocation() + " = " +
					(indexColNominal ? DB.quote(indexValue) : indexValue) +
					" AND ";
			}
			if ( ! when.equals("")) {
				when = when.substring(0, when.length() - 5);
			}
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator Pivotize: M4 Exception caught when accessing column " +
			                          "of index or pivot attribute: " + m4e.getMessage());
		}	 
		
		if (when == null || when.equals("")) {
			throw new M4CompilerError("Operator Pivotize (Step '" + this.getStep().getName() +
					"'): no when conditions created!");
		}
		
	    String colDef = aggregationOpString + 
	                    "(CASE WHEN " +
	                    when + 
	                    " THEN " +
	                    pivotCol.getSQLPlusLocation() +
	                    " ELSE " + myNull + " END)";
	               
	    // create Metadata for the new Column:
	    Column outputColumn;
        try {
            outputColumn = pivotCol.copyColToCS(csForOutputConcept);
            this.getStep().addToTrash(outputColumn);
            outputColumn.setBaseAttribute(outBA);
            outputColumn.setSQLDefinition(outBA.getName());
            outputColumn.setName(outBA.getName());
	    
            return columnExpr + colDef + " AS " + outBA.getName() + ", "; 
        }
        catch (M4Exception m4e) {
            throw new M4CompilerError("Operator Pivotize: M4 exception caught when creating column for " +
                                      "output BaseAttribute '" + outBA.getName() + 
                                      "': " + m4e.getMessage());
        } 
	}
	
	// Lists the names of the columns of the GroupBy-Attributes, separated by commas, in a String
	private String getGroupByStatement() throws M4CompilerError {
		Feature[] theGroupByFeatures = (Feature[]) this.getParameter(PARAMETER_GROUPBY_ATTR);
		String theList = "";
		try {
			for (int i = 0; i < theGroupByFeatures.length; i++) {
				if (theGroupByFeatures[i] instanceof BaseAttribute) {
					theList += ((BaseAttribute) theGroupByFeatures[i]).getCurrentColumn().getName() + ", ";
				}
				else {
					if ( ! (theGroupByFeatures[i] instanceof MultiColumnFeature)) {
						throw new M4CompilerError("Operator Pivotize: One of the features listed in '" +
						                          PARAMETER_GROUPBY_ATTR + "' is neither a BaseAttribute nor a MCF!");
					}
					Collection c = ((MultiColumnFeature) theGroupByFeatures[i]).getBaseAttributes();
					if (c == null || c.isEmpty()) {
						throw new M4CompilerError("Operator Pivotize: No BAs found in MCF '" +
						                          theGroupByFeatures[i].getName() + "'!");
					}
					Iterator it = c.iterator();
					while (it.hasNext()) {
						BaseAttribute ba = (BaseAttribute) it.next();
						theList += ba.getCurrentColumn().getName() + ", ";
					}
				}
			}
		}
		catch (M4Exception m4e) {
			throw new M4CompilerError("Operator Pivotize: M4Exception caught when collecting the names " +
			                          "of the GroupBy-Attributes: " + m4e.getMessage());
		}
		if (theList.length() > 2) {
			theList = theList.substring(0, theList.length() - 2);
		}
		return theList;
	}	
	
	private BaseAttribute[] getIndexBAs() throws M4CompilerError {
		BaseAttribute[] bas = (BaseAttribute[]) this.getParameter(PARAMETER_INDEX_ATTR);
		if (bas == null || bas.length == 0) {
			throw new M4CompilerError("Operator Pivotize: Parameter '" +
			                          PARAMETER_INDEX_ATTR + "' not found!");
		}
		return bas;
	}
	
	private String[] getIndexVals() throws M4CompilerError {
		Value[] vals = (Value[]) this.getParameter(PARAMETER_VALUES);
		if (vals == null || vals.length == 0) {
			throw new M4CompilerError("Operator Pivotize: Parameter '" +
			                          PARAMETER_VALUES + "' not found!");
		}
		String[] st = new String[vals.length];
		for (int i = 0; i < vals.length; i++) {
			st[i] = vals[i].getValue();
		}
		return st;
	}
	
	private Collection getAttrValueCombis() throws M4CompilerError {
		if (this.myAttributeValueCombinations != null) {
			return this.myAttributeValueCombinations;
		}
		else {
			BaseAttribute[] indexBAs = this.getIndexBAs();
			String[] indexVals = this.getIndexVals();
			// here every indexVal contains values that the corresponding
			// indexBA takes. Now we compute all combinations of values:
			try {
				this.myAttributeValueCombinations = getCombinations(
						indexVals, 
						indexBAs, 
						((BaseAttribute) this.getSingleParameter(PARAMETER_PIVOT_ATTR)));
				return this.myAttributeValueCombinations;
			}
			catch (M4Exception m4e) {
				throw new M4CompilerError("Pivotize-Operator in Step '" + this.getStep().getName() +
						"': error computing combinations of index values: " + m4e.getMessage());
			}
		}
	}
	
	private AttributeValueCombination getCombination(BaseAttribute outputBa) throws M4CompilerError {
		Iterator it = this.getAttrValueCombis().iterator();
		while (it.hasNext()) {
			AttributeValueCombination avc = (AttributeValueCombination) it.next();
			if (avc.getOutputBaName().equalsIgnoreCase(outputBa.getName())) {
				return avc;
			}
		}
		return null;
	}
	
	public static Collection getCombinations(String[] indexValues, 
			 								 BaseAttribute[] indexAttrs,
											 BaseAttribute pivotAttr) 
	throws M4Exception {
		if (indexValues == null || indexValues.length == 0 ||
			indexAttrs == null || indexAttrs.length == 0) {
			throw new M4Exception("Pivotize: got empty index information!");
		}
		if  (indexValues.length != indexAttrs.length) {
			throw new M4Exception("For pivotisation an equal number of index value parameters and index attributes is needed!");
		}
		Vector ret = new Vector();
		Vector currentCombi = new Vector();
		
		computeCombinations(0, indexAttrs, pivotAttr, indexValues, currentCombi, ret, new Pivotize());
		
		return ret;
	}
	
	private static void computeCombinations( int currentIndex, 
											 BaseAttribute[] indexAttrs, 
											 BaseAttribute pivotAttr, 
											 String[] indexValues, 
											 Vector currentCombination,
											 Collection theCombis,
											 Pivotize dummy) 
	throws M4Exception {
		String valuesOfCurrentIndexAttr = indexValues[currentIndex];
		StringTokenizer st = new StringTokenizer(valuesOfCurrentIndexAttr, ", ");
		while (st.hasMoreTokens()) {
			String nextValue = st.nextToken().trim();
			Vector nextCombination = (Vector) currentCombination.clone();
			nextCombination.add(nextValue);
			if (currentIndex < indexValues.length - 1) {
				computeCombinations(currentIndex + 1, 
						            indexAttrs, 
									pivotAttr, 
									indexValues, 
									nextCombination, 
									theCombis, 
									dummy);
			}
			else {
				Object[] current = nextCombination.toArray();
				String[] currentCombi = new String[current.length];
				for (int i = 0; i < current.length; i++) {
					currentCombi[i] = current[i].toString();
				}
				AttributeValueCombination theCombi = dummy.new AttributeValueCombination(
						 getNameOfOutputAttribute(currentCombi, pivotAttr.getName()),
						 indexAttrs,
						 currentCombi);
				theCombis.add(theCombi);
			}
		}			
	}
	
	public static String getNameOfOutputAttribute( String[] indexValue,
												   String pivotAttrName) {
		String ret = pivotAttrName + "_";
		for (int i = 0; i < indexValue.length; i++) {
			ret += indexValue[i] + "_";
		}
		ret = ret.substring(0, ret.length() - 1);
		return ret;
	}
	
	public class AttributeValueCombination {
		private String output;
		private BaseAttribute[] indexAttrs;
		private String[] indexVals;
		
		public AttributeValueCombination(String outBaName, BaseAttribute[] indexBAs, String[] indexValues) throws M4Exception {
			if (indexBAs.length != indexValues.length) {
				throw new M4Exception("Operator Pivotize: The number of index attributes and the number of index values in a value combination must be equal!");
			}
			this.output = outBaName;
			this.indexAttrs = indexBAs;
			this.indexVals = indexValues;
		}
		
		public String getOutputBaName() {
			return this.output;
		}
		
		public int getNoOfIndexes() {
			return this.indexAttrs.length;
		}
		
		public BaseAttribute getIndexBA(int index) {
			return this.indexAttrs[index];
		}
		
		public String getIndexValue(int index) {
			return this.indexVals[index];
		}
	}
}
/*
 * Historie
 * --------
 * 
 * $Log: Pivotize.java,v $
 * Revision 1.6  2006/05/19 16:24:05  euler
 * New operator 'ReversePivotize'.
 *
 * Revision 1.5  2006/04/11 14:10:12  euler
 * Updated license text.
 *
 * Revision 1.4  2006/04/06 16:31:11  euler
 * Prepended license remark.
 *
 * Revision 1.3  2006/03/20 09:15:46  euler
 * Updated Pivotize to allow pivotisation without aggregation.
 *
 * Revision 1.2  2006/03/17 17:06:39  euler
 * *** empty log message ***
 *
 * Revision 1.1  2006/01/03 09:54:22  hakenjos
 * Initial version!
 *
 */
