/*
 * 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.Arrays;

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

import edu.udo.cs.miningmart.exception.M4CompilerError;
import edu.udo.cs.miningmart.exception.M4Exception;
import edu.udo.cs.miningmart.exception.ParameterDeselectedError;
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.Concept;
import edu.udo.cs.miningmart.m4.Feature;
import edu.udo.cs.miningmart.m4.Relation;
import edu.udo.cs.miningmart.m4.Step;
import edu.udo.cs.miningmart.m4.utils.Print;

/**
 * @author Timm Euler
 * @version $Id: MultiRelationalFeatureConstruction.java,v 1.7 2006/04/11 14:10:12 euler Exp $
 */
public class MultiRelationalFeatureConstruction extends SingleCSOperator
{
    private String condition, listOfColumnSets;

	/**
	 * Gets the myChainConcepts.
	 * @return Returns a Concept[]
	 */
	protected Concept[] getMyChainConcepts() throws M4CompilerError {
		return (Concept[]) this.getParameter("TheConcepts");
	}

	/**
	 * Gets the myRelations.
	 * @return Returns a Relation[]
	 */
	protected Relation[] getMyRelations() throws M4CompilerError {
		return (Relation[]) this.getParameter("TheRelations");
	}

	/**
	 * Gets the selectedFeatures.
	 * @return Returns a Feature[]
	 */
	protected Feature[] getSelectedFeatures() throws M4CompilerError {
		return (Feature[]) this.getParameter("TheChainedFeatures");
	}

	/**
	 * Gets the condition.
	 * @return Returns a String
	 */
	protected String getCondition() {
		return condition;
	}

	/**
	 * Gets the listOfColumnSets.
	 * @return Returns a String
	 */
	protected String getListOfColumnSets() {
		return listOfColumnSets;
	}

	/**
	 * Sets the condition.
	 * @param condition The condition to set
	 */
	protected void setCondition(String condition) {
		this.condition = condition;
	}

	/**
	 * Sets the listOfColumnSets.
	 * @param listOfColumnSets The listOfColumnSets to set
	 */
	protected void setListOfColumnSets(String listOfColumnSets) {
		this.listOfColumnSets = listOfColumnSets;
	}


    public void load(Step st) throws ParameterDeselectedError, M4CompilerError {
	    super.load(st);
        checkChainValidity();
    }

    /**
     * Must implement this method from ConceptOperator().
     */
    public String getTypeOfNewColumnSet()
    {  return Columnset.CS_TYPE_VIEW;  }

    /* This method is only implemented for the java compiler, it is never called... */
    protected boolean mustCopyFeature(String nameOfFeature)
    {  return false;  }

    /**
     * Implementation of the abstract method from <code>SingleCSOperator</code>.
     */
    public String generateSQLDefinition(String selectPart) throws M4CompilerError
    {
	    String ret = "(select " + selectPart +
		             " from " + this.getListOfColumnSets() +
	    	         " where " + this.getCondition() + ")";
	    return ret;
    }

    /**
     * This method overrides the method in <code>ConceptOperator</code>.
	 * 
	 * @see miningmart.operator.ConceptOperator#generateColumns(Columnset)
     */
    protected String generateColumns(Columnset csForOutputConcept) throws M4CompilerError
    {
		try
		{
			// go through the chain of concepts, get the current columnsets, find
			// the keys that link them (by name of column of key objects), compose
			// the join condition, the list of columns and columnsets.

			// Referring to the UserGuide, in the following code, 
			// the Concepts "actualDomain" and "actualRange" will be from the
			// NEW Concepts C1' to Cn', while "domain"
			// and "range" will be from the ORIGINAL Concepts C1 to Cn.

			String listOfColumns = ""; // to be returned
			this.setCondition("");
			this.setListOfColumnSets("");
			int index = 0;
			Concept actualDomain = this.getInputConcept();
			Concept actualRange;
			Columnset actualDomainCS, actualRangeCS, crossCS;
			Column domainKeyCol,
				rangeKeyCol,
				crossKeyCol_1,
				crossKeyCol_2,
				actualDomainKeyCol,
				actualRangeKeyCol;

			// add the selected features of the first concept to the generated Columnset:
			listOfColumns =
				addToListsAndCreateMetadata(
					actualDomain,
					csForOutputConcept,
					listOfColumns);

			// loop through the further concepts in the order that is given by the
			// relations, and add the other selected features to the generated Columnset:
			while (index < this.getMyRelations().length)
			{
				// get this relation's concepts:
				actualRange = this.getMyChainConcepts()[index];
				Relation currentRelation = this.getMyRelations()[index];
				crossCS = currentRelation.getCrossLinkColumnSet();
				if (currentRelation.isOneToManyRelation()) {
					// 1:n relation
					domainKeyCol = 
						this
							.getMyRelations()[index]
							.getFromKey()
							.getFirstColumnLink()
							.getForeignKeyColumn();
					rangeKeyCol = 
						this
							.getMyRelations()[index]
							.getFromKey()
							.getFirstColumnLink()
							.getPrimaryKeyColumn();
					actualDomainKeyCol =
						findColumnInConcept(domainKeyCol, actualDomain);
					actualRangeKeyCol =
						findColumnInConcept(rangeKeyCol, actualRange);
					actualDomainCS = actualDomain.getCurrentColumnSet();
					actualRangeCS = actualRange.getCurrentColumnSet();
					this.setCondition(
						this.getCondition()
							+ actualDomainCS.getSchema()
							+ "."
							+ actualDomainCS.getName()
							+ "."
							+ actualDomainKeyCol.getSQLDefinition()
							+ " = "
							+ actualRangeCS.getSchema()
							+ "."
							+ actualRangeCS.getName()
							+ "."
							+ actualRangeKeyCol.getSQLDefinition()
							+ " AND ");
				}
				else
				{
					// n:m relation
					if (this.getMyRelations()[index].getToKey() == null)
					{
						throw new M4CompilerError(
							"MRFC: Relation with Id "
								+ this.getMyRelations()[index].getId()
								+ " seems to be n:m, but the second key is missing!");
					}
					domainKeyCol = 
						this
							.getMyRelations()[index]
							.getFromKey()
							.getFirstColumnLink()
							.getPrimaryKeyColumn();
					crossKeyCol_1 = 
						this
							.getMyRelations()[index]
							.getFromKey()
							.getFirstColumnLink()
							.getForeignKeyColumn();
					crossKeyCol_2 = 
						this
							.getMyRelations()[index]
							.getToKey()
							.getFirstColumnLink()
							.getForeignKeyColumn();
					rangeKeyCol =
						this
							.getMyRelations()[index]
							.getToKey()
							.getFirstColumnLink()
							.getPrimaryKeyColumn();
					actualDomainKeyCol =
						findColumnInConcept(domainKeyCol, actualDomain);
					actualRangeKeyCol =
						findColumnInConcept(rangeKeyCol, actualRange);
					actualDomainCS = actualDomain.getCurrentColumnSet();
					actualRangeCS = actualRange.getCurrentColumnSet();
					this.setCondition(
						this.getCondition()
							+ actualDomainCS.getSchema()
							+ "."
							+ actualDomainCS.getName()
							+ "."
							+ actualDomainKeyCol.getSQLDefinition()
							+ " = "
							+ crossCS.getSchema()
							+ "."
							+ crossCS.getName()
							+ "."
							+ crossKeyCol_1.getSQLDefinition()
							+ " AND "
							+ actualRangeCS.getSchema()
							+ "."
							+ actualRangeCS.getName()
							+ "."
							+ actualRangeKeyCol.getSQLDefinition()
							+ " = "
							+ crossCS.getSchema()
							+ "."
							+ crossCS.getName()
							+ "."
							+ crossKeyCol_2.getSQLDefinition()
							+ " AND ");
					this.setListOfColumnSets(
						this.getListOfColumnSets()
							+ crossCS.getSchema()
							+ "."
							+ crossCS.getName()
							+ ", ");
				}

				listOfColumns =	addToListsAndCreateMetadata(actualRange, csForOutputConcept, listOfColumns);

				index++;
				actualDomain = actualRange;
			}
			// delete last "AND" and commas:
			this.setCondition(
				this.getCondition().substring(
					0,
					this.getCondition().length() - 5));
			listOfColumns =
				listOfColumns.substring(0, listOfColumns.length() - 2);
			this.setListOfColumnSets(
				this.getListOfColumnSets().substring(
					0,
					this.getListOfColumnSets().length() - 2));

			return listOfColumns;
		}
		catch (M4Exception m4e)
		{
			throw new M4CompilerError(
				"M4 interface error in "
					+ this.getName()
					+ ": "
					+ m4e.getMessage());
		}
    } // end private void generateColumns

    private String addToListsAndCreateMetadata(Concept currentConcept, Columnset csForOutputConcept, String listOfColumns) throws M4CompilerError
    {
    	try {
		// check if any selected features belong to the current concept:
		Vector selectedFeaturesOfCurrentConcept = new Vector();
		Collection concFeatures = currentConcept.getFeatures();
		Iterator it = concFeatures.iterator();
		Feature f;
		final Feature[] selectedFeat = this.getSelectedFeatures();
		while (it.hasNext()) {
			f = (Feature) it.next();
			for (int j = 0; j < selectedFeat.length; j++)
			{
				if (f.getId() == selectedFeat[j].getId()) {
					selectedFeaturesOfCurrentConcept.add(f);
				}
			}
		}

		// if no features must be selected from this concept, do nothing:
		if (selectedFeaturesOfCurrentConcept.isEmpty()) {
			return listOfColumns;
		}

		Feature oldF, newF;

		// the current concept's columnset must be included in the "FROM"-clause:
		this.setListOfColumnSets(
			this.getListOfColumnSets()
				+ currentConcept.getCurrentColumnSet().getSchema()
				+ "."
				+ currentConcept.getCurrentColumnSet().getName()
				+ ", ");

		// create new metadata for the selected features:
		for (int i = 0; i < selectedFeaturesOfCurrentConcept.size(); i++)
		{
			oldF = (Feature) selectedFeaturesOfCurrentConcept.get(i);
			newF = null;
			// find corresponding Feature in output concept:
			int k = 0;
			while ((k < getOutputConcept().getFeatures().size())
				&& (!((newF = getOutputConcept().getFeature(k))
					.correspondsTo(oldF))))
			{
				k++;
			}
			if (k == getOutputConcept().getFeatures().size())
			{
				throw new M4CompilerError(
					"MRFC: TheChainedFeature '"
						+ oldF.getName()
						+ "' not found in TheOutputConcept!");
			}

			if (this.isDeselectedParameter(oldF))
			{
				this.doPrint(
					Print.PARAM,
					"MRFC: TheChainedFeature '"
						+ oldF.getName()
						+ "' is skipped "
						+ "because it was deselected by a FeatureSelection operator.");
				continue;
			}
			// copy metadata for column of oldF to output columnset and newF
			listOfColumns = this.createMetadata(oldF, newF, csForOutputConcept, listOfColumns);
		} // end loop through the selected features
		return listOfColumns;
	}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in " + this.getName() + ": " + m4e.getMessage());  } 
    } // end private String addToListsAndCreateMetadata

    // returns the Column in the given concept that corresponds to the given
    // Column (of another concept), using the method "correspondsTo" in Feature.java
    private Column findColumnInConcept(Column keyCol, Concept toSearch) throws M4CompilerError
    {
    	try {
	        Collection fs = toSearch.getFeatures();
    	    Feature foundFeature = null;
        	Iterator it = fs.iterator();
	        Feature f;
	        while (it.hasNext())
	        {   f = (Feature) it.next();
	        	if (f.correspondsTo(keyCol.getTheBaseAttribute()))
	            {   foundFeature = f;   }
	        }
	        if (foundFeature == null)
	        {   throw new M4CompilerError("MRFC: Column '"+keyCol.getName()+"' (Id: "+keyCol.getId()+") was found in one of the relation keys, " +
	                                      "but could not be matched to the current Concept!");
	        }
	
	        BaseAttribute foundBA = (BaseAttribute) foundFeature; // must be castable
	        Column foundColumn = foundBA.getCurrentColumn();
	
    	    return foundColumn;
    	}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in " + this.getName() + ": " + m4e.getMessage());  } 
    } // end private Column findColumn

    // This method checks if the input parameters are ok, ie are a chain of
    // concepts and relations in a valid order. Throws exception if not.
    private void checkChainValidity() throws M4CompilerError
    {
        String info = "No valid chain of relations found among the input concepts! \n" +
                      "Parameter 'TheInputConcept' must be the first element of the chain. \n" +
                      "The following elements must be given as parameters 'TheConcepts' \n" +
                      "in the order of the chain. The relations between these concepts \n" +
                      "must be given as parameters 'TheRelations' in the same order. \n" +
                      "There must be as many relations as concepts, excluding 'TheInputConcept'.";

        // check number of relations and concepts:
        if (this.getMyRelations().length != this.getMyChainConcepts().length)
        {  throw new M4CompilerError(info);  }

        // can't make further checks here!
        /*
        int index = 0;
        Concept range, domain;

        while (index < myRelations.length)
        {
            range = myRelations[index].getToConcept();

            if (range.getId() != myChainConcepts[index].getId())
            {   throw new M4CompilerError(info);   }

            index++;

            if (index < myRelations.length)
            {   domain = myRelations[index].getFromConcept();
                if (range.getId() != domain.getId())
                {   throw new M4CompilerError(info);   }
            }
        }
        */
    } // end private void checkChainValidity

	/**
	 * MultiStepBranch information of all Concepts need to be merged.
	 * In order to reduce the risk of different permutations of entries, which
	 * might be problematic with the current implementation of the <code>Unsegment</code>
	 * operator, the MultiStepBranch-<code>String</code>s of all <code>Columnset</code>s
	 * are sorted before merging them. Multiple occurences of the same attribute are
	 * not supported; after the first occurence of an attribute each further occurence
	 * is removed.
	 * @see ConceptOperator#setNewCSMultiStepBranch(Columnset, int)
	 */
	protected void setNewCSMultiStepBranch(final Columnset newCS, int index)
		throws M4CompilerError
	{
		try {
			final Concept[] theConcepts = this.getMyChainConcepts();
			String[] msbArray = new String[theConcepts.length + 1];
			int i = 0;
			while (i<theConcepts.length) {
				String msb = theConcepts[i].getCurrentColumnSet().getMultiStepBranch();
				msbArray[i++] = nullToEmpty(msb);
			}
			msbArray[i] = nullToEmpty(this.getInputConcept().getCurrentColumnSet().getMultiStepBranch());
			Arrays.sort(msbArray);
			newCS.setMultiStepBranch("");
			for (i=0; i<msbArray.length; i++) {
				newCS.addMultiStepBranch(msbArray[i]);
			}
		}
   		catch (M4Exception m4e)
   		{   throw new M4CompilerError("M4 interface error in " + this.getName() + ": " + m4e.getMessage());  } 
	}

	private static String nullToEmpty(String s) {
		return (s == null) ? "" : s;
	}
}
/*
 * Historie
 * --------
 * 
 * $Log: MultiRelationalFeatureConstruction.java,v $
 * Revision 1.7  2006/04/11 14:10:12  euler
 * Updated license text.
 *
 * Revision 1.6  2006/04/06 16:31:11  euler
 * Prepended license remark.
 *
 * Revision 1.5  2006/03/31 15:22:20  euler
 * *** empty log message ***
 *
 * Revision 1.4  2006/03/30 16:07:13  scholz
 * fixed author tags for release
 *
 * Revision 1.3  2006/03/23 11:13:45  euler
 * Improved exception handling.
 *
 * Revision 1.2  2006/01/06 16:28:50  euler
 * Bugfixes
 *
 * Revision 1.1  2006/01/03 09:54:22  hakenjos
 * Initial version!
 *
 */
