package ch.psi.plot.jfree;

import ch.psi.plot.LinePlotBase;
import ch.psi.plot.LinePlotSeries;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYAnnotation;
import org.jfree.chart.annotations.XYDrawableAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.StandardEntityCollection;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.labels.XYToolTipGenerator;
import org.jfree.chart.plot.Marker;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.xy.XYDataItem;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;
import org.jfree.ui.Layer;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RefineryUtilities;
import org.jfree.ui.TextAnchor;

public class LinePlot extends LinePlotBase {

    private static final Logger logger = Logger.getLogger(LinePlot.class.getName());

    private ChartPanel chartPanel;

    private static final int chartPanelWidth = 500;
    private static final int chartPanelHeight = 270;

    //Defining Context Menu Label
    private static final String showLegendMenuLabel = "Show Legend";
    private static final String hideLegendMenuLabel = "Hide Legend";
    private static final String showTooltipsMenuLabel = "Show Tooltips";
    private static final String hideTooltipsMenuLabel = "Hide Tooltips";

    private XYSeriesCollection data;

    JFreeChart chart;

    private boolean tooltips = false;

    /**
     * @param title	Title of plot
     */
    public LinePlot() {
        super();
        data = new XYSeriesCollection();
    }

    @Override
    public void onAppendData(LinePlotSeries series, double x, double y) {
        XYSeries s = getXYSeries(series);
        s.add(new XYDataItem(x, y), false);
    }

    @Override
    protected void onSetData(LinePlotSeries series, double[] x, double[] y) {
        XYSeries s = getXYSeries(series);
        if (!s.isEmpty()) {
            s.clear();
        }
        for (int i = 0; i < y.length; i++) {
            s.add(new XYDataItem(x[i], y[i]), false);
        }
    }

    @Override
    protected Object onAddedSeries(LinePlotSeries series) {
        final XYSeries s = new XYSeries(series.getName());
        data.addSeries(s);

        return s;
    }

    @Override
    protected void onRemovedSeries(LinePlotSeries series) {
        /*
         for (Object s : data.getSeries()) {
         if (((XYSeries)s).getKey().equals(series.getName())) {
         data.removeSeries(((XYSeries)s));
         }
         }
         */
        if ((series != null) && (series.getToken() != null)) {
            data.removeSeries((XYSeries) (series.getToken()));
        }
    }

    @Override
    public double[][] getSeriesData(final LinePlotSeries series) {
        final XYSeries s = getXYSeries(series);
        List<XYDataItem> items = s.getItems();
        double[] x = new double[items.size()];
        double[] y = new double[items.size()];
        for (int i = 0; i < y.length; i++) {
            x[i] = items.get(i).getXValue();
            y[i] = items.get(i).getYValue();
        }
        return new double[][]{x, y};
    }

    @Override
    public void onRemovedAllSeries() {
        data.removeAllSeries();
    }

    public void updateSeries(LinePlotSeries series) {
        XYSeries s = getXYSeries(series);
        s.fireSeriesChanged();
    }

    XYSeries getXYSeries(LinePlotSeries series) {
        /*
         for (Object s : data.getSeries()) {
         if (((XYSeries)s).getKey().equals(series.getName())) {
         return ((XYSeries)s);
         }
         }
         return null;
         */
        return (XYSeries) (series.getToken());
    }

    /**
     * Get the chart panel of this plot. The chart panel will be lazily created
     * the first time this function is called
     *
     * @return
     */
    @Override
    protected JPanel createChartPanel() {
        // Create chart
        chart = ChartFactory.createXYLineChart(getTitle(), getAxis(AxisId.X).getLabel(), getAxis(AxisId.Y).getLabel(), data, PlotOrientation.VERTICAL, true, tooltips, false);

        // Customize chart look and feel
        // Customize background (gray: new Color(238,238,238))
        chart.setBackgroundPaint(null);

        // Customize legend
        chart.getLegend().setVisible(false);
        chart.getLegend().setBackgroundPaint(null);
        chart.getLegend().setBorder(0, 0, 0, 0);

        // Customize plot area look and feel
        XYPlot plot = (XYPlot) chart.getPlot();
        plot.setBackgroundPaint(Color.white);
        plot.setDomainGridlinePaint(Color.gray);
        plot.setRangeGridlinePaint(Color.gray);

        // Show data point
        ((XYLineAndShapeRenderer) plot.getRenderer()).setBaseShapesVisible(true);

        // Include zeros in range (x) axis
        ((NumberAxis) plot.getRangeAxis()).setAutoRangeIncludesZero(false);

        // Lazy creation of the chart panel
        chartPanel = new ChartPanel(chart);
        chartPanel.setName(getTitle());

        // Remove border
        chartPanel.getChart().setBorderVisible(false);

        // Set size of chart
        chartPanel.setPreferredSize(new java.awt.Dimension(chartPanelWidth, chartPanelHeight));

        // Add more items to the context menu
        amendContextMenu();

        //Activate (arrow) keys
        addKeyBindings();
        return chartPanel;
    }

    /**
     * Change visible part of chart
     *
     * @param translationVector
     */
    private void moverOverPlot(XYDataItem translationVector) {
        double translatedDomainIntervalMin = chart.getXYPlot().getDomainAxis().getRange().getLowerBound() + translationVector.getX().doubleValue();
        double translatedDomainIntervalMax = chart.getXYPlot().getDomainAxis().getRange().getUpperBound() + translationVector.getX().doubleValue();
        double translatedRangeIntervalMin = chart.getXYPlot().getRangeAxis().getRange().getLowerBound() + translationVector.getY().doubleValue();
        double translatedRangeIntervalMax = chart.getXYPlot().getRangeAxis().getRange().getUpperBound() + translationVector.getY().doubleValue();

        Range domainAxisRange = new Range(translatedDomainIntervalMin, translatedDomainIntervalMax);
        Range rangeAxisRange = new Range(translatedRangeIntervalMin, translatedRangeIntervalMax);
        //We set notify to false in the first call..
        chart.getXYPlot().getDomainAxis().setRange(domainAxisRange, true, false);
        //...and true in the last
        chart.getXYPlot().getRangeAxis().setRange(rangeAxisRange, true, true);

    }

    /**
     * Add key bindings to chart panel
     */
    private void addKeyBindings() {
        final String moveUpKey = "move up";
        final String moveDownKey = "move down";
        final String moveRightKey = "move right";
        final String moveLeftKey = "move left";

        // Up arrow
        chartPanel.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), moveUpKey);
        chartPanel.getActionMap().put(moveUpKey, new AbstractAction() {
            // Default serial id
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                moverOverPlot(new XYDataItem(0.0, ((NumberAxis) chart.getXYPlot().getRangeAxis()).getTickUnit().getSize()));
            }
        });

        // Down arrow
        chartPanel.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), moveDownKey);
        chartPanel.getActionMap().put(moveDownKey, new AbstractAction() {
            // Default serial id
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                moverOverPlot(new XYDataItem(0.0, -((NumberAxis) chart.getXYPlot().getRangeAxis()).getTickUnit().getSize()));
            }
        });

        // Right arrow
        chartPanel.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), moveRightKey);
        chartPanel.getActionMap().put(moveRightKey, new AbstractAction() {
            // Default serial id
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                moverOverPlot(new XYDataItem(((NumberAxis) chart.getXYPlot().getDomainAxis()).getTickUnit().getSize(), 0.0));
            }
        });

        // Left arrow
        chartPanel.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), moveLeftKey);
        chartPanel.getActionMap().put(moveLeftKey, new AbstractAction() {
            // Default serial id
            private static final long serialVersionUID = 1L;

            @Override
            public void actionPerformed(ActionEvent e) {
                moverOverPlot(new XYDataItem(-((NumberAxis) chart.getXYPlot().getDomainAxis()).getTickUnit().getSize(), 0.0));
            }
        });
    }

    @Override
    protected void onAxisRangeChanged(AxisId axis_id) {
        ValueAxis axis = null;

        switch (axis_id) {
            case X:
                axis = chart.getXYPlot().getDomainAxis();
                break;
            case Y:
                axis = chart.getXYPlot().getRangeAxis();
                break;
            default:
                return;
        }
        if (getAxis(axis_id).isAutoRange()) {
            axis.setAutoRange(true);
        } else {
            axis.setRange(new Range(getAxis(axis_id).getMin(), getAxis(axis_id).getMax()), true, true);
        }
    }

    /**
     * Add additional items to the context menu of the plot
     */
    public void amendContextMenu() {

        chartPanel.getPopupMenu().addSeparator();

        JMenu toolsMenu = new JMenu("Tools");
        chartPanel.getPopupMenu().add(toolsMenu);

        // Mark average
        JMenuItem averageMenuItem = new JMenuItem("Average");
        averageMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (LinePlotSeries series : getAllSeries()) {
                    double a = series.getAverage();
                    // Remove all range markers
                    Collection<?> c = chartPanel.getChart().getXYPlot().getRangeMarkers(Layer.FOREGROUND);
                    if (c != null) {
                        for (final Object marker : c) {
                            SwingUtilities.invokeLater(new Runnable() {	// Avoid concurrent modification exception
                                @Override
                                public void run() {
                                    chartPanel.getChart().getXYPlot().removeRangeMarker((Marker) marker);
                                }
                            });

                        }
                    }

                    final ValueMarker marker = new ValueMarker(a, Color.BLACK, new BasicStroke(1));
                    marker.setLabel("Average [" + series.getName() + "]: " + String.format("%.6f", a));
                    marker.setLabelAnchor(RectangleAnchor.CENTER);
                    marker.setLabelTextAnchor(TextAnchor.TOP_RIGHT);

                    SwingUtilities.invokeLater(new Runnable() {	// Avoid concurrent modification exception
                        @Override
                        public void run() {
                            chartPanel.getChart().getXYPlot().addRangeMarker(marker, Layer.FOREGROUND);
                        }
                    });
                }
            }
        });
        toolsMenu.add(averageMenuItem);

        // Mark minimum value
        JMenuItem minimumMenuItem = new JMenuItem("Minimum");
        minimumMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (LinePlotSeries series : getAllSeries()) {
                    double[] min = series.getMinimum();
//					System.out.println("Minimum: "+m.getXValue());
                    // Remove all annotation for the series
                    for (Object o : chartPanel.getChart().getXYPlot().getAnnotations()) {
                        chartPanel.getChart().getXYPlot().removeAnnotation((XYAnnotation) o);
                    }
                    XYDrawableAnnotation cross = new XYDrawableAnnotation(min[0], min[1], 10.0, 10.0, new CrossAnnotation(Color.BLACK));
                    cross.setToolTipText("Minimum: " + min[0] + " / " + min[1]);
                    chartPanel.getChart().getXYPlot().addAnnotation(cross);
                }
            }
        });
        toolsMenu.add(minimumMenuItem);

        // Mark maximum value
        JMenuItem maximumMenuItem = new JMenuItem("Maximum");
        maximumMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (LinePlotSeries series : getAllSeries()) {
                    double[] max = series.getMaximum();
                    // Remove all annotation for the series
                    for (Object o : chartPanel.getChart().getXYPlot().getAnnotations()) {
                        chartPanel.getChart().getXYPlot().removeAnnotation((XYAnnotation) o);
                    }
                    XYDrawableAnnotation cross = new XYDrawableAnnotation(max[0], max[1], 10.0, 10.0, new CrossAnnotation(Color.BLACK));
                    cross.setToolTipText("Maximum: " + max[0] + " / " + max[1]);
                    chartPanel.getChart().getXYPlot().addAnnotation(cross);
                }
            }
        });
        toolsMenu.add(maximumMenuItem);

        // Show derivative
        JMenuItem derivativeMenuItem = new JMenuItem("Derivative");
        derivativeMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                LinePlot p = new LinePlot();
                p.setTitle("Derivative - " + getTitle());
                for (LinePlotSeries series : getAllSeries()) {
                    double[][] derivative = series.getDerivative();
                    LinePlotSeries dseries = new LinePlotSeries(series.getName() + "'");
                    p.addSeries(dseries);
                    dseries.setData(derivative[0], derivative[1]);
                }
                JFrame frame = new JFrame();
                frame.setContentPane(p.getChartPanel());
                frame.pack();
                RefineryUtilities.centerFrameOnScreen(frame);
                frame.setVisible(true);
            }
        });
        toolsMenu.add(derivativeMenuItem);

        // Show integral
        JMenuItem integralMenuItem = new JMenuItem("Integral");
        integralMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // Show new frame
                LinePlot p = new LinePlot();
                p.setTitle("Integral - " + getTitle());
                for (LinePlotSeries series : getAllSeries()) {
                    double[][] integral = series.getIntegral();
                    LinePlotSeries dseries = new LinePlotSeries("integral-" + series.getName());
                    p.addSeries(dseries);
                    dseries.setData(integral[0], integral[1]);
                }
                JFrame frame = new JFrame();
                frame.setContentPane(p.getChartPanel());
                frame.pack();
                RefineryUtilities.centerFrameOnScreen(frame);
                frame.setVisible(true);
            }
        });
        toolsMenu.add(integralMenuItem);

        /*
         chartPanel.getPopupMenu().addSeparator();
         // Detach plot into a separate frame
         JMenuItem detachPlotMenuItem = new JMenuItem("Detach");
         detachPlotMenuItem.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent e) {
         LinePlot p = new LinePlot();
         p.setTitle(getTitle());
         for (LinePlotSeries series : LinePlot.this.getAllSeries()) {
         LinePlotSeries detachedSeries = new LinePlotSeries(series.getName(), series.getColor());
         p.addSeries(detachedSeries);
         detachedSeries.setData(series.getX(), series.getY());
         }

         JFrame frame = new JFrame();
         frame.setContentPane(p.getChartPanel());
         frame.pack();
         RefineryUtilities.centerFrameOnScreen(frame);
         frame.setVisible(true);
         }
         });
         chartPanel.getPopupMenu().add(detachPlotMenuItem);
         */
        // Hide/Show legend
        JMenuItem hideLegendMenuItem = new JMenuItem(showLegendMenuLabel);
        hideLegendMenuItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JMenuItem source = (JMenuItem) (e.getSource());

                if (source.getText() == showLegendMenuLabel) {
                    chart.getLegend().setVisible(true);
                    source.setText(hideLegendMenuLabel);
                } else if (source.getText() == hideLegendMenuLabel) {
                    chart.getLegend().setVisible(false);
                    source.setText(showLegendMenuLabel);
                }
            }
        });
        chartPanel.getPopupMenu().add(hideLegendMenuItem);

        // Hide/Show tooltips
        JMenuItem hideToolTipsMenuItem = new JMenuItem(hideTooltipsMenuLabel);
        if (!tooltips) {
            hideToolTipsMenuItem.setText(showTooltipsMenuLabel);
        }
        hideToolTipsMenuItem.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                JMenuItem source = (JMenuItem) (e.getSource());
                if (source.getText() == showTooltipsMenuLabel) {
                    showTooltips();
                    source.setText(hideTooltipsMenuLabel);
                } else if (source.getText() == hideTooltipsMenuLabel) {
                    hideTooltips();
                    source.setText(showTooltipsMenuLabel);
                }
            }
        });
        chartPanel.getPopupMenu().add(hideToolTipsMenuItem);

    }

    private void showTooltips() {
        tooltips = true;
        DecimalFormat dm = new DecimalFormat("0.##########");
        XYToolTipGenerator xYToolTipGenerator = new StandardXYToolTipGenerator("{1}/{2}", dm, dm);
        chart.getXYPlot().getRenderer().setBaseToolTipGenerator(xYToolTipGenerator);
        chartPanel.setDisplayToolTips(true);
        chartPanel.getChartRenderingInfo().setEntityCollection(new StandardEntityCollection());
    }

    private void hideTooltips() {
        tooltips = false;
        // http://www.jfree.org/phpBB2/viewtopic.php?t=12788&highlight=redraw+speed+performance+problem
        chartPanel.getChartRenderingInfo().setEntityCollection(null);
        chart.getXYPlot().getRenderer().setBaseToolTipGenerator(null);
    }

    protected void addPopupMenuItem(final JMenuItem item) {
        if (item == null) {
            chartPanel.getPopupMenu().addSeparator();
        } else {
            chartPanel.getPopupMenu().add(item);
        }
    }
    /*
     @Override
     protected void doUpdate() 
     if (data != null) {
     SwingUtilities.invokeLater(new Runnable() {	// Try to avoid concurrent modification exception
     @Override
     public void run() {
     try {
     for (XYSeries s : data.getSeries()) {
     s.fireSeriesChanged();
     }
     } catch (ConcurrentModificationException e) {
     logger.log(Level.WARNING, "Series got modified concurrently: " + e.getMessage(), e);
     }
     }
     });
     }
     }
     */
    /*
     //Trash
     @Override
     public void setData(Object data) {
     super.setData(data);
     this.data = (XYSeriesCollection) data;
     }
     */
}
