/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.apache.jorphan.math;

import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;

import org.apache.commons.lang.mutable.MutableLong;

/**
 * This class serves as a way to calculate the median, max, min etc. of a list of values.
 * It is not threadsafe.
 *
 */
public abstract class StatCalculator<T extends Number & Comparable<? super T>> {

    // key is the type to collect (usually long), value = count of entries
    private final TreeMap<T, MutableLong> valuesMap = new TreeMap<T, MutableLong>();
    // We use a TreeMap because we need the entries to be sorted

    // Running values, updated for each sample
    private double sum = 0;

    private double sumOfSquares = 0;

    private double mean = 0;

    private double deviation = 0;

    private int count = 0;

    private T min;

    private T max;

    private long bytes = 0;

    private final T ZERO;

    private final T MAX_VALUE; // e.g. Long.MAX_VALUE

    private final T MIN_VALUE; // e.g. Long.MIN_VALUE

    /**
     * This constructor is used to set up particular values for the generic class instance.
     *
     * @param zero - value to return for Median and PercentPoint if there are no values
     * @param min - value to return for minimum if there are no values
     * @param max - value to return for maximum if there are no values
     */
    public StatCalculator(final T zero, final T min, final T max) {
        super();
        ZERO = zero;
        MAX_VALUE = max;
        MIN_VALUE = min;
        this.min = MAX_VALUE;
        this.max = MIN_VALUE;
    }

    public void clear() {
        valuesMap.clear();
        sum = 0;
        sumOfSquares = 0;
        mean = 0;
        deviation = 0;
        count = 0;
        bytes = 0;
        max = MIN_VALUE;
        min = MAX_VALUE;
    }


    public void addBytes(long newValue) {
        bytes += newValue;
    }

    public void addAll(StatCalculator<T> calc) {
        for (T val : calc.valuesMap.keySet()) {
            addValue(val);
        }
    }

    public T getMedian() {
        return getPercentPoint(0.5);
    }

    public long getTotalBytes() {
        return bytes;
    }

    /**
     * Get the value which %percent% of the values are less than. This works
     * just like median (where median represents the 50% point). A typical
     * desire is to see the 90% point - the value that 90% of the data points
     * are below, the remaining 10% are above.
     *
     * @param percent
     * @return number of values less than the percentage
     */
    public T getPercentPoint(float percent) {
        return getPercentPoint((double) percent);
    }

    /**
     * Get the value which %percent% of the values are less than. This works
     * just like median (where median represents the 50% point). A typical
     * desire is to see the 90% point - the value that 90% of the data points
     * are below, the remaining 10% are above.
     *
     * @param percent
     * @return the value which %percent% of the values are less than
     */
    public T getPercentPoint(double percent) {
        if (count <= 0) {
                return ZERO;
        }
        if (percent >= 1.0) {
            return getMax();
        }

        // use Math.round () instead of simple (long) to provide correct value rounding
        long target = Math.round (count * percent);
        try {
            for (Entry<T, MutableLong> val : valuesMap.entrySet()) {
                target -= val.getValue().longValue();
                if (target <= 0){
                    return val.getKey();
                }
            }
        } catch (ConcurrentModificationException ignored) {
            // ignored. May happen occasionally, but no harm done if so.
        }
        return ZERO; // TODO should this be getMin()?
    }

    /**
     * Returns the distribution of the values in the list.
     *
     * @return map containing either Integer or Long keys; entries are a Number array containing the key and the [Integer] count.
     * TODO - why is the key value also stored in the entry array?
     */
    public synchronized Map<Number, Number[]> getDistribution() {
        HashMap<Number, Number[]> items = new HashMap <Number, Number[]> ();
        Number[] dis;

        for (T nx : valuesMap.keySet()) {
            dis = new Number[2];
            dis[0] = nx;
            dis[1] = valuesMap.get(nx);
            items.put(nx, dis);
        }
        return items;
    }

    public double getMean() {
        return mean;
    }

    public double getStandardDeviation() {
        return deviation;
    }

    public T getMin() {
        return min;
    }

    public T getMax() {
        return max;
    }

    public int getCount() {
        return count;
    }

    protected abstract T divide(T val, int n);

    public void addValue(T val, int sampleCount) {
        updateValueCount(val, sampleCount);
        count += sampleCount;
        double currentVal = val.doubleValue();
        sum += currentVal;
        T actualValue = val;
        if (sampleCount > 1){
            // For n values in an aggregate sample the average value = (val/n)
            // So need to add n * (val/n) * (val/n) = val * val / n
            sumOfSquares += currentVal * currentVal / sampleCount;
            actualValue = divide(val, sampleCount);
        } else {
            sumOfSquares += currentVal * currentVal;
            actualValue = val;
        }
        mean = sum / count;
        deviation = Math.sqrt((sumOfSquares / count) - (mean * mean));
        if (actualValue.compareTo(max) > 0){
            max=actualValue;
        }
        if (actualValue.compareTo(min) < 0){
            min=actualValue;
        }
    }

    public void addValue(T val) {
        addValue(val,1);
    }

    private void updateValueCount(T val, int sampleCount) {
        MutableLong count = valuesMap.get(val);
        if (count != null) {
            count.add(sampleCount);
        } else {
            // insert new value
            valuesMap.put(val, new MutableLong(sampleCount));
        }
    }
}

@@ -5731,52 +5731,8 @@
 ) %7B%0A
-        updateValueCount(val, sampleCount);%0A
     
@@ -5749,32 +5749,32 @@
 += sampleCount;%0A
+
         double c
@@ -6259,32 +6259,84 @@
  val;%0A        %7D%0A
+        updateValueCount(actualValue, sampleCount);%0A
         mean = s
@@ -6695,19 +6695,27 @@
 Count(T 
-val
+actualValue
 , int sa
@@ -6769,19 +6769,27 @@
 Map.get(
-val
+actualValue
 );%0A     
@@ -6923,19 +6923,27 @@
 Map.put(
-val
+actualValue
 , new Mu

Fix up values map for aggregate samples