/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2004-2006, GeoTools Project Managment Committee (PMC)
 *    
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library 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
 *    Lesser General Public License for more details.
 *
 *    reated on October 27, 2004, 11:27 AM
 */
package org.geotools.filter.function;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.geotools.feature.Feature;
import org.geotools.filter.DefaultExpression;
import org.geotools.filter.Expression;
import org.geotools.filter.ExpressionType;
import org.geotools.filter.FilterFactory;
import org.geotools.filter.FilterFactoryFinder;
import org.geotools.filter.FunctionExpression;
import org.geotools.filter.LiteralExpression;
import org.geotools.util.ProgressListener;
import org.opengis.filter.expression.ExpressionVisitor;

/**
 * Parent for classifiers which break a feature collection into the specified number of classes.
 * 
 * @author James Macgill
 * @author Cory Horner, Refractions Research Inc.
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/filter/function/ClassificationFunction.java $
 */
public abstract class ClassificationFunction extends DefaultExpression implements FunctionExpression {

    protected static final java.util.logging.Logger LOGGER = org.geotools.util.logging.Logging.getLogger("org.geotools.filter.function");

    /** function name **/
    String name;
    
    /** function params **/
    List params = new ArrayList(2);
    
    ProgressListener progress;
    
    /** Creates a new instance of ClassificationFunction.  Subclasses should call setName */
    public ClassificationFunction() {
        setName("ClassificationFunction");
        params.add(0, null);
        params.add(1, null);
        this.expressionType = ExpressionType.FUNCTION;
    }
    
    public abstract int getArgCount();
    
    /**
     * @see org.opengis.filter.expression.Expression#accept(ExpressionVisitor, Object)
     */
    public Object accept(ExpressionVisitor visitor, Object extraData) {
        return visitor.visit(this, extraData);
    }
    
    //overriding evaluate(feature) to point at evaluate(object)
    public Object evaluate(Feature feature) {
        return evaluate((Object) feature);
    }

    public abstract Object evaluate(Object arg);
    
    /**
     * @deprecated please use getParameters
     */
    public Expression[] getArgs() {
        List parameters = getParameters();
        return (Expression[]) parameters.toArray(new Expression[parameters.size()]);
    }
    
    /**
     * @deprecated please use setParameters
     */
    public void setArgs(Expression[] args) {
        List parameters = new ArrayList();
        for (int i = 0; i < args.length; i++) {
            parameters.add(i, args[i]);
        }
        setParameters(parameters);
    }
    
    /**
     * Gets the name of this function.
     *
     * @return the name of the function.
     * 
     */
    public String getName() {
        return name;
    }

    /**
     * Sets the name of the function.
     */
    public void setName(String name) {
        this.name = name;
    }
    
    /**
     * Returns the function parameters (the contents are Expressions, usually attribute expression and literal expression).
     */
    public List getParameters() {
        return params;
    }
    
    /**
     * Sets the function parameters.
     */
    public void setParameters(List params) {
        this.params = params;
    }
    
    public ProgressListener getProgressListener() {
        return progress;
    }
    
    public void setProgressListener(ProgressListener progress) {
        this.progress = progress;
    }
    
    /**
     * @deprecated use getClasses()
     */
    public int getNumberOfClasses() {
        return getClasses();
    }
    
    public int getClasses() {
        LiteralExpression classes = (LiteralExpression) getParameters().get(1);
        return ((Integer) classes.evaluate(null, Integer.class)).intValue();
    }

    /**
     * @deprecated use setClasses()
     */
    public void setNumberOfClasses(int classes) {
        setClasses(classes);
    }

    public void setClasses(int classes) {
        FilterFactory ff = FilterFactoryFinder.createFilterFactory();
        LiteralExpression expression = ff.createLiteralExpression(classes);
        getParameters().set(1, expression);
    }
    
    public Expression getExpression() {
        return (Expression) getParameters().get(0);
    }
    
    public void setExpression(Expression e) {
        getParameters().set(0, e);
    }
    
    /**
     * Returns the implementation hints. The default implementation returns an empty map.
     */
    public Map getImplementationHints() {
        return Collections.EMPTY_MAP;
    }
    
    /**
     * Determines the number of decimal places to truncate the interval at.
     * 
     * @param slotWidth
     */
    protected int decimalPlaces(double slotWidth) {
        String str = Double.toString(slotWidth);
        if (str.indexOf(".") > -1) {
            while (str.endsWith("0")) {
                str = str.substring(0, str.length() - 1);
            }
        }
        int intPart = new Double(Math.floor(slotWidth)).intValue();
        double decPart = slotWidth - intPart;
        int intPoints = Integer.toString(intPart).length();
        int decPoints = str.length() - intPoints;
        if (str.indexOf(".") > -1) {
            decPoints--;
        }
        if (decPart == 0) {
            decPoints = 0;
        }
        //if there are dec points, show at least one
        if (intPart == 0) { //if int part is 0, be very specific
            if (decPoints > 6) {
                return 5;
            } else if (decPoints > 0) {
                return decPoints;
            } else {
                return 1;
            }
        } else if (decPoints == 0) { //if there are no dec points, don't show any
            return 0;
        } else { //aim for a number of digits (not including '.') up to a reasonable limit (5)
            int chars = intPoints + decPoints;
            if (chars < 6) {
                return decPoints;
            } else if (intPoints > 4) {
                return 1;
            } else {
                return 5 - intPoints;
            }
        }
    }
    
    /**
     * Truncates a double to a certain number of decimals places. Note:
     * truncation at zero decimal places will still show up as x.0, since we're
     * using the double type.
     * 
     * @param value
     *            number to round-off
     * @param decimalPlaces
     *            number of decimal places to leave
     * @return the rounded value
     */
    protected double round(double value, int decimalPlaces) {
    	double divisor = Math.pow(10, decimalPlaces);
    	double newVal = value * divisor;
    	newVal =  (new Long(Math.round(newVal)).intValue())/divisor; 
    	return newVal;
    }
    
    /**
     * Corrects a round off operation by incrementing or decrementing the
     * decimal place (preferably the smallest one). This should usually be used
     * to adjust the bounds to include a value. Example: 0.31-->0.44 where 0.44
     * is the maximum value and end of the range. We could just make the ,
     * round(0.31, 1)=0.3; round(0.44 max value = 0.49
     * 
     * @param value
     * @param decimalPlaces
     * @param up
     */
    protected double fixRound(double value, int decimalPlaces, boolean up) {
    	double divisor = Math.pow(10, decimalPlaces);
    	double newVal = value * divisor;
    	if (up) newVal++; //+0.001 (for 3 dec places)
    	else newVal--; //-0.001
    	newVal =  newVal/divisor; 
    	return newVal;
    }

}
