/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2005-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.
 *
 */
package org.geotools.filter.function;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;

import org.geotools.feature.FeatureCollection;
import org.geotools.feature.visitor.MaxVisitor;
import org.geotools.feature.visitor.MinVisitor;
import org.geotools.feature.visitor.UniqueVisitor;
import org.geotools.filter.IllegalFilterException;
import org.geotools.util.NullProgressListener;


/**
 * Classification function for breaking a feature collection into edible chunks
 * of "equal" size.
 * 
 * @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/EqualIntervalFunction.java $
 */
public class EqualIntervalFunction extends ClassificationFunction {
	
    public EqualIntervalFunction() {
        setName("EqualInterval");
    }

    private Object calculate(FeatureCollection featureCollection) {
        int classNum = getClasses();
        Comparable globalMin;
        Comparable globalMax;
		try {
            MinVisitor minVisit = new MinVisitor(getExpression());
			if (progress == null) progress = new NullProgressListener();
			featureCollection.accepts(minVisit, progress);
			if (progress.isCanceled()) return null;
			globalMin = (Comparable) minVisit.getResult().getValue();

			MaxVisitor maxVisit = new MaxVisitor(getExpression());
			featureCollection.accepts(maxVisit, progress);
			if (progress.isCanceled()) return null;
			globalMax = (Comparable) maxVisit.getResult().getValue();
			
			if ((globalMin instanceof Number) && (globalMax instanceof Number)) {
                return calculateNumerical(classNum, globalMin, globalMax);
            } else {
                return calculateNonNumerical(classNum, featureCollection);
			}
        } catch (IllegalFilterException e) { // accepts exploded
            LOGGER.log(Level.SEVERE, "EqualIntervalFunction calculate(FeatureCollection) failed", e);
            return null;
        } catch (IOException e) { // getResult().getValue() exploded
            LOGGER.log(Level.SEVERE, "EqualIntervalFunction calculate(FeatureCollection) failed", e);
            return null;
        }

    }
    
    private Object calculateNumerical(int classNum, Comparable globalMin, Comparable globalMax) {
        double slotWidth = (((Number) globalMax).doubleValue() - ((Number) globalMin).doubleValue()) / classNum;
        //size arrays
        Comparable[] localMin = new Comparable[classNum];
        Comparable[] localMax = new Comparable[classNum];
        for (int i = 0; i < classNum; i++) {
            //calculate the min + max values
            localMin[i] = new Double(((Number) globalMin).doubleValue() + (i * slotWidth));
            localMax[i] = new Double(((Number) globalMax).doubleValue() - ((classNum - i - 1) * slotWidth));
            //determine number of decimal places to allow
            int decPlaces = decimalPlaces(slotWidth);
            //clean up truncation error
            if (decPlaces > -1) {
                localMin[i] = new Double(round(((Number) localMin[i]).doubleValue(), decPlaces));
                localMax[i] = new Double(round(((Number) localMax[i]).doubleValue(), decPlaces));
            }
            
            if (i == 0) {
                //ensure first min is less than or equal to globalMin
                if (localMin[i].compareTo(new Double(((Number) globalMin).doubleValue())) < 0)
                    localMin[i] = new Double(fixRound(((Number) localMin[i]).doubleValue(), decPlaces, false));
            } else if (i == classNum - 1) { 
                //ensure last max is greater than or equal to globalMax
                if (localMax[i].compareTo(new Double(((Number) globalMax).doubleValue())) > 0)
                    localMax[i] = new Double(fixRound(((Number) localMax[i]).doubleValue(), decPlaces, true));
            }
            //synchronize min with previous max
            if ((i != 0) && (!localMin[i].equals(localMax[i-1]))) {
                localMin[i] = localMax[i-1];
            }
        }
        return new RangedClassifier(localMin, localMax);
    }
    
    private Object calculateNonNumerical(int classNum, FeatureCollection featureCollection) throws IOException {
        //obtain of list of unique values, so we can enumerate
        UniqueVisitor uniqueVisit = new UniqueVisitor(getExpression());
        featureCollection.accepts(uniqueVisit, null);
        List result = uniqueVisit.getResult().toList();
        //sort the results and put them in an array
        Collections.sort(result);
        
        Comparable[] values = (Comparable[]) result.toArray( new Comparable[ result.size()]);
        
        //size arrays
        Comparable[] localMin = new Comparable[classNum];
        Comparable[] localMax = new Comparable[classNum];

        //we have 2 options here:
        //1. break apart by numeric value: (aaa, aab, aac, bbb) --> [aaa, aab, aac], [bbb]
        //2. break apart by item count:                         --> [aaa, aab], [aac, bbb]

        // this code currently implements option #2 (this is a quantile, why don't we use their code instead)
        
        //calculate number of items to put in each of the larger bins
        int binPop = new Double(Math.ceil((double) values.length / classNum)).intValue();
        //determine index of bin where the next bin has one less item
        int lastBigBin = values.length % classNum;
        if (lastBigBin == 0) lastBigBin = classNum;
        else lastBigBin--;

        int itemIndex = 0;
        //for each bin
        for (int binIndex = 0; binIndex < classNum; binIndex++) {
            //store min
            localMin[binIndex] = values[itemIndex];
            itemIndex+=binPop;
            //store max
            if (binIndex == classNum - 1) {
                localMax[binIndex] = values[itemIndex];                
            } else {
                localMax[binIndex] = values[itemIndex+1];                
            }
            if (lastBigBin == binIndex)
                binPop--; // decrease the number of items in a bin for the
                            // next iteration
        }
        return new RangedClassifier(localMin, localMax);
    }

    public Object evaluate( Object object ) {
        if (!(object instanceof FeatureCollection)) {
            return null;
        }
        return calculate((FeatureCollection) object);
    }

    public int getArgCount() {
        return 2;
    }
    
}
