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

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

import edu.udo.cs.miningmart.exception.M4Exception;

/**
 * @author Timm Euler
 * @see edu.udo.cs.miningmart.m4.EstimatedStatistics
 */
public class EstimatedStatistics implements edu.udo.cs.miningmart.m4.EstimatedStatistics {

	// the estimated values
	private int numberOfRows;
	private Map attributesToInfos; // a Map from attribute names to AttributeInfo objects
	
	/**
	 * This constructor creates an EstimatedStatistics
	 * object for the given Concept. If the concept is of type DB then
	 * its actual statistics are computed and copied to the estimated values.
	 * Otherwise all estimated values are set to be unknown. This means that
	 * any inference or estimation has to be done elsewhere.
	 */
	public EstimatedStatistics(edu.udo.cs.miningmart.m4.Concept theConcept) throws M4Exception {
		this(); // sets values to "unknown"
		
		if (theConcept == null) {
			throw new M4Exception("Constructor of EstimatedStatistics: got NULL concept!");
		}	

		this.initiateValueLists(theConcept); // prepares the map		
		
		// maybe some values are known:
		if (theConcept.getType().equals(Concept.TYPE_DB)) {
			Columnset theCs = (Columnset) theConcept.getCurrentColumnSet();
			if (theCs != null) {
				theCs.updateStatistics(); // updates only where necessary (one hopes)
				this.numberOfRows = theCs.getStatisticsAll();
				Iterator it = theCs.getColumns().iterator();
				while (it.hasNext()) {
					Column myCol = (Column) it.next();
					BaseAttribute myBa = (BaseAttribute) myCol.getTheBaseAttribute();
					// some columns may not be connected to BAs or the BAs may not belong to the given concept (?)
					if (myBa != null && theConcept.hasFeature(myBa)) { 
                        AttributeInfo theAttrInfo = (AttributeInfo) this.attributesToInfos.get(myBa.getName());
                        if (theAttrInfo == null) {
                        	throw new M4Exception("EstimatedStatistics, constructor: found unknown (?) BaseAttribute '" + myBa.getName() + "'!");
                        }
                        Collection colStats1 = myCol.getBasicColStats();
                        if (colStats1 != null && ( ! colStats1.isEmpty())) {
                        	Iterator csIt = colStats1.iterator();
                        	boolean moreThanOne = false;						
                        	while (csIt.hasNext()) {
                        		if (moreThanOne) {
                        			throw new M4Exception("EstimatedStatistics, constructor: found more than one ColStatist1 object for column '" + myCol.getName() + "'!");
                        		}
                        		moreThanOne = true;
                        		ColumnStatistics1 colstat1 = (ColumnStatistics1) csIt.next();
                        		Integer noOfMiss = colstat1.getNrOfMissingValuesI();
                        		if (noOfMiss != null) {
                        			theAttrInfo.setNumberOfMissingValues(noOfMiss.intValue());
                        		}
                        		String max = colstat1.getMaximum();
                        		if (max != null) {
                        			theAttrInfo.setMaximum(Double.parseDouble(max));
                        		}
                        		String min = colstat1.getMinimum();
                        		if (min != null) {
                        			theAttrInfo.setMinimum(Double.parseDouble(min));
                        		}							
                        	}
                        }
                        Vector someValues = theAttrInfo.getValues();
                        Collection colStats2 = myCol.getDistributionStatistics();
                        if (colStats2 != null && ( ! colStats2.isEmpty())) {
                        	Iterator csIt = colStats2.iterator();
                        	while (csIt.hasNext()) {
                        		ValueInfo valInfo = new ValueInfo();
                        		someValues.add(valInfo);
                        		ColumnStatistics2 colstat2 = (ColumnStatistics2) csIt.next();
                        		String value = colstat2.getDistributionValue();
                        		if (value != null) {
                        			valInfo.setTheValue(value);
                        		}
                        		int count = colstat2.getDistributionCount();
                        		if ((count != VALUE_INT_UNKNOWN) && (count > 0)) {
                        			valInfo.setNumberOfOccurrences(count);
                        		}
                        		if (count == 0) {
								// 	only for debugging:
                        			System.out.println("Warning: value '" + value + "' in column '" + 
                        					myCol.getName() +
											"' occurs 0 times, it's not used in estimated statistics!");
								// 	removing this information:
                        			someValues.remove(valInfo);
                        		}
                        	}
						}
					}
				}
			}
		}
	}
	
	private EstimatedStatistics() {
		this.numberOfRows = VALUE_INT_UNKNOWN;
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#getValueList(String)
	 */
	public Vector getValueList(String nameOfAttribute) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		Vector theValues = new Vector();
		Vector valInfos = ai.getValues();
		Iterator it = valInfos.iterator();
		while (it.hasNext()) {
			ValueInfo vi = (ValueInfo) it.next();
			theValues.add(vi.getTheValue());
		}
		return theValues;
	}
	
	public void setValueList(String nameOfAttribute, Vector theValues) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		if (theValues != null) {
			Vector theValueInfos = new Vector();
			Iterator it = theValues.iterator();
			while (it.hasNext()) {
				String myValue = (String) it.next();
				ValueInfo vi = new ValueInfo();
				vi.setTheValue(myValue);
				theValueInfos.add(vi);
			}
			ai.setValues(theValueInfos);
		}
	}

	private AttributeInfo getAttribInfo(String nameOfAttribute) {
		return (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
	}
	
	public void copyValueList( String nameOfDestinationAttribute,
			                   edu.udo.cs.miningmart.m4.EstimatedStatistics from,
							   String nameOfSourceAttribute) {
		AttributeInfo toAi = (AttributeInfo) this.attributesToInfos.get(nameOfDestinationAttribute);
		EstimatedStatistics coreFrom = (EstimatedStatistics) from;
		AttributeInfo fromAi = coreFrom.getAttribInfo(nameOfSourceAttribute);
		Iterator it = fromAi.getValues().iterator();
		Vector copyOfList = new Vector();
		while (it.hasNext()) {
			ValueInfo vi = (ValueInfo) it.next();
			copyOfList.add(vi.copy());
		}
		toAi.setValues(copyOfList);
	}
	
	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#removeValue(String, String)
	 */
	public void removeValue(String value, String nameOfAttribute) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		Vector valInfos = ai.getValues();
		ValueInfo toRemove = null;
		Iterator it = valInfos.iterator();
		while (it.hasNext()) {
			ValueInfo vi = (ValueInfo) it.next();
			if (vi.getTheValue().equals(value)) {
				toRemove = vi;
			}
		}
		if (toRemove != null) {
			ai.getValues().remove(toRemove);
		}
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#getNumberOfMissingValues(String)
	 */
	public int getNumberOfMissingValues(String nameOfAttribute) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		return ai.getNumberOfMissingValues();
	}

	/**
	 * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#setNumberOfMissingValues(String, int)
	 */
	public void setNumberOfMissingValues(String nameOfAttribute, int number) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		ai.setNumberOfMissingValues(number);
	}
	
	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#getBiggestValue(String)
	 */
	public double getBiggestValue(String nameOfAttribute) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		return ai.getMaximum();
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#setBiggestValue(String, double)
	 */
	public void setBiggestValue(String nameOfAttribute, double value) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		ai.setMaximum(value);
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#setLowestValue(String, double)
	 */
	public void setLowestValue(String nameOfAttribute, double value) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		ai.setMinimum(value);
	}
	
	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#getLowestValue(String)
	 */
	public double getLowestValue(String nameOfAttribute) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		return ai.getMinimum();
	}
	
	private void initiateValueLists(edu.udo.cs.miningmart.m4.Concept theConcept) throws M4Exception {
		this.attributesToInfos = new HashMap();
		Collection bas = theConcept.getAllBaseAttributes();
		if (bas != null && ( ! bas.isEmpty())) {
			Iterator it = bas.iterator();
			while (it.hasNext()) {
				BaseAttribute ba = (BaseAttribute) it.next();
				this.attributesToInfos.put(ba.getName(), new AttributeInfo());
			}
		}
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#getNumberOfRows()
	 */
	public int getNumberOfRows() {
		return numberOfRows;
	}

	/**
     * @see edu.udo.cs.miningmart.m4.EstimatedStatistics#setNumberOfRows(int)
	 */
	public void setNumberOfRows(int numberOfRows) {
		this.numberOfRows = numberOfRows;
	}
	
	public void setNumberOfOccurrences(String nameOfAttribute, String value, int number) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		Vector valInfos = ai.getValues();
		ValueInfo toRemove = null;
		Iterator it = valInfos.iterator();
		while (it.hasNext()) {
			ValueInfo vi = (ValueInfo) it.next();
			if (vi.getTheValue().equals(value)) {
				if (number == 0) {
					toRemove = vi;
				}
				else {
					vi.setNumberOfOccurrences(number);
				}
			}
		}
		if (toRemove != null) {
			ai.getValues().remove(toRemove);
		}
	}
	
	public int getNumberOfOccurrences(String nameOfAttribute, String value) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		Vector valInfos = ai.getValues();
		Iterator it = valInfos.iterator();
		while (it.hasNext()) {
			ValueInfo vi = (ValueInfo) it.next();
			if (vi.getTheValue().equals(value)) {
				return vi.getNumberOfOccurrences();
			}
		}
		return VALUE_INT_UNKNOWN;
	}
	
	public edu.udo.cs.miningmart.m4.EstimatedStatistics copy() {
		EstimatedStatistics copy = new EstimatedStatistics();
		copy.setNumberOfRows(this.getNumberOfRows());
		Iterator entryIt = this.attributesToInfos.entrySet().iterator();
		while (entryIt.hasNext()) {
			Map.Entry myEntry = (Map.Entry) entryIt.next();
			String nameOfAttrib = (String) myEntry.getKey();
			AttributeInfo ai = (AttributeInfo) myEntry.getValue();
			copy.addAttribInfo(nameOfAttrib, ai.copy());
		}		
		return copy;
	}

	public void addValueInformation(String nameOfAttribute, String value, int noOfOccurrences) {
		AttributeInfo ai = (AttributeInfo) this.attributesToInfos.get(nameOfAttribute);
		if (ai != null) {
			ai.addValueInfo(value, noOfOccurrences);
		}
	}
	
	private void addAttribInfo(String nameOfAttribute, AttributeInfo ai) {
		if (this.attributesToInfos == null) {
			this.attributesToInfos = new HashMap();
		}
		if (ai != null && nameOfAttribute != null) {
			this.attributesToInfos.put(nameOfAttribute, ai);
		}
	}
	
	public void addAttribute(String nameOfAttribute) {
		if (this.attributesToInfos == null) {
			this.attributesToInfos = new HashMap();
		}
		this.attributesToInfos.put(nameOfAttribute, new AttributeInfo());
	}
	
	/* Objects of this class store information about a single attribute.
	 */ 
	private class AttributeInfo {
		private double minimum;
		private double maximum;
		private int numberOfMissingValues;
		private Vector values;
		
		public AttributeInfo() {
			this.maximum = VALUE_DOUBLE_UNKNOWN;
			this.minimum = VALUE_DOUBLE_UNKNOWN;
			this.numberOfMissingValues = VALUE_INT_UNKNOWN;
			this.values = new Vector();
		}

		public double getMaximum() {
			return maximum;
		}
		public void setMaximum(double maximum) {
			this.maximum = maximum;
		}
		public double getMinimum() {
			return minimum;
		}
		public void setMinimum(double minimum) {
			this.minimum = minimum;
		}
		public int getNumberOfMissingValues() {
			return numberOfMissingValues;
		}
		public void setNumberOfMissingValues(int numberOfMissingValues) {
			this.numberOfMissingValues = numberOfMissingValues;
		}
		public Vector getValues() {
			return values;
		}
		public void setValues(Vector values) {
			this.values = values;
		}
		public void setValueInfo(String value, int noOfOccurrences) {
			Iterator it = this.values.iterator();
			ValueInfo toRemove = null;
			while (it.hasNext()) {
				ValueInfo vi = (ValueInfo) it.next();
				if (vi.getTheValue().equals(value)) {
					if (noOfOccurrences > 0) {
						vi.setNumberOfOccurrences(noOfOccurrences);
					}
					else if (noOfOccurrences == 0) {
						toRemove = vi;
					}
				}
			}
			if (toRemove != null) {
				this.values.remove(toRemove);
			}
		}
		public void addValueInfo(String value, int noOfOccurrences) {
			// first check if this value is not yet present:
			Iterator it = this.values.iterator();
			while (it.hasNext()) {
				ValueInfo vi = (ValueInfo) it.next();
				if (vi.getTheValue().equals(value)) {
					return;
				}
			}
			ValueInfo vi = new ValueInfo();
			vi.setTheValue(value);
			vi.setNumberOfOccurrences(noOfOccurrences);
			this.values.add(vi);
		}
		public AttributeInfo copy() {
			AttributeInfo copy = new AttributeInfo();
			copy.setMaximum(this.getMaximum());
			copy.setMinimum(this.getMinimum());
			copy.setNumberOfMissingValues(this.getNumberOfMissingValues());
			Vector copyOfValues = new Vector();
			Iterator it = this.getValues().iterator();
			while (it.hasNext()) {
				ValueInfo vi = (ValueInfo) it.next();
				ValueInfo copyVi = vi.copy();
				copyOfValues.add(copyVi);
			}
			copy.setValues(copyOfValues);
			return copy;
		}
	}
	
	/* Objects of this class store information about a single value of
	 * a single attribute.
	 */ 
	private class ValueInfo {

		private String theValue;
		private int numberOfOccurrences;
		
		public ValueInfo() {
			this.theValue = VALUE_DISCRETE_UNKNOWN;
			this.numberOfOccurrences = VALUE_INT_UNKNOWN;
		}
		
		public int getNumberOfOccurrences() {
			return numberOfOccurrences;
		}
		public void setNumberOfOccurrences(int numberOfOccurrences) {
			this.numberOfOccurrences = numberOfOccurrences;
		}
		public String getTheValue() {
			return theValue;
		}
		public void setTheValue(String theValue) {
			// do some cleaning:
			theValue = theValue.trim();
			try {
				Double.parseDouble(theValue);
				// if this works, see if too many 0s are there:
				while (theValue.indexOf(".") > -1 && theValue.endsWith("0")) {
					theValue = theValue.substring(0, theValue.length()-1);
				}
				if (theValue.endsWith(".")) {
					theValue = theValue.substring(0, theValue.length()-1);
				}
			}
			catch (NumberFormatException e) {
				// not a numeric value, ok
			}
			this.theValue = theValue;
		}
		public ValueInfo copy() {
			ValueInfo copy = new ValueInfo();
			copy.setNumberOfOccurrences(this.getNumberOfOccurrences());
			copy.setTheValue(this.getTheValue());
			return copy;
		}
	}
}
