package test.beast.beast2vs1.trace;

import beast.util.HeapSort;

/**
 * @author Alexei Drummond
 * @author Andrew Rambaut
 * @author Walter Xie
 */
public class TraceStatistics {
    private static final int MAX_LAG = 2000;
    private boolean isValid = true;
    private boolean hasGeometricMean = false;

    private double minimum, maximum;
    private double mean;
    private double median;
    private double geometricMean;
    private double stdev;
    private double variance;
    private double cpdLower, cpdUpper, hpdLower, hpdUpper;
    private double ESS;

    private double stdErrorOfMean;
    private double autoCorrelationTime;
    private double stdevAutoCorrelationTime;

    public TraceStatistics(double[] values) {
        analyseDistributionContinuous(values, 0.95);
    }

    public TraceStatistics(double[] values, long stepSize) {
        this(values);

        if (isValid) {
            analyseCorrelationContinuous(values, stepSize);
        }
    }

    /**
     * @param valuesC    the values to analyze
     * @param proportion the proportion of probability mass included within interval.
     */
    private void analyseDistributionContinuous(double[] valuesC, double proportion) {

        mean = DiscreteStatistics.mean(valuesC);
        stdev = DiscreteStatistics.stdev(valuesC);
        variance = DiscreteStatistics.variance(valuesC);

        minimum = Double.POSITIVE_INFINITY;
        maximum = Double.NEGATIVE_INFINITY;

        for (double value : valuesC) {
            if (value < minimum) minimum = value;
            if (value > maximum) maximum = value;
        }

        if (minimum > 0) {
            geometricMean = DiscreteStatistics.geometricMean(valuesC);
            hasGeometricMean = true;
        }

        if (maximum == minimum) {
            isValid = false;
            return;
        }

        int[] indices = new int[valuesC.length];
        HeapSort.sort(valuesC, indices);
        median = DiscreteStatistics.quantile(0.5, valuesC, indices);
        cpdLower = DiscreteStatistics.quantile(0.025, valuesC, indices);
        cpdUpper = DiscreteStatistics.quantile(0.975, valuesC, indices);
        calculateHPDInterval(proportion, valuesC, indices);
        ESS = valuesC.length;

    }


    /**
     * @param proportion the proportion of probability mass included within interval.
     * @param array      the data array
     * @param indices    the indices of the ranks of the values (sort order)
     */
    private void calculateHPDInterval(double proportion, double[] array, int[] indices) {
        final double[] hpd = DiscreteStatistics.HPDInterval(proportion, array, indices);
        hpdLower = hpd[0];
        hpdUpper = hpd[1];
    }

    /**
     * Analyze trace
     *
     * @param values   the values
     * @param stepSize the sampling frequency of the values
     */
    private void analyseCorrelationContinuous(double[] values, long stepSize) {

        final int samples = values.length;
        int maxLag = Math.min(samples - 1, MAX_LAG);

        double[] gammaStat = new double[maxLag];
        //double[] varGammaStat = new double[maxLag];
        double varStat = 0.0;
        //double varVarStat = 0.0;
        //double assVarCor = 1.0;
        //double del1, del2;

        for (int lag = 0; lag < maxLag; lag++) {
            for (int j = 0; j < samples - lag; j++) {
                final double del1 = values[j] - mean;
                final double del2 = values[j + lag] - mean;
                gammaStat[lag] += (del1 * del2);
                //varGammaStat[lag] += (del1*del1*del2*del2);
            }

            gammaStat[lag] /= (samples - lag);
            //varGammaStat[lag] /= ((double) samples-lag);
            //varGammaStat[lag] -= (gammaStat[0] * gammaStat[0]);

            if (lag == 0) {
                varStat = gammaStat[0];
                //varVarStat = varGammaStat[0];
                //assVarCor = 1.0;
            } else if (lag % 2 == 0) {
                // fancy stopping criterion :)
                if (gammaStat[lag - 1] + gammaStat[lag] > 0) {
                    varStat += 2.0 * (gammaStat[lag - 1] + gammaStat[lag]);
                    // varVarStat += 2.0*(varGammaStat[lag-1] + varGammaStat[lag]);
                    // assVarCor  += 2.0*((gammaStat[lag-1] * gammaStat[lag-1]) + (gammaStat[lag] * gammaStat[lag])) / (gammaStat[0] * gammaStat[0]);
                }
                // stop
                else
                    maxLag = lag;
            }
        }

        // standard error of mean
        stdErrorOfMean = Math.sqrt(varStat / samples);
        // auto correlation time
        autoCorrelationTime = stepSize * varStat / gammaStat[0];
        // effective sample size
        ESS = (stepSize * samples) / autoCorrelationTime;
        // standard deviation of autocorrelation time
        stdevAutoCorrelationTime = (2.0 * Math.sqrt(2.0 * (2.0 * (maxLag + 1)) / samples) * (varStat / gammaStat[0]) * stepSize);

        isValid = true;
    }

    public double getMinimum() {
        return minimum;
    }

    public double getMaximum() {
        return maximum;
    }

    public double getMean() {
        return mean;
    }

    public double getMedian() {
        return median;
    }

    public double getGeometricMean() {
        if (hasGeometricMean) return geometricMean;
        return Double.NaN;
    }

    public double getStdev() {
        return stdev;
    }

    public double getVariance() {
        return variance;
    }

    public double getCpdLower() {
        return cpdLower;
    }

    public double getCpdUpper() {
        return cpdUpper;
    }

    public double getHpdLower() {
        return hpdLower;
    }

    public double getHpdUpper() {
        return hpdUpper;
    }

    public double getESS() {
        return ESS;
    }

    public double getStdErrorOfMean() {
        return stdErrorOfMean;
    }

    public double getAutoCorrelationTime() {
        return autoCorrelationTime;
    }

    public double getStdevAutoCorrelationTime() {
        return stdevAutoCorrelationTime;
    }
}
