package ch.psi.plot.jfree;

import ch.psi.plot.MatrixPlotBase;
import ch.psi.plot.MatrixPlotSeries;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.text.DecimalFormat;
import java.util.Arrays;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.event.AxisChangeEvent;
import org.jfree.chart.event.AxisChangeListener;
import org.jfree.chart.labels.StandardXYZToolTipGenerator;
import org.jfree.chart.labels.XYZToolTipGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.LookupPaintScale;
import org.jfree.chart.renderer.PaintScale;
import org.jfree.chart.renderer.xy.XYBlockRenderer;
import org.jfree.chart.title.PaintScaleLegend;
import org.jfree.data.Range;
import org.jfree.data.xy.DefaultXYZDataset;
import org.jfree.ui.RectangleAnchor;
import org.jfree.ui.RectangleEdge;
import org.jfree.ui.RectangleInsets;

/**
 * Returns a matrix plot panel
 */
public class MatrixPlot extends MatrixPlotBase {

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

    private boolean grayScale = false;

    private MatrixPlotSeries series;
    private DefaultXYZDataset data;
    private JFreeChart chart;
    private ChartPanel chartPanel;

    double[] xvalues = new double[0];
    double[] yvalues = new double[0];
    double[] zvalues = new double[0];

    /**
     * Constructor
     *
     * @param title
     * @param data
     */
    XYPlot plot;
    final NumberAxis xAxis, yAxis;
    final XYBlockRenderer renderer;

    public MatrixPlot() {
        super();
        // Create matrix chart
        // Init size of plot area according to min and max set in the datas metadata
        // Set axis: (plus half bin size on both sides), since we plot the bins centered.
        xAxis = new NumberAxis("");
        yAxis = new NumberAxis("");

        // Configure block renderer to have the blocks rendered in the correct size
        renderer = new XYBlockRenderer();
        renderer.setBaseCreateEntities(false);
        plot = new XYPlot(null, xAxis, yAxis, renderer);

        // Remove background paint/color
        plot.setBackgroundPaint(null);

        // Set the maximum zoom out to the initial zoom rate This also 
        // provides a workaround for dynamic plots because there zoom out does not work correctly (zoom out to infinity) 
        plot.getRangeAxis().addChangeListener(new AxisChangeListener() {
            @Override
            public void axisChanged(AxisChangeEvent event) {
                if (series == null) {
                    return;
                }
                ValueAxis axis = ((ValueAxis) event.getAxis());
                if (axis.getLowerBound() < yMin || axis.getUpperBound() > yMax) {
                    Range range = new Range(yMin, yMax);
                    axis.setRange(range, true, false);
                }
            }
        });
        plot.getDomainAxis().addChangeListener(new AxisChangeListener() {
            @Override
            public void axisChanged(AxisChangeEvent event) {
                if (series == null) {
                    return;
                }
                ValueAxis axis = ((ValueAxis) event.getAxis());
                if (axis.getLowerBound() < xMin || axis.getUpperBound() > xMax) {
                    Range range = new Range(xMin, xMax);
                    axis.setRange(range, true, false);
                }
            }
        });

    }

    double xMin, xMax, yMin, yMax;

    @Override
    protected Object onAddedSeries(MatrixPlotSeries series) {
        this.series = series;
        int arraylength = series.getNumberOfBinsX() * series.getNumberOfBinsY();
        xvalues = new double[arraylength];
        yvalues = new double[arraylength];
        zvalues = new double[arraylength];
        Arrays.fill(xvalues, Double.NaN);
        Arrays.fill(yvalues, Double.NaN);
        Arrays.fill(zvalues, Double.NaN);
        double[][] dataArray = new double[][]{xvalues, yvalues, zvalues};
        //Create the XYDataset (org.jfree), not to be confused with the ch.psi dataSet)
        data = new DefaultXYZDataset();
        data.addSeries("Data", dataArray);

        onAxisRangeChanged(AxisId.X);
        onAxisRangeChanged(AxisId.Y);
        xAxis.setLabel(getAxis(AxisId.X).getLabel());
        yAxis.setLabel(getAxis(AxisId.Y).getLabel());

        renderer.setBlockWidth(series.getBinWidthX()); // If this is not set the default block size is 1
        renderer.setBlockHeight(series.getBinWidthY()); // If this is not set the default block size is 1
        renderer.setBlockAnchor(RectangleAnchor.CENTER);

        // Remove background paint/color
        plot.setBackgroundPaint(null);

        plot.setDataset(data);

        return data;
    }

    @Override
    protected void onAxisRangeChanged(AxisId axis_id) {
        // The axis min and max values need to be half a bin larger. Otherwise the outer pixels
        // will not be plotted correctly (half of the pixel is missing)        
        if (axis_id == AxisId.X) {
            if (getAxis(AxisId.X).isAutoRange()) {
                xMin = series.getMinX() - 0.5 * series.getBinWidthX();
                xMax = series.getMaxX() + 0.5 * series.getBinWidthX();
            } else {
                xMin = getAxis(AxisId.X).getMin();
                xMax = getAxis(AxisId.X).getMax();
            }
            xAxis.setLowerBound(xMin);
            xAxis.setUpperBound(xMax);
        }
        if (axis_id == AxisId.Y) {
            if (getAxis(AxisId.Y).isAutoRange()) {
                yMin = series.getMinY() - 0.5 * series.getBinWidthY();
                yMax = series.getMaxY() + 0.5 * series.getBinWidthY();
            } else {
                yMin = getAxis(AxisId.Y).getMin();
                yMax = getAxis(AxisId.Y).getMax();
            }
            yAxis.setLowerBound(yMin);
            yAxis.setUpperBound(yMax);
        }
    }

    @Override
    protected void onRemovedSeries(MatrixPlotSeries s) {
        series = null;
        doUpdate();
    }

    @Override
    protected void onAppendData(MatrixPlotSeries series, int indexX, int indexY, double x, double y, double z) {
        final int index = indexY * series.getNumberOfBinsX() + indexX;
        //x Value is column, y value is row
        xvalues[index] = x;
        yvalues[index] = y;
        zvalues[index] = z;
    }

    @Override
    protected void onSetData(MatrixPlotSeries series, double[][] data) {
        int index = 0;
        for (int y = 0; y < data.length; y++) {
            for (int x = 0; x < data[0].length; x++) {
                //final int index = y * series.getNumberOfBinsX() + x;
                xvalues[index] = series.getMinX() + series.getBinWidthX() * x;
                yvalues[index] = series.getMinY() + series.getBinWidthY() * y;
                zvalues[index++] = data[y][x];
            }
        }
    }

    @Override
    public void clear() {
        if (series != null) {
            removeSeries(series);
            doUpdate();
        }
    }

    @Override
    public double[][] getSeriesData(MatrixPlotSeries s) {
        int x = series.getNumberOfBinsX();
        int y = series.getNumberOfBinsY();
        double[][] ret = new double[y][x];
        int index = 0;
        for (int i = 0; i < y; i++) {
            for (int j = 0; j < x; j++) {
                ret[i][j] = data.getZValue(0, index++);
            }
        }
        return ret;
    }

    // Action Classes that implement methods called after keyboard input
    /**
     * Adapt the context menu of the chart panel
     */
    private void adaptContextMenu(final ChartPanel chartPanel) {

        // Show hide tooltips
        final String tooltipsMenuLabel = "Tooltips";
        final String hideTooltipsMenuLabel = "Hide Tooltips";
        JMenuItem contextMenuItemToolTip = new JMenuItem(tooltipsMenuLabel);
        contextMenuItemToolTip.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                JMenuItem source = (JMenuItem) (e.getSource());
                if (source.getText() == tooltipsMenuLabel) {
                    showTooltip();
                    source.setText(hideTooltipsMenuLabel);
                } else if (source.getText() == hideTooltipsMenuLabel) {
                    hideTooltip();
                    source.setText(tooltipsMenuLabel);
                }
            }
        });
        chartPanel.getPopupMenu().add(contextMenuItemToolTip);

        // Update colormap
        JMenuItem contextMenuItemAdaptScale = new JMenuItem("Update Scale");
        contextMenuItemAdaptScale.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                adaptColorMapScale();
            }
        });

        // Set gray scale colormap
        JMenuItem contextMenuItemGrayScale = new JMenuItem("Gray Scale");
        contextMenuItemGrayScale.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                grayScale = true;
                adaptColorMapScale();
            }
        });

        // Set color colormap
        JMenuItem contextMenuItemTemperatureColor = new JMenuItem("Color Scale");
        contextMenuItemTemperatureColor.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                grayScale = false;
                adaptColorMapScale();
            }
        });

        // Set color colormap
        JMenuItem menuItemScale = new JMenuItem("Manual Scale");
        menuItemScale.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                ManualScaleDialog d = new ManualScaleDialog();
                d.setLocationRelativeTo(chartPanel);
                d.setLow(((PaintScaleLegend) chart.getSubtitles().get(0)).getScale().getLowerBound());
                d.setHigh(((PaintScaleLegend) chart.getSubtitles().get(0)).getScale().getUpperBound());
                d.setMatrixPlot(MatrixPlot.this);

                d.showDialog();
                if (d.getSelectedOption() == JOptionPane.OK_OPTION) {
                    setColorScale(d.getLow(), d.getHigh());
                }
            }
        });

        // Group colormap related menu items
        JMenuItem contextMenuChooseColorMap = new JMenu("ColorMap");
        contextMenuChooseColorMap.add(contextMenuItemAdaptScale);
        contextMenuChooseColorMap.add(menuItemScale);
        contextMenuChooseColorMap.add(contextMenuItemGrayScale);
        contextMenuChooseColorMap.add(contextMenuItemTemperatureColor);
        chartPanel.getPopupMenu().add(contextMenuChooseColorMap);
        /*   
         // Add separator
         chartPanel.getPopupMenu().addSeparator();

     
         // Detach plot into an own frame
         JMenuItem contextMenuItemDetach = new JMenuItem("Detach Plot");
         contextMenuItemDetach.addActionListener(new ActionListener() {
         @Override
         public void actionPerformed(ActionEvent e) {
         MatrixPlot p = new MatrixPlot();
         p.setTitle(title);
         p.setSeries(series);

         final JFrame frame = new JFrame(title);
         frame.getContentPane();
         frame.setContentPane(p.getChartPanel());
         frame.pack();
         frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // Close application if frame is closed

         // Set color scale
         p.adaptColorMapScale();

         java.awt.EventQueue.invokeLater(new Runnable() {
         public void run() {
         frame.setVisible(true);
         }
         });
         }
         });
         chartPanel.getPopupMenu().add(contextMenuItemDetach);
         */
    }

    /**
     * Show Tooltips. This is not done per default since it makes the app slow
     * for datasize >= 1M
     */
    private void showTooltip() {
        //Tooltips are quit expensive
        DecimalFormat dm = new DecimalFormat("0.##########");
        XYZToolTipGenerator xYZToolTipGenerator = new StandardXYZToolTipGenerator("{0}: ({1} / {2} / {3})", dm, dm, dm);

        chart.getXYPlot().getRenderer().setBaseToolTipGenerator(xYZToolTipGenerator);
        ((XYBlockRenderer) chart.getXYPlot().getRenderer()).setBaseCreateEntities(true);
    }

    /**
     * Clear Tooltips
     */
    private void hideTooltip() {
        //Tooltips are quit expensive
        ((XYBlockRenderer) chart.getXYPlot().getRenderer()).setBaseToolTipGenerator(null);
        ((XYBlockRenderer) chart.getXYPlot().getRenderer()).setBaseCreateEntities(false);

    }

    /**
     * Adapt the lower and upper color map scale to the min and max data values
     * of the currently selected region Need to be called AFTER the chart panel
     * is created
     */
    public void adaptColorMapScale() {
        double[] v = series.minMaxZValue();
        if (v[0] != Double.NaN && v[1] != Double.NaN && v[0] < v[1]) {
            setColorScale(v[0], v[1]);
        }
    }

    /**
     * Set the min and max of color map scale to scaleMin and scaleMax
     *
     * @param scaleMin	min value of scale
     * @param scaleMax	max value of scale
     */
    public void setColorScale(double scaleMin, double scaleMax) {

        // Create scale for legend
        LookupPaintScale legendScale = new LookupPaintScale(scaleMin, scaleMax, Color.GRAY);
        setScaleColors(legendScale, scaleMin, scaleMax);

        // Create legend
        PaintScaleLegend legend = new PaintScaleLegend(legendScale, new NumberAxis());
        legend.setPadding(new RectangleInsets(5, 5, 5, 5));
        // Width of the legend (colorbar)
        legend.setStripWidth(20);
        // Position of the legend
        legend.setPosition(RectangleEdge.RIGHT);
        // Remove background paint/color
        legend.setBackgroundPaint(null);

        // Add legend to plot panel
        chart.clearSubtitles();
        chart.addSubtitle(legend);

        //We need a second scale for rendering only, where the 'infinities' are in correct color 
        LookupPaintScale rendererScale = new LookupPaintScale(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Color.GRAY);
        setScaleColors(rendererScale, scaleMin, scaleMax);
        // Make the paint scale range from - to + infinity
        if (grayScale) {
            rendererScale.add(Double.NEGATIVE_INFINITY, ColorManager.getGrayForValue(scaleMin, scaleMin, scaleMax));
            rendererScale.add(Double.POSITIVE_INFINITY, ColorManager.getGrayForValue(scaleMax, scaleMin, scaleMax));
        } else {
            rendererScale.add(Double.NEGATIVE_INFINITY, ColorManager.getColourForValue(scaleMin, scaleMin, scaleMax));
            rendererScale.add(Double.POSITIVE_INFINITY, ColorManager.getColourForValue(scaleMax, scaleMin, scaleMax));
        }
        ((XYBlockRenderer) chart.getXYPlot().getRenderer()).setPaintScale(rendererScale);
    }

    /**
     * Set the colors for the colored Paint Scale (either single color or
     * temperature color)
     *
     * @param paintScale
     * @param scaleMin
     * @param scaleMax
     */
    private void setScaleColors(PaintScale paintScale, double scaleMin, double scaleMax) {
        if (grayScale) {
            for (int i = 0; i < 256; i++) {
                double value = scaleMin + (i / 255.0) * (scaleMax - scaleMin);
                ((LookupPaintScale) paintScale).add(value, ColorManager.getGrayForValue(value, scaleMin, scaleMax));
            }
        } else {
            for (int i = 0; i < 256; i++) {
                double value = scaleMin + (i / 255.0) * (scaleMax - scaleMin);
                ((LookupPaintScale) paintScale).add(value, ColorManager.getColourForValue(value, scaleMin, scaleMax));
            }
        }
    }

    /**
     * Get the chart panel
     *
     * @return
     */
    @Override
    protected JPanel createChartPanel() {
        chart = new JFreeChart(getTitle(), plot);

        //Remove the series label (also called legend)
        chart.removeLegend();
                    //AntiAliasing is used to speed up rendering
//			chart.setAntiAlias(false);

        // Init PaintScale
        setColorScale(0, 1);

        //Create the Chartpanel where the chart will be plotted
        chartPanel = new ChartPanel(chart);

        chartPanel.setPreferredSize(new java.awt.Dimension(chartPanelWidth, chartPanelHeight));

        //All interactive menu items
        adaptContextMenu(chartPanel);
        return chartPanel;
    }

    /* (non-Javadoc)
     * @see ch.psi.plot.Plot#update()
     */
    @Override
    public void doUpdate() {
        if (chart != null) {
            chart.fireChartChanged();
            adaptColorMapScale();
        }
    }

    @Override
    public void updateSeries(MatrixPlotSeries series) {
        doUpdate();
    }

    protected void addPopupMenuItem(final JMenuItem item) {
        if (item == null) {
            chartPanel.getPopupMenu().addSeparator();
        } else {
            chartPanel.getPopupMenu().add(item);
        }
    }

	//public JFreeChart getChart() {
    //	return chart;
    //}
    //@Override
    //public void setData(Object data){
    //    super.setData(data);
    //    this.data=(MatrixPlotSeries) data;
    //}
}
