/*
 * SessionOfStandardView.java
 *
 *
 * Copyright 2006-2018 James F. Bowring, CIRDLES.org, and Earth-Time.org
 *
 *  Licensed 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.earthtime.Tripoli.dataViews.simpleViews;

import Jama.Matrix;
import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.SortedSet;
import javax.swing.JLayeredPane;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import org.earthtime.Tripoli.dataModels.DataModelFitFunctionInterface;
import org.earthtime.Tripoli.dataModels.DataModelInterface;
import org.earthtime.Tripoli.dataModels.sessionModels.AbstractSessionForStandardDataModel;
import org.earthtime.Tripoli.dataViews.AbstractRawDataView;
import org.earthtime.Tripoli.fitFunctions.AbstractFunctionOfX;
import org.earthtime.Tripoli.fractions.TripoliFraction;
import org.earthtime.dataDictionaries.DataPresentationModeEnum;
import org.earthtime.dataDictionaries.IncludedTypeEnum;
import org.earthtime.utilities.TicGeneratorForAxes;

/**
 *
 * @author James F. Bowring
 */
public class SessionOfStandardView extends AbstractRawDataView implements FitFunctionDataInterface {

    /**
     *
     */
//    public static int DEFAULT_WIDTH_OF_PANE = 490;//600;
    //
    private DataModelFitFunctionInterface sessionForStandardDataModel;
    private SortedSet<TripoliFraction> tripoliFractions;
    private TripoliFraction[] tripoliFractionArray;
    private ArrayList<Double> zeroBasedFractionAquireTimes;
    private boolean[] fractionIncludedMap;
    private double[] stdErrorOfMean;
    // because of log space and the fact that upper and lower may not be equal in future
    // these are for plotting only
    private double[] myOnPeakDataPlusUnct;
    private double[] myOnPeakDataLessUnct;
    private double[] myOnPeakDataPlusUnctPlusOD;
    private double[] myOnPeakDataLessUnctPlusOD;
    private double[][] fitFunctionDataDisplay;
    private double[][] fitFunctionMinusUnctDataDisplay;
    private double[][] fitFunctionPlusUnctDataDisplay;
    private Color paintColor;

    /**
     *
     * @param sampleSessionDataView
     * @param rawRatioDataModel
     * @param sessionForStandardDataModel
     * @param tripoliFractions
     * @param dataPresentationMode
     * @param bounds
     */
    public SessionOfStandardView(//
            JLayeredPane sampleSessionDataView,//
            DataModelFitFunctionInterface sessionForStandardDataModel,//
            SortedSet<TripoliFraction> tripoliFractions,//
            DataModelInterface rawRatioDataModel,//
            DataPresentationModeEnum dataPresentationMode,//
            Rectangle bounds) {
        super(bounds);

        this.sampleSessionDataView = sampleSessionDataView;
        this.sessionForStandardDataModel = sessionForStandardDataModel;
        this.tripoliFractions = tripoliFractions;

        this.tripoliFractionArray = null;
        this.zeroBasedFractionAquireTimes = null;
        this.fractionIncludedMap = null;
        this.stdErrorOfMean = null;
        this.fitFunctionDataDisplay = null;

        this.paintColor = Color.black;

        this.dataPresentationMode = dataPresentationMode;

        this.standardValue = ((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getStandardValue();

        addMeAsMouseListener();
    }

    /**
     *
     * @param g2d
     */
    @Override
    public void paint(Graphics2D g2d) {
        //super.paint( g2d );

        paintInit(g2d);

        if (!(myOnPeakData == null)) {
            // box and whiskers
            g2d.setStroke(new BasicStroke(1.0f));
            for (int i = 0; i < myOnPeakData.length; i++) {
                try {
                    Shape meanSquare = new Rectangle2D.Double( //
                            mapX(zeroBasedFractionAquireTimes.get(i)) - 1.0,// 
                            mapY(myOnPeakData[i]) - 1.0,//
                            2,
                            2);
                    g2d.setPaint(determineDataColor(i, paintColor));

                    g2d.fill(meanSquare);
                    g2d.draw(meanSquare);

                    if (fitFunctionDataDisplay != null) {
                        g2d.setColor(Color.red);

                        Shape stdErrLinePlusOD = new Line2D.Double(//
                                mapX(zeroBasedFractionAquireTimes.get(i)) + 0.0,// 
                                mapY(myOnPeakDataPlusUnctPlusOD[i]),//
                                mapX(zeroBasedFractionAquireTimes.get(i)) + 0.0,// 
                                mapY(myOnPeakDataLessUnctPlusOD[i]));

                        g2d.draw(stdErrLinePlusOD);
                    }

                    g2d.setColor(Color.black);
                    Shape stdErrLine = new Line2D.Double(//
                            mapX(zeroBasedFractionAquireTimes.get(i)) + 0.0,// 
                            mapY(myOnPeakDataPlusUnct[i]),//
                            mapX(zeroBasedFractionAquireTimes.get(i)) + 0.0,// 
                            mapY(myOnPeakDataLessUnct[i]));

                    g2d.draw(stdErrLine);

                    // paint red vertical line where mouse clicks
                    if (tripoliFractionArray[i].getShowVerticalLineAtThisIndex() > -1) {
                        paintFractionVerticalTicRed(g2d, i);
                    }
                } catch (Exception e) {
                }
            }
        }

        if (fitFunctionDataDisplay != null) {

            // plot smoothing fittedCurve or fitted line as required
            Path2D fittedCurve = new Path2D.Double();
            fittedCurve.moveTo(//
                    mapX(fitFunctionDataDisplay[0][0]),//
                    mapY(fitFunctionDataDisplay[1][0]));

            for (int i = 1; i < fitFunctionDataDisplay[1].length; i++) {
                fittedCurve.lineTo(//
                        mapX(fitFunctionDataDisplay[0][i]),//
                        mapY(fitFunctionDataDisplay[1][i]));
            }

            for (int i = fitFunctionDataDisplay[1].length - 1; i >= 0; i--) {
                fittedCurve.lineTo(//
                        mapX(fitFunctionDataDisplay[0][i]),//
                        mapY(fitFunctionDataDisplay[1][i]) + 1.0f);
            }

            fittedCurve.lineTo(//
                    mapX(fitFunctionDataDisplay[0][0]),//
                    mapY(fitFunctionDataDisplay[1][0]));

            g2d.setPaint(Color.blue);
            g2d.fill(fittedCurve);

            // plot smoothing PLUS UNCERTAINTY ENVELOPE fittedCurve or fitted line as required
            fittedCurve = new Path2D.Double();
            fittedCurve.moveTo(//
                    mapX(fitFunctionPlusUnctDataDisplay[0][0]),//
                    mapY(fitFunctionPlusUnctDataDisplay[1][0]));

            for (int i = 1; i < fitFunctionPlusUnctDataDisplay[1].length; i++) {
                fittedCurve.lineTo(//
                        mapX(fitFunctionPlusUnctDataDisplay[0][i]),//
                        mapY(fitFunctionPlusUnctDataDisplay[1][i]));
            }

            for (int i = fitFunctionMinusUnctDataDisplay[1].length - 1; i >= 0; i--) {
                fittedCurve.lineTo(//
                        mapX(fitFunctionMinusUnctDataDisplay[0][i]),//
                        mapY(fitFunctionMinusUnctDataDisplay[1][i]));
            }

            fittedCurve.lineTo(//
                    mapX(fitFunctionPlusUnctDataDisplay[0][0]),//
                    mapY(fitFunctionPlusUnctDataDisplay[1][0]));

            Composite originalComposite = g2d.getComposite();
            g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.10f));
            g2d.setPaint(Color.red);
            g2d.fill(fittedCurve);
            //restore composite
            g2d.setComposite(originalComposite);
        }

    }

    @Override
    protected Color determineDataColor(int index, Color defaultColor) {
        return ((Color) (fractionIncludedMap[index] ? defaultColor : EXCLUDED_COLOR));
    }

    private void paintFractionVerticalTicRed(Graphics2D g2d, int chosenDatumIndex) {
        Shape redDatumLine = new Line2D.Double(//
                mapX(zeroBasedFractionAquireTimes.get(chosenDatumIndex)),// 
                mapY(minY),//
                mapX(zeroBasedFractionAquireTimes.get(chosenDatumIndex)),// 
                mapY(maxY));

        Paint savedPaint = g2d.getPaint();
        Stroke savedStroke = g2d.getStroke();
        g2d.setPaint(EXCLUDED_COLOR);
        g2d.setStroke(new BasicStroke(0.5f));
        g2d.draw(redDatumLine);
        g2d.setPaint(savedPaint);
        g2d.setStroke(savedStroke);
    }

    /**
     *
     */
    @Override
    public void updateFittedData(boolean doReScale) {

        if (doReScale) {
            // Y-axis is ratios
            minY = Double.MAX_VALUE;
            maxY = -Double.MAX_VALUE;
        }

        // X-axis lays out time 
        minX = 0;
        maxX = zeroBasedFractionAquireTimes.get(zeroBasedFractionAquireTimes.size() - 1);

        AbstractFunctionOfX fitFunc;
        fitFunc = sessionForStandardDataModel.getSelectedFitFunction();

        double[] timesOfMatrixJfPlotting = ((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getTripoliSession().getTimesForPlotting();
        if ((fitFunc != null) &&(timesOfMatrixJfPlotting != null)) {

            int countOfPlottedPoints = timesOfMatrixJfPlotting.length;

            fitFunctionDataDisplay = new double[2][countOfPlottedPoints];
            fitFunctionMinusUnctDataDisplay = new double[2][countOfPlottedPoints];
            fitFunctionPlusUnctDataDisplay = new double[2][countOfPlottedPoints];

            fitFunctionDataDisplay[0] = timesOfMatrixJfPlotting;
            fitFunctionMinusUnctDataDisplay[0] = timesOfMatrixJfPlotting;
            fitFunctionPlusUnctDataDisplay[0] = timesOfMatrixJfPlotting;

            Matrix matrixJfPlotting
                    = ((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getTripoliSession().getMatrixJfPlottingActiveStandards(fitFunc.getShortName());

            double[] variances = fitFunc.calculateInterpolatedVariances(matrixJfPlotting, timesOfMatrixJfPlotting);

            for (int i = 0; i < countOfPlottedPoints; i++) {

                try {
                    double fitFuncValue = fitFunc.f(fitFunctionDataDisplay[0][i]);
                    double fitFuncUnct = Math.sqrt(variances[i]);

                    fitFunctionDataDisplay[1][i] = convertLogDatumToPresentationMode(fitFuncValue);
                    fitFunctionMinusUnctDataDisplay[1][i] = convertLogDatumToPresentationMode(fitFuncValue - 2 * fitFuncUnct);
                    fitFunctionPlusUnctDataDisplay[1][i] = convertLogDatumToPresentationMode(fitFuncValue + 2 * fitFuncUnct);

                } catch (Exception e) {
                }
            }

            if (doReScale) {
                // prepare fitted data for display as alpha or ratio or log
                for (int i = 0; i < fitFunctionDataDisplay[1].length; i++) {
                    minY = Math.min(minY, fitFunctionDataDisplay[1][i]);
                    maxY = Math.max(maxY, fitFunctionDataDisplay[1][i]);
                }

                // note uncertainty envelopes not to be scaled per Noah Feb 2013
            }

        } else {
            fitFunctionDataDisplay = null;
        }

        // show data anyway
        if (!(((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getIncludedStandardMeanLogRatios() == null)) {

            // nov 2014
            myOnPeakData = ((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getAllStandardsMeanLogRatios().clone();
            stdErrorOfMean = ((AbstractSessionForStandardDataModel) sessionForStandardDataModel).getAllStandardsMeanLogRatioStdErrs().clone();

            // prepare means data for display as alpha or ratio or log
            myOnPeakDataPlusUnct = new double[myOnPeakData.length];
            myOnPeakDataLessUnct = new double[myOnPeakData.length];
            myOnPeakDataPlusUnctPlusOD = new double[myOnPeakData.length];
            myOnPeakDataLessUnctPlusOD = new double[myOnPeakData.length];

            for (int i = 0; i < myOnPeakData.length; i++) {
                myOnPeakDataPlusUnct[i] = convertLogDatumToPresentationMode(myOnPeakData[i] + 2.0 * stdErrorOfMean[i]);
                myOnPeakDataLessUnct[i] = convertLogDatumToPresentationMode(myOnPeakData[i] - 2.0 * stdErrorOfMean[i]);

                if (fitFunc != null) {
                    double oneSigmaPlusOD = Math.sqrt(stdErrorOfMean[i] * stdErrorOfMean[i] + fitFunc.getOverDispersion());
                    myOnPeakDataPlusUnctPlusOD[i] = convertLogDatumToPresentationMode(myOnPeakData[i] + 2.0 * oneSigmaPlusOD);
                    myOnPeakDataLessUnctPlusOD[i] = convertLogDatumToPresentationMode(myOnPeakData[i] - 2.0 * oneSigmaPlusOD);
                }

                // do last as data is needed in above calcs            
                myOnPeakData[i] = convertLogDatumToPresentationMode(myOnPeakData[i]);

                if (doReScale) {
                    // sept 2015 modified to allow rescaling when needed
                    // find min and max y
                    boolean showAll = showIncludedDataPoints.equals(IncludedTypeEnum.ALL);
                    // rework logic April 2016 - we have both included fractions and included data to consider
                    // here we include or not the data points in the view via minY and maxY 
                    if (showAll || fractionIncludedMap[i]) {
                        // added for no fit func
                        if (fitFunc == null) {
                            //handling alpha flip too
                            minY = Math.min(minY, myOnPeakDataPlusUnct[i]);
                            minY = Math.min(minY, myOnPeakDataLessUnct[i]);

                            maxY = Math.max(maxY, myOnPeakDataLessUnct[i]);
                            maxY = Math.max(maxY, myOnPeakDataPlusUnct[i]);
                        } else {
                            // do both min and max to be sure especially as alphas may flip
                            // just use the one including plus OD  which will be bigger or equal to err
                            minY = Math.min(minY, myOnPeakDataPlusUnctPlusOD[i]);
                            minY = Math.min(minY, myOnPeakDataLessUnctPlusOD[i]);

                            maxY = Math.max(maxY, myOnPeakDataPlusUnctPlusOD[i]);
                            maxY = Math.max(maxY, myOnPeakDataLessUnctPlusOD[i]);
                        }
                    }
                }
            }
        }

        double xMarginStretch = TicGeneratorForAxes.generateMarginAdjustment(minX, maxX, 0.05);
        minX -= xMarginStretch;
        maxX += xMarginStretch;

        if (doReScale) {
            double yMarginStretch = TicGeneratorForAxes.generateMarginAdjustment(minY, maxY, 12.0 / this.getHeight());
            minY -= yMarginStretch;
            maxY += yMarginStretch;
        }
    }

    /**
     *
     * @param doReScale the value of doReScale
     * @param inLiveMode the value of inLiveMode
     */
    @Override
    public void preparePanel(boolean doReScale, boolean inLiveMode) {

        this.removeAll();

        if (doReScale) {
            setDisplayOffsetY(0.0);
        }
        setDisplayOffsetX(0.0);

        // map fraction means into myOnPeakData and fraction times into myOnPeakNormalizedAquireTimes  
        tripoliFractionArray = new TripoliFraction[tripoliFractions.size()];
        zeroBasedFractionAquireTimes = new ArrayList<>();
        fractionIncludedMap = new boolean[tripoliFractions.size()];

        int index = 0;
        Iterator<TripoliFraction> fractionIterator = tripoliFractions.iterator();
        while (fractionIterator.hasNext()) {
            TripoliFraction tf = fractionIterator.next();
            tripoliFractionArray[index] = tf;
            zeroBasedFractionAquireTimes.add((double) tf.getZeroBasedNormalizedTimeStamp() + 2.0);
            fractionIncludedMap[index] = tf.isIncluded();

            index++;
        }

        updateFittedData(doReScale);

    }

    @Override
    public void mousePressed(MouseEvent evt
    ) {
        int timeSlot = convertMouseXToValue(evt.getX()) - (int) shiftAquiredTimeIndex;

        if (timeSlot < 0) {
            timeSlot = 0;
        }

        // find fraction to left of click or under it
        int index = Collections.binarySearch(zeroBasedFractionAquireTimes, (double) timeSlot);

        if (index < 0) {
            index = Math.abs(index) - 2;
        }
        if (index >= (zeroBasedFractionAquireTimes.size() - 1)) {
            index = zeroBasedFractionAquireTimes.size() - 2;
        }

        // adjust index to nearest fraction
        if ((timeSlot - zeroBasedFractionAquireTimes.get(index == -1 ? 0 : index)) //
                > (zeroBasedFractionAquireTimes.get(index + 1) - timeSlot)) {
            index++;
        }

        final int finalTimeSlot = (int) ((index == -1) ? 0 : index);

        // tripolifraction used as placeholder for mouse click only
        tripoliFraction = tripoliFractionArray[finalTimeSlot];
        tripoliFraction.setShowVerticalLineAtThisIndex(finalTimeSlot);
        try {
            sampleSessionDataView.repaint();
        } catch (Exception e) {
        }

        // handle right button or control button for mac mouse
        // feb 2013 this solves the mac/windows/one button/ two button problem
        if (evt.isPopupTrigger() || (evt.getButton() != MouseEvent.BUTTON1)) {
            //Create the popup menu.
            JPopupMenu popup = new JPopupMenu();

            //  show coordinates fyi
            double onPeakValue = zeroBasedFractionAquireTimes.get(finalTimeSlot);
            DecimalFormat f = new DecimalFormat("#######0 seconds");

            JMenuItem menuItem
                    = //
                    new JMenuItem("(" + f.format(onPeakValue) + ")");
            popup.add(menuItem);

            if (tripoliFraction.isIncluded()) {

                menuItem = new JMenuItem("EXCLUDE this fraction (all aquisitions).");
                menuItem.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent arg0) {

                        tripoliFraction.toggleAllDataExceptShaded(false);
                        fractionIncludedMap[finalTimeSlot] = false;
                        tripoliFraction.setShowVerticalLineAtThisIndex(-1);
                        try {
                            //moved to button may 2016 ((TripoliSessionFractionationCalculatorInterface) sampleSessionDataView).calculateSessionFitFunctionsForPrimaryStandard();
                            //((AbstractRawDataView) sampleSessionDataView).refreshPanel();
                        } catch (Exception e) {
                            System.out.println(">>>>>>>>>>>>trouble at standard exclude");
                        }
                        // removed may 2016 updateReportTable();
                        ((AbstractRawDataView) sampleSessionDataView).refreshPanel(true, false);
                    }
                });
                popup.add(menuItem);
            } else {
                menuItem = new JMenuItem("INCLUDE this fraction (all aquisitions).");
                menuItem.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent arg0) {

                        tripoliFraction.toggleAllDataExceptShaded(true);
                        fractionIncludedMap[finalTimeSlot] = true;
                        tripoliFraction.setShowVerticalLineAtThisIndex(-1);
                        try {
                            // moved to button may 2016 ((TripoliSessionFractionationCalculatorInterface) sampleSessionDataView).calculateSessionFitFunctionsForPrimaryStandard();
                        } catch (Exception e) {
                            System.out.println(">>>>>>>>>>>>trouble at standard include");
                        }
                        // removed may 2016 updateReportTable();
                        ((AbstractRawDataView) sampleSessionDataView).refreshPanel(true, false);
                    }
                });
                popup.add(menuItem);
            }

            // show the menu
            popup.show(evt.getComponent(), evt.getX() + 10, evt.getY() - 10);

        }
    }

    @Override
    public void mouseReleased(MouseEvent e
    ) {
        tripoliFraction.setShowVerticalLineAtThisIndex(-1);
        if (!(sampleSessionDataView == null)) {
            sampleSessionDataView.repaint();
        }
    }

    /**
     * @param paintColor the paintColor to set
     */
    @Override
    public void setPaintColor(Color paintColor
    ) {
        this.paintColor = paintColor;
    }

    /**
     * @return the sessionForStandardDataModel
     */
    public DataModelFitFunctionInterface getFractionationAlphaDataModel() {
        return sessionForStandardDataModel;
    }

    @Override
    public DataModelInterface getDataModel() {
        return tripoliFractions.first().getRawRatioDataModelByName(//
                sessionForStandardDataModel.getRawRatioModelName());
    }

    /**
     * @return the sessionForStandardDataModel
     */
    public DataModelFitFunctionInterface getSessionForStandardDataModel() {
        return sessionForStandardDataModel;
    }

    /**
     *
     * @return
     */
    @Override
    public boolean amShowingUnknownFraction() {
        return false;
    }

    @Override
    public void setShowFittedFunction(boolean showFittedFunction) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}