package vismon.data;

import vismon.util.Constants;
import vismon.util.Util;

import java.util.*;


public class DataItem {

    private final int CDF_BIN_NUM = 200;

    protected MngAction mngAction;
    protected IndicatorValues[] indValues;  // length = simulation number
    protected Map<Indicator, double[]> mirroredIndNNaNValues;

    protected Map<Indicator, double[]> minMaxs;
    protected IndicatorValues averages;
    protected IndicatorValues medians;
    protected Map<Indicator, double[]> quartiles;
    protected Map<Indicator, Integer> nanNumbers;

    protected boolean filtered;

    protected boolean pdfComputed;
    protected Map<Indicator, Double> pdfWholeArea;
    protected Map<Indicator, List<Double>> pdfAreas;
    protected Map<Indicator, List<Double>> pdfValues;
    protected Map<Indicator, Double> pdfMax;
    protected Map<Indicator, Map<Integer, double[]>> errorBar;

    //protected Map<Indicator, Boolean>         fulfillsIndProbValues;

    public DataItem() {
        mngAction = new MngAction();

        indValues = new IndicatorValues[Session.getSimulationNum()];
        for (int i = 0; i < indValues.length; ++i)
            indValues[i] = new IndicatorValues();

        mirroredIndNNaNValues = new HashMap<Indicator, double[]>();

        minMaxs = new HashMap<Indicator, double[]>();
        averages = new IndicatorValues();
        medians = new IndicatorValues();
        quartiles = new HashMap<Indicator, double[]>();
        nanNumbers = new HashMap<Indicator, Integer>();

        filtered = false;

        pdfComputed = false;
        pdfWholeArea = new HashMap<Indicator, Double>();
        pdfAreas = new HashMap<Indicator, List<Double>>();
        pdfValues = new HashMap<Indicator, List<Double>>();
        pdfMax = new HashMap<Indicator, Double>();
        errorBar = new HashMap<Indicator, Map<Integer, double[]>>();

        /*fulfillsIndProbValues = new HashMap<Indicator, Boolean>();
        for (Indicator dim : Session.getIndicators())
            fulfillsIndProbValues.put(dim, true);*/
    }

    public DataItem(MngAction action) {
        this();
        mngAction = action;
    }

    public MngAction getMngAction() {
        return mngAction;
    }

    public boolean isFiltered() {
        for (Indicator indicator : Session.getIndicators()) {
            double value = getAverageValue(indicator);
            if (indicator.isMaxBest()) {
                if (value < indicator.getConstraintValue())
                    return true;
            } else {
                if (value > indicator.getConstraintValue())
                    return true;
            }

        }
        return false;
        //return filtered;
    }

    public boolean isFullFillsProbability(Indicator indicator, double percentage) {
        if (Session.getSimulationNum() == nanNumbers.get(indicator))
            return true;

        double fullfillingSimNum = Arrays.binarySearch(mirroredIndNNaNValues.get(indicator),
                indicator.getConstraintValue());
        if (fullfillingSimNum < 0) {
            fullfillingSimNum = -fullfillingSimNum - 1;
        }
        if (indicator.isMaxBest())
            fullfillingSimNum = (Session.getSimulationNum() - nanNumbers.get(indicator)) - fullfillingSimNum;

        double prob = fullfillingSimNum / (Session.getSimulationNum() - nanNumbers.get(indicator));

        if (prob >= percentage)
            return true;

        return false;
    }

    public void setMirroredIndValues() {
        for (Indicator ind : Session.getIndicators()) {
            double[] values = new double[Session.getSimulationNum() - nanNumbers.get(ind)];
            int index = 0;
            for (int i = 0; i < Session.getSimulationNum(); ++i) {
                double value = indValues[i].getValue(ind);
                if (!Util.isNaN(value))
                    values[index++] = value;
            }

            Arrays.sort(values);
            mirroredIndNNaNValues.put(ind, values);
        }
    }

    public boolean hasMngAction(MngAction action) {
        return mngAction.equals(action);
    }

    public void addValue(int simNum, Indicator ind, double value) {
        indValues[simNum].addValue(ind, value);

        // FINDING MIN AND MAX VALUES OF ALL SIMULATIONS FOR "DIM"
        if (ind.getMin() > value)
            ind.setMin(value);
        if (ind.getMax() < value)
            ind.setMax(value);
    }

    public double getValue(int simNum, Indicator ind) {
        //return mirroredIndNNaNValues.get(ind)[simNum];
        return indValues[simNum].getValue(ind);
    }

    public double[] getValues(Indicator ind) {
        return mirroredIndNNaNValues.get(ind);
        /*double[] v_s = new double[Session.getSimulationNum()];
        for (int i = 0; i < Session.getSimulationNum(); ++i)
            v_s[i] = indValues[i].getValue(ind);
        return v_s;*/
    }

    public boolean hasValue(int simNum, Indicator ind, double v) {
        return (Util.equals(v, mirroredIndNNaNValues.get(ind)[simNum]));
        //return indValues[simNum].hasValue(ind, v);
    }

    public double getAverageValue(Indicator ind) {
        return averages.getValue(ind);
    }

    public double getNormalizedAverageValue(Indicator ind) {
        return (averages.getValue(ind) - ind.getMin()) / (ind.getMax() - ind.getMin());
    }

    public boolean hasAverageValue(Indicator ind, double v) {
        return averages.hasValue(ind, v);
    }

    public double getMedianValue(Indicator ind) {
        return medians.getValue(ind);
    }

    public double getMinValue(Indicator ind) {
        return minMaxs.get(ind)[0];
    }

    public double getMaxValue(Indicator ind) {
        return minMaxs.get(ind)[1];
    }

    public double[] getQuartiles(Indicator ind) {
        return quartiles.get(ind);
    }

    ////////////////////////////////////////////////////////////////////////////

    public void doComputations() {
        countNaNNumbers();
        setMirroredIndValues();
        computeMinMaxs();
        computeAverages();
        computeMedians();
        computeQuartiles();
    }

    public void computeMinMaxs() {
        for (Indicator ind : Session.getIndicators())
            minMaxs.put(ind, computeMinMax(ind));
    }

    public void computeAverages() {
        for (Indicator ind : Session.getIndicators())
            averages.addValue(ind, computeAverage(ind));
    }

    public void computeMedians() {
        for (Indicator ind : Session.getIndicators())
            medians.addValue(ind, computeMedian(ind));
    }
    public void computeQuartiles() {
        for (Indicator ind : Session.getIndicators())
            quartiles.put(ind, computeQuartile(ind));
    }

    public void countNaNNumbers() {
        for (Indicator ind : Session.getIndicators())
            nanNumbers.put(ind, countNaNNumber(ind));
    }

    public double[] computeMinMax(Indicator ind) {
        double min = Double.MAX_VALUE;
        double max = Double.MIN_VALUE;
        for (double v : mirroredIndNNaNValues.get(ind)) {
            if (v < min)
                min = v;
            if (v > max)
                max = v;
        }
        return new double[]{min, max};
    }

    public double computeAverage(Indicator ind) {
        double[] values = mirroredIndNNaNValues.get(ind);

        int num = values.length;
        if (num == 1)
            return values[0];

        double sum = 0.0;
        for (double v : values)
            sum += v;
        return sum / num;
    }

    public double computeMedian(Indicator ind) {
        double[] values = mirroredIndNNaNValues.get(ind);
        int num = values.length;
        if (num == 0)
            return Constants.NA;

        return values[(int) (0.5 * num)];
    }

    public double[] computeQuartile(Indicator ind) {
        double values[] = mirroredIndNNaNValues.get(ind);
        int num = values.length;
        if (num == 0)
            return new double[]{Constants.NA, Constants.NA};

        return new double[]{values[(int) (0.25 * num)], values[(int) (0.75 * num)]};
    }

    private int countNaNNumber(Indicator indicator) {
        int nanNum = 0;
        for (int i = 0; i < Session.getSimulationNum(); ++i)
            if (Util.isNaN(getValue(i, indicator)))
                ++nanNum;
        return nanNum;
    }

    public double[] getErrorBar(Indicator ind, int percent) {
        Map<Integer, double[]> error = errorBar.get(ind);
        if (error != null && error.get(percent) != null)
            return error.get(percent);

        double[] ci = computeErrorBar(ind, percent);
        if (error == null)
            error = new HashMap<Integer, double[]>();
        error.put(percent, ci);
        errorBar.put(ind, error);

        return ci;
    }

    public double[] computeErrorBar(Indicator ind, int percent) {
        double[] errorBarBounds = new double[2];

        double min = minMaxs.get(ind)[0];
        double max = minMaxs.get(ind)[1];

        double a0 = (max - min) / (CDF_BIN_NUM - 1);

        double pnPercent = 0.5 - percent * 0.005;

        List<Double> fAreas = pdfAreas.get(ind);
        double fWholeArea = pdfWholeArea.get(ind);
        int fAreasNum = fAreas.size();

        double prevArea = 0;
        for (int i = 0; i < fAreasNum; ++i) {
            prevArea += fAreas.get(i);
            if ((prevArea / fWholeArea) >= pnPercent) {
                errorBarBounds[0] = min + (i - 1) * a0;
                break;
            }
        }

        double nextArea = 0;
        for (int i = fAreasNum - 1; i >= 0; --i) {
            nextArea += fAreas.get(i);
            if ((nextArea / fWholeArea) >= pnPercent) {
                errorBarBounds[1] = min + (i + 1) * a0;
                break;
            }
        }

        return errorBarBounds;
    }

    public void computePDF() {
        if (pdfComputed)
            return;

        int standardDeviation = 10;

        for (Indicator ind : Session.getIndicators()) {
            double[] values = getValues(ind);
            List<Double> fValues = new ArrayList<Double>();

            double minValue = Double.MAX_VALUE;
            double maxValue = Double.MIN_VALUE;

            for (int i = 0; i < values.length; ++i) {
                if (values[i] < minValue)
                    minValue = values[i];
                if (values[i] > maxValue)
                    maxValue = values[i];
            }

            double a0 = (maxValue - minValue) / (CDF_BIN_NUM - 1);
            double fMax = Double.MIN_VALUE;

            for (int n = 0; n < CDF_BIN_NUM; ++n) {
                double y_n = minValue + n * a0;
                double func_y_n = 0;

                for (int p = 0; p < values.length; ++p) {
                    if (Util.isNaN(values[p]))
                        continue;

                    func_y_n += Util.getGaussian(values[p], y_n, standardDeviation);
                }
                fValues.add(func_y_n);

                if (fMax < func_y_n)
                    fMax = func_y_n;
            }

            pdfValues.put(ind, fValues);
            pdfMax.put(ind, fMax);

            //CALCULATING AREAS OF FUNCTION TRAPEZOIDS
            List<Double> fAreas = new ArrayList<Double>();
            double wholeArea = 0;

            for (int i = 0; i < fValues.size() - 1; ++i) {
                double area = (fValues.get(i) + fValues.get(i + 1)) * a0 * 0.5;
                fAreas.add(area);
                wholeArea += area;
            }
            pdfAreas.put(ind, fAreas);
            pdfWholeArea.put(ind, wholeArea);
        }

        pdfComputed = true;
    }

    public Map<Indicator, Double> getPdfMax() {
        return pdfMax;
    }

    public Map<Indicator, List<Double>> getPdfValues() {
        return pdfValues;
    }

    /*public boolean isFulfillsIndProbValues(Indicator ind) {
        return fulfillsIndProbValues.get(ind);
    }*/

    /*public void checkFulfillsIndProbValues(Indicator ind) {
        double f = 0.0;
        for (int i = 0; i < Session.getSimulationNum(); ++i)
            if ( (ind.isMinBest() && indValues[i].getValue(ind) >= StatedProbDimTable.getBound(ind))
              || (!ind.getDShader().isReverseShading() && indValues[i].getValue(ind) <= StatedProbDimTable.getBound(ind)))
            ++f;

        double prob = (double)f / DataTable.getSimulationNum();

        boolean fulfill = false;
        if (prob <= StatedProbDimTable.getProb(ind))
            fulfill = true;
        fulfillsIndProbValues.put(ind, fulfill);
    }*/

    /*public boolean checkFulfillsStatedProb(StatedProbDim spd) {
        Indicator spdDim = spd.getDim();
        double spdBound = spd.getBound();
        double spdProb = spd.getProb();

        double f = 0.0;
        for (int i = 0; i < DataTable.getSimulationNum(); ++i)
            if ( (spdDim.getDShader().isReverseShading() && indValues[i].getValue(spdDim) >= spdBound)
              || (!spdDim.getDShader().isReverseShading() && indValues[i].getValue(spdDim) <= spdBound))
            ++f;

        double prob = (double)f / DataTable.getSimulationNum();
        if (prob <= spdProb)
            return true;
        return false;
    }*/

    @Override
    public boolean equals(Object o) {
        if (o instanceof DataItem) {
            DataItem oDataItem = (DataItem) o;
            return mngAction.equals(oDataItem.getMngAction());
        }
        return false;
    }

    @Override
    public String toString() {
        return mngAction.toString();
    }

}
