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

import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.GeoTools;
import org.geotools.factory.Hints;
import org.geotools.feature.AttributeType;
import org.geotools.feature.Feature;
import org.geotools.feature.FeatureType;
import org.geotools.filter.IllegalFilterException;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.expression.Expression;

/**
 * Calculates the Average
 *
 * @author Cory Horner, Refractions
 *
 * @since 2.2.M2
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/main/src/main/java/org/geotools/feature/visitor/AverageVisitor.java $
 */
public class AverageVisitor implements FeatureCalc {
    private Expression expr;

    /**
     * This flag lets us know that an optimized result was stored and therefore
     * we don't have enough data to perform any merges.
     */
    private boolean isOptimized = false;
    AverageStrategy strategy;

    /**
     * Constructor class for the AverageVisitor using AttributeType ID
     *
     * @param attributeTypeIndex integer representing the AttributeType
     * @param type FeatureType
     *
     * @throws IllegalFilterException
     */
    public AverageVisitor(int attributeTypeIndex, FeatureType type)
        throws IllegalFilterException {
        FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
        AttributeType attributeType = type.getAttributeType(attributeTypeIndex);
        expr = factory.property(attributeType.getName());
        createStrategy(attributeType.getType());
    }

    /**
     * Constructor class for the AverageVisitor using AttributeType Name
     *
     * @param attrName string respresenting the AttributeType
     * @param type FeatureType
     *
     * @throws IllegalFilterException
     */
    public AverageVisitor(String attrName, FeatureType type)
        throws IllegalFilterException {
        FilterFactory factory = CommonFactoryFinder.getFilterFactory(null);
        AttributeType attributeType = type.getAttributeType(attrName);
        expr = factory.property(attributeType.getName());
        createStrategy(attributeType.getType());
    }

    /**
     * Constructor class for the AverageVisitor using an expression
     *
     * @param expr
     *
     * @throws IllegalFilterException
     */
    public AverageVisitor(Expression expr) throws IllegalFilterException {
        this.expr = expr;
    }

    /**
     * Factory method for creating the appropriate strategy object
     * 
     * <P></p>
     *
     * @param type the class to use for the calculation
     *
     * @return a strategy object of the correct data type to be used for
     *         calculating the average
     */
    private static AverageStrategy createStrategy(Class type) {
        if (type == Integer.class) {
            return new IntegerAverageStrategy();
        } else if (type == Double.class) {
            return new DoubleAverageStrategy();
        } else if (type == Long.class) {
            return new LongAverageStrategy();
        } else if (type == Float.class) {
            return new FloatAverageStrategy();
        }

        return null;
    }

    public void visit(Feature feature) {
        Object value = expr.evaluate(feature);

        if (strategy == null) {
            Class type = value.getClass();
            strategy = createStrategy(type);
        }

        strategy.add(value);
    }

    public Expression getExpression() {
        return expr;
    }

    /**
     * Returns the average from the visitor's current
     *
     * @return the average
     */
    public Object getAverage() {
        return strategy.getResult();
    }

    /**
     * Resets the "Average" strategy pattern
     */
    public void reset() {
        strategy = null;
        isOptimized = false;
    }

    /**
     * Returns a CalcResult object (containing the Average)
     *
     */
    public CalcResult getResult() {
        return new AverageResult(strategy, isOptimized);
    }

    public void setValue(Object newAverage) {
        reset();

        Class type = newAverage.getClass();
        strategy = createStrategy(type);
        strategy.add(newAverage);
        isOptimized = true;
    }

    public void setValue(int newCount, Object newSum) {
        reset();
        strategy = createStrategy(newSum.getClass());
        strategy.set(newCount, newSum);
        isOptimized = false; // this is an optimization, but we have the same

        // information we would otherwise have if we had
        // in fact visited each feature
    }

    /**
     * Encapsulates the strategy pattern for the "Average" Visitor
     */
    interface AverageStrategy {
        public void add(Object value);

        public Object getResult();

        public Object getSum();

        public int getCount();

        public void set(int count, Object sum);
    }

    /**
     * Implements the average calculation for values of the type double
     */
    static class DoubleAverageStrategy implements AverageStrategy {
        double number = 0;
        int count = 0;

        public void add(Object value) {
            number += ((Number) value).doubleValue();
            count++;
        }

        public Object getResult() {
            return new Double(number / count);
        }

        public Object getSum() {
            return new Double(number);
        }

        public int getCount() {
            return count;
        }

        public void set(int newCount, Object sum) {
            number = ((Number) sum).doubleValue();
            count = newCount;
        }
    }

    /**
     * Implements the average calculation for values of the type float
     */
    static class FloatAverageStrategy implements AverageStrategy {
        float number = 0;
        int count = 0;

        public void add(Object value) {
            number += ((Number) value).floatValue();
            count++;
        }

        public Object getResult() {
            return new Float((float) number / count);
        }

        public Object getSum() {
            return new Float(number);
        }

        public int getCount() {
            return count;
        }

        public void set(int newCount, Object sum) {
            number = ((Number) sum).floatValue();
            count = newCount;
        }
    }

    /**
     * Implements the average calculation for values of the type long
     */
    static class LongAverageStrategy implements AverageStrategy {
        long number = 0;
        int count = 0;

        public void add(Object value) {
            number += ((Number) value).longValue();
            count++;
        }

        public Object getResult() {
            return new Double((double) number / count);
        }

        public Object getSum() {
            return new Long(number);
        }

        public int getCount() {
            return count;
        }

        public void set(int newCount, Object sum) {
            number = ((Number) sum).longValue();
            count = newCount;
        }
    }

    /**
     * Implements the average calculation for values of the type integer
     */
    static class IntegerAverageStrategy implements AverageStrategy {
        int number = 0;
        int count = 0;

        public void add(Object value) {
            number += ((Number) value).intValue();
            count++;
        }

        public Object getResult() {
            return new Double((double) number / count);
        }

        public Object getSum() {
            return new Integer(number);
        }

        public int getCount() {
            return count;
        }

        public void set(int newCount, Object sum) {
            number = ((Number) sum).intValue();
            count = newCount;
        }
    }

    /**
     *
     */
    public static class AverageResult extends AbstractCalcResult {
        private AverageStrategy averageStrategy;
        private boolean isOptimized = false;

        public AverageResult(Object newAverageStrategy) {
            averageStrategy = (AverageStrategy) newAverageStrategy;
        }

        public AverageResult(Object newAverageStrategy, boolean isOptimized) {
            averageStrategy = (AverageStrategy) newAverageStrategy;
            this.isOptimized = isOptimized;
        }

        public AverageResult(int newCount, Object newSum) {
            averageStrategy = createStrategy(newSum.getClass());
            averageStrategy.set(newCount, newSum);
        }

        public Object getResult() {
            return averageStrategy.getResult();
        }

        public Object getValue() {
            return averageStrategy.getResult();
        }

        /**
         * The count used to calculate the average
         *
         * @return the count, or -1 if unknown
         */
        public int getCount() {
            if (isOptimized) {
                return -1; //there is no count, as an optimization was used.
            } else {
                return averageStrategy.getCount();
            }
        }

        /**
         * The sum used to calculate the average
         *
         */
        public Object getSum() {
            if (isOptimized) {
                return null;
            } else {
                return averageStrategy.getSum();
            }
        }

        /**
         * Determines if the target CalcResult object can be merged with this
         * CalcResult object
         *
         * @param targetResults a second CalcResult object (target)
         *
         * @return boolean
         */
        public boolean isCompatible(CalcResult targetResults) {
            if (targetResults instanceof AverageResult) {
                return true;
            } else {
                return false;
            }
        }

        /**
         * Merges the contents of a CalcResult Object with another CalcResult
         * Object.   If the two CalcResult objects are compatible, the merged
         * result (a new object) is returned.
         *
         * @param resultsToAdd the CalcResult to merge the results with
         *
         * @return a new merged CalcResult object
         *
         * @throws IllegalArgumentException when the resultsToAdd are
         *         inappropriate
         */
        public CalcResult merge(CalcResult resultsToAdd) {
            if (!isCompatible(resultsToAdd)) {
                throw new IllegalArgumentException(
                    "Parameter is not a compatible type");
            }

            if (resultsToAdd instanceof AverageResult) {
                AverageResult moreResults = (AverageResult) resultsToAdd;

                //ensure both results are NOT optimized
                if (isOptimized || moreResults.isOptimized) {
                    throw new IllegalArgumentException(
                        "Optimized average results cannot be merged.");
                }

                Number[] sums = new Number[] {
                        (Number) averageStrategy.getSum(),
                        (Number) moreResults.averageStrategy.getSum()
                    };
                Number newSum = CalcUtil.sum(sums);
                Number newCount = (Number) new Integer(averageStrategy.getCount()
                        + moreResults.averageStrategy.getCount());
                Number[] params = new Number[] { newSum, newCount };
                Object newAverage = CalcUtil.getObject(params);
                AverageStrategy newAverageObj;
                newAverageObj = createStrategy(newAverage.getClass());
                newAverageObj.set(newCount.intValue(), newSum);

                return new AverageResult(newAverageObj);
            } else {
                throw new IllegalArgumentException(
                    "The CalcResults claim to be compatible, but the appropriate merge method has not been implemented.");
            }
        }
    }
}
