/*
 * Copyright (c) 2007-2010 by The Broad Institute, Inc. and the Massachusetts Institute of Technology.
 * All Rights Reserved.
 *
 * This software is licensed under the terms of the GNU Lesser General Public License (LGPL), Version 2.1 which
 * is available at http://www.opensource.org/licenses/lgpl-2.1.php.
 *
 * THE SOFTWARE IS PROVIDED "AS IS." THE BROAD AND MIT MAKE NO REPRESENTATIONS OR WARRANTIES OF
 * ANY KIND CONCERNING THE SOFTWARE, EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT
 * OR OTHER DEFECTS, WHETHER OR NOT DISCOVERABLE.  IN NO EVENT SHALL THE BROAD OR MIT, OR THEIR
 * RESPECTIVE TRUSTEES, DIRECTORS, OFFICERS, EMPLOYEES, AND AFFILIATES BE LIABLE FOR ANY DAMAGES OF
 * ANY KIND, INCLUDING, WITHOUT LIMITATION, INCIDENTAL OR CONSEQUENTIAL DAMAGES, ECONOMIC
 * DAMAGES OR INJURY TO PROPERTY AND LOST PROFITS, REGARDLESS OF WHETHER THE BROAD OR MIT SHALL
 * BE ADVISED, SHALL HAVE OTHER REASON TO KNOW, OR IN FACT SHALL KNOW OF THE POSSIBILITY OF THE
 * FOREGOING.
 */

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package org.broad.igv.tools;

import org.broad.igv.util.collections.DoubleArrayList;
import org.apache.commons.math.stat.StatUtils;
import org.apache.log4j.Logger;
import org.broad.igv.track.WindowFunction;

import java.util.*;

/**
 * Estimating percentiles -- weighted average of multiple estimates
 *
 * @author jrobinso
 */
public class Accumulator {

    static Set<WindowFunction> PERCENTILE_WINDOW_FUNCTIONS = new HashSet();
    public static int MAX_VALUE_COUNT = 100000;
    private static Logger log = Logger.getLogger(Accumulator.class);

    static {
        PERCENTILE_WINDOW_FUNCTIONS.add(WindowFunction.median);
        PERCENTILE_WINDOW_FUNCTIONS.add(WindowFunction.percentile2);
        PERCENTILE_WINDOW_FUNCTIONS.add(WindowFunction.percentile10);
        PERCENTILE_WINDOW_FUNCTIONS.add(WindowFunction.percentile90);
        PERCENTILE_WINDOW_FUNCTIONS.add(WindowFunction.percentile98);
    }


    boolean isFinished = false;

    List<WindowFunction> windowFunctions;
    List<WindowFunction> quantileFunctions;
    DoubleArrayList values = null;
    float sum = 0.0f;
    int nPts = 0;


    Map<WindowFunction, List<PercentileValue>> percentiles = new HashMap();


    float min = Float.NaN;
    float max = Float.NaN;
    float mean = Float.NaN;
    float median = Float.NaN;
    float percentile2 = Float.NaN;
    float percentile10 = Float.NaN;
    float percentile90 = Float.NaN;
    float percentile98 = Float.NaN;


    public Accumulator(Collection<WindowFunction> windowFunctions) {
        this.windowFunctions = new ArrayList(windowFunctions);
        quantileFunctions = new ArrayList();
        for (WindowFunction wf : windowFunctions) {
            if (PERCENTILE_WINDOW_FUNCTIONS.contains(wf)) {
                quantileFunctions.add(wf);
                if (values == null) {
                    values = new DoubleArrayList();
                }
            }
        }
    }

    public void add(float v) {
        if (!Float.isNaN(v)) {
            min = Float.isNaN(min) ? v : Math.min(min, v);
            max = Float.isNaN(max) ? v : Math.max(max, v);
            sum += v;
            nPts++;
            if (values != null) {
                values.add(v);
                if (values.size() > MAX_VALUE_COUNT) {
                    computePercentiles();
                    values.clear();
                }
            }
        }
    }


    public void finish() {

        if (isFinished) {
            return;
        }

        mean = Float.isNaN(sum) ? Float.NaN : sum / nPts;

        if (values != null) {
            if (nPts == 1) {
                for (WindowFunction wf : quantileFunctions) {
                    setValue(wf, mean);
                }
            } else {
                if (values.size() > 1) {
                    computePercentiles();
                }
                for (WindowFunction wf : quantileFunctions) {

                    List<PercentileValue> pList = percentiles.get(wf);
                    float v = Float.NaN; // <= Default,
                    if (pList != null && pList.size() > 0) {
                        double weightedSum = 0;
                        double sumOfWeights = 0;
                        for (PercentileValue pv : pList) {
                            double weight = (double) pv.nPoints / nPts;
                            sumOfWeights += weight;
                            weightedSum += weight * pv.value;
                        }
                        v = (float) (weightedSum / sumOfWeights);
                    }
                    setValue(wf, v);

                }

            }
        }
        values = null;
        isFinished = true;

    }

    private void computePercentiles() {
        if (values != null) {
            double[] valueArray = values.toArray();
            for (WindowFunction wf : quantileFunctions) {
                double p = this.getPercentile(wf);
                if (p > 0) {
                    float v = (float) StatUtils.percentile(valueArray, p);
                    if (Float.isInfinite(v)) {
                        log.error("Infinite percentile (" + wf + ")");
                    } else {
                        List<PercentileValue> pList = percentiles.get(wf);
                        if (pList == null) {
                            pList = new ArrayList();
                            percentiles.put(wf, pList);
                        }
                        pList.add(new PercentileValue(valueArray.length, v));
                    }
                }
            }
        }

    }

    private void setValue(WindowFunction wf, float value) {
        switch (wf) {
            case mean:
                mean = value;
                break;
            case median:
                median = value;
                break;
            case min:
                min = value;
                break;
            case max:
                max = value;
                break;
            case percentile2:
                percentile2 = value;
                break;
            case percentile10:
                percentile10 = value;
                break;
            case percentile90:
                percentile90 = value;
                break;
            case percentile98:
                percentile98 = value;
                break;
            default:
                System.err.println("Unexpected window function: " + wf.toString());
        }


    }

    public float getValue(WindowFunction wf) {

        switch (wf) {
            case mean:
                return mean;
            case median:
                return median;
            case min:
                return min;
            case max:
                return max;
            case percentile2:
                return percentile2;
            case percentile10:
                return percentile10;
            case percentile90:
                return percentile90;
            case percentile98:
                return percentile98;
            case count:
                return nPts;
            default:
                System.err.println("Unexpected window function: " + wf.toString());

        }
        return Float.NaN;

    }


    public double getPercentile(WindowFunction wf) {
        switch (wf) {
            case percentile2:
                return 2;
            case percentile10:
                return 10;
            case percentile90:
                return 90;
            case percentile98:
                return 98;
            case median:
                return 50;
            default:
                return -1.0;
        }
    }


    class PercentileValue {
        int nPoints;
        double value;

        PercentileValue(int nPoints, double value) {
            this.nPoints = nPoints;
            this.value = value;
        }
    }

}
