/*
 * JChart.java
 *
 * Copyright (c) 2002-2015 Alexei Drummond, Andrew Rambaut and Marc Suchard
 *
 * This file is part of BEAST.
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership and licensing.
 *
 * BEAST is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 *  BEAST is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with BEAST; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

package dr.app.gui.chart;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Vector;

public class JChart extends JPanel {

    /**
     *
     */
    private static final long serialVersionUID = -7064065852204509247L;
    protected Axis yAxis, xAxis;
    private Vector<Plot> plots = new Vector<Plot>();

    private Paint plotBackgroundPaint = Color.white;

    private Stroke originStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
    private Paint originPaint = Color.lightGray;

    private Stroke frameStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
    private Paint framePaint = Color.black;

    private Stroke axisStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER);
    private Paint axisPaint = Color.black;

    private Font labelFont = new Font("Helvetica", Font.PLAIN, 12);
    private Paint labelPaint = Color.black;

    private Rectangle2D plotBounds = null;

    private Rectangle2D dragRectangle = null;

    private double majorTickSize = 4;
    private double minorTickSize = 2;

    private double xTickLabelOffset;
    private double yTickLabelOffset;

    private boolean showLegend = false;
    private int legendAlignment = SwingConstants.NORTH_EAST;

    private double xScale, yScale, xOffset, yOffset;

    public JChart(Axis xAxis, Axis yAxis) {
        this(null, xAxis, yAxis);
    }

    public JChart(Plot plot, Axis xAxis, Axis yAxis) {

        setOpaque(false);

        this.xAxis = xAxis;
        this.yAxis = yAxis;

        if (plot != null)
            addPlot(plot);

        addMouseListener(new MListener());
        addMouseMotionListener(new MMListener());

    }

    public void setXAxis(Axis xAxis) {
        this.xAxis = xAxis;
        for (Object plot : plots) {
            Plot p = (Plot) plot;
            p.setAxes(xAxis, yAxis);
        }
        recalibrate();
        repaint();
    }

    public Axis getXAxis() {
        return xAxis;
    }

    public void setYAxis(Axis yAxis) {
        this.yAxis = yAxis;
        for (Plot p : plots) {
            p.setAxes(xAxis, yAxis);
        }
        recalibrate();
        repaint();
    }

    public Axis getYAxis() {
        return yAxis;
    }

    public void setFontSize(int size) {
        labelFont = new Font("Helvetica", Font.PLAIN, size);
    }

    public void addPlot(Plot plot) {
        plot.setAxes(xAxis, yAxis);
        plots.add(plot);
        recalibrate();
        repaint();
    }

    public void removePlot(Plot plot) {
        plots.remove(plot);
        xAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
        yAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
        for (Plot p : plots) {
            p.setAxes(xAxis, yAxis);
        }
        recalibrate();
        repaint();
    }

    public void removeAllPlots() {
        plots.clear();
        xAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
        yAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
        recalibrate();
        repaint();
    }

    public int getPlotCount() {
        return plots.size();
    }

    public Plot getPlot(int index) {
        return plots.get(index);
    }

    public Rectangle2D getDragRectangle() {
        return dragRectangle;
    }

    public void setDragRectangle(Rectangle2D dragRectangle) {
        this.dragRectangle = dragRectangle;
        repaint();
    }

    public void selectPoints(Rectangle2D dragRectangle, boolean addToSelection) {
        for (Plot plot : plots) {
            plot.selectPoints(dragRectangle, addToSelection);
        }
        repaint();
    }

    public void clearSelection() {
        for (Plot plot : plots) {
            plot.clearSelection();
        }
        repaint();
    }

    private void resetPlots() {
//		xAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
//		yAxis.setRange(Double.POSITIVE_INFINITY,Double.NEGATIVE_INFINITY);
        for (Plot p : plots) {
            p.resetAxes();
        }
    }

    /**
     *	Set origin style. Use a stroke of null to not draw origin
     */
    public void setOriginStyle(Stroke originStroke, Paint originPaint) {
        this.originStroke = originStroke;
        this.originPaint = originPaint;
    }

    /**
     *	Set axis style.
     */
    public void setAxisStyle(Stroke axisStroke, Paint axisPaint) {
        this.axisStroke = axisStroke;
        this.axisPaint = axisPaint;
    }

    /**
     *	Get axis stroke.
     */
    public Stroke getAxisStroke() {
        return axisStroke;
    }

    /**
     *	Get axis paint.
     */
    public Paint getAxisPaint() {
        return axisPaint;
    }

    /**
     *	Set frame style. Use a stroke of null to not draw frame
     */
    public void setFrameStyle(Stroke frameStroke, Paint framePaint) {
        this.frameStroke = frameStroke;
        this.framePaint = framePaint;
    }

    /**
     *	Set label style.
     */
    public void setLabelStyle(Font labelFont, Paint labelPaint) {
        this.labelFont = labelFont;
        this.labelPaint = labelPaint;
    }

    /**
     *	Get label font.
     */
    public Font getLabelFont() {
        return labelFont;
    }

    /**
     *	Get label paint.
     */
    public Paint getLabelPaint() {
        return labelPaint;
    }

    /**
     *	Set legend on/off.
     */
    public void setShowLegend(boolean showLegend) {
        this.showLegend = showLegend;
        repaint();
    }

    /**
     *	Set legend alignment (use SwingConstants.NORTH_EAST etc.).
     */
    public void setLegendAlignment(int alignment) {
        this.legendAlignment = alignment;
        repaint();
    }

    public Dimension getMinimumSize() {
        return new Dimension(128, 128);
    }

    public Rectangle2D getPlotBounds() {
        return plotBounds;
    }

    public double getMajorTickSize() {
        return majorTickSize;
    }

    public double getMinorTickSize() {
        return minorTickSize;
    }

    public double getXTickLabelOffset() {
        return xTickLabelOffset;
    }

    public double getYTickLabelOffset() {
        return yTickLabelOffset;
    }

    private boolean calibrated = true;
    public void recalibrate() { calibrated = false; }
    protected void calibrate(Graphics2D g2, Dimension size) {
        resetPlots();
    }
    protected boolean hasContents() {
        return plots.size() > 0;
    }

    public void paintComponent(Graphics g) {

        if (!hasContents()) return;

        Graphics2D g2 = (Graphics2D)g;
        Dimension size = getSize();

//        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        try {

            if (!calibrated) {
                calibrate(g2, size);
                calibrated = true;
            }

            g2.setFont(labelFont);

            double tickLabelHeight = g2.getFontMetrics().getHeight();

            xTickLabelOffset = g2.getFontMetrics().getAscent();
            yTickLabelOffset = g2.getFontMetrics().getAscent() / 2;

            double maxXTickLabelWidth = getMaxTickLabelWidth(g2, xAxis);
            double maxYTickLabelWidth = getMaxTickLabelWidth(g2, yAxis);

            double w = size.width - (majorTickSize * 1.25) - maxYTickLabelWidth - (maxXTickLabelWidth / 2);
            double h = size.height - yTickLabelOffset - (majorTickSize * 1.25) - tickLabelHeight;

            plotBounds = new Rectangle2D.Double((majorTickSize * 1.25) + maxYTickLabelWidth, yTickLabelOffset, w, h);

            xOffset = plotBounds.getX();
            yOffset = plotBounds.getMaxY();
            xScale = w / (xAxis.transform(xAxis.getMaxAxis()) - xAxis.transform(xAxis.getMinAxis()));
            yScale = -h / (yAxis.transform(yAxis.getMaxAxis()) - yAxis.transform(yAxis.getMinAxis()));

            g2.setPaint(plotBackgroundPaint);
            g2.fill(plotBounds);
            g2.setClip(plotBounds);

            Stroke oldStroke = g2.getStroke();

            if (originStroke != null && originPaint != null) {
                double minX = xAxis.getMinAxis();
                double maxX = xAxis.getMaxAxis();
                double minY = yAxis.getMinAxis();
                double maxY = yAxis.getMaxAxis();

                g2.setPaint(originPaint);
                g2.setStroke(originStroke);

                if (minX<0.0 && maxX>0.0) {
                    Line2D line = new Line2D.Double(transformX(0.0), transformY(minY),
                            transformX(0.0), transformY(maxY));
                    g2.draw(line);
                }

                if (minY<0.0 && maxY>0.0) {
                    Line2D line = new Line2D.Double(transformX(minX), transformY(0.0),
                            transformX(maxX), transformY(0.0));
                    g2.draw(line);
                }
            }
            g2.setStroke(oldStroke);

            paintContents(g2);

            if (showLegend) {
                paintLegend(g2);
            }

            if (dragRectangle != null) {
                g2.setPaint(new Color(128, 128, 128, 128));
                g2.fill(dragRectangle);
            }

            g2.setClip(null);

            paintFrame(g2);

            paintAxis(g2, xAxis, true);
            paintAxis(g2, yAxis, false);
        } catch (ChartRuntimeException cre) {
            // ignore (just won't paint chart)
        }

    }

    protected void paintContents(Graphics2D g2) {
        for (Plot plot : plots) {
            plot.paintPlot(g2, xScale, yScale, xOffset, yOffset);
        }
    }

    protected void paintFrame(Graphics2D g2) {
        g2.setPaint(framePaint);
        g2.setStroke(frameStroke);
        g2.draw(plotBounds);
    }


    /**
     *	Transform a chart co-ordinates into a drawing co-ordinates
     */
    protected double transformX(double value) {
        return ((xAxis.transform(value) - xAxis.transform(xAxis.getMinAxis())) * xScale) + xOffset;
    }

    /**
     *	Transform a chart co-ordinates into a drawing co-ordinates
     */
    protected double transformY(double value) {
        return ((yAxis.transform(value) - yAxis.transform(yAxis.getMinAxis())) * yScale) + yOffset;
    }

    /*
     *	Transform a drawing co-ordinate into a chart co-ordinate
      * /
     private double untransformX(double value) {

         return xAxis.untransform(
                         xAxis.transform(xAxis.getMinAxis()) + ((value - xOffset) / xScale));
     }*/

    /*
     *	Transform a drawing co-ordinate into a chart co-ordinate
      * /
     private double untransformY(double value) {

         return yAxis.untransform(
                         yAxis.transform(yAxis.getMinAxis()) + ((value - yOffset) / yScale));
     }*/

    protected void paintLegend(Graphics2D g2)
    {
        float width = 0;
        int itemCount = 0;

        for (int i = 0; i < getPlotCount(); i++) {
            String name = getPlot(i).getName();
            if (name != null) {
                float w = (float)g2.getFontMetrics().stringWidth(name);
                if (width < w) {
                    width = w;
                }
                itemCount++;
            }
        }

        if (width == 0) return; // no plots have names so just return

        width += 32;
        float height = (float)(g2.getFontMetrics().getAscent() + 8) * itemCount;

        float x, y;

        if (legendAlignment == SwingConstants.NORTH_WEST ||
                legendAlignment == SwingConstants.WEST ||
                legendAlignment == SwingConstants.SOUTH_WEST) {
            x = (float)(plotBounds.getX() + 8);
        } else if (legendAlignment == SwingConstants.NORTH_EAST ||
                legendAlignment == SwingConstants.EAST ||
                legendAlignment == SwingConstants.SOUTH_EAST) {
            x = (float)(plotBounds.getX() + plotBounds.getWidth() - width - 8);
        } else { // centered
            x = (float)(plotBounds.getX() + ((plotBounds.getWidth() - width) / 2));
        }

        if (legendAlignment == SwingConstants.NORTH_WEST ||
                legendAlignment == SwingConstants.NORTH ||
                legendAlignment == SwingConstants.NORTH_EAST) {
            y = (float)(plotBounds.getY() + 8);
        } else if (legendAlignment == SwingConstants.SOUTH_EAST ||
                legendAlignment == SwingConstants.SOUTH ||
                legendAlignment == SwingConstants.SOUTH_WEST) {
            y = (float)(plotBounds.getY() + plotBounds.getHeight() - height - 8);
        } else { // centered
            y = (float)(plotBounds.getY() + ((plotBounds.getHeight() - height) / 2));
        }

        Rectangle2D legendBounds = new Rectangle2D.Float(x, y, width, height);
        g2.setPaint(framePaint);
        g2.setStroke(frameStroke);
        g2.draw(legendBounds);

        x += 8;

        float iy = 8 + g2.getFontMetrics().getAscent();
        y += g2.getFontMetrics().getAscent() + 4;
        for (int i = 0; i < getPlotCount(); i++) {
            Plot plot = getPlot(i);
            String name = plot.getName();
            if (name != null) {

                g2.setPaint(plot.getLineColor());
                g2.fill(new Rectangle2D.Float(x, y - 8, 8, 8));

                g2.setPaint(framePaint);
                g2.drawString(name, x + 16, y);

                y += iy;
            }
        }

    }

    /**
     *	Get the maximum width of the labels of an axis
     */
    protected double getMaxTickLabelWidth(Graphics2D g2, Axis axis)
    {
        String label;
        double width;
        double maxWidth = 0;

        if (axis.getLabelFirst()) { // Draw first minor tick as a major one (with a label)
            label = axis.format(axis.getMinorTickValue(0, -1));
            width = g2.getFontMetrics().stringWidth(label);
            if (maxWidth < width)
                maxWidth = width;
        }
        int n = axis.getMajorTickCount();
        for (int i = 0; i < n; i++) {
            label = axis.format(axis.getMajorTickValue(i));
            width = g2.getFontMetrics().stringWidth(label);
            if (maxWidth < width)
                maxWidth = width;
        }
        if (axis.getLabelLast()) { // Draw first minor tick as a major one (with a label)
            label = axis.format(axis.getMinorTickValue(0, n - 1));
            width = g2.getFontMetrics().stringWidth(label);
            if (maxWidth < width)
                maxWidth = width;
        }

        return maxWidth;
    }

    protected void paintAxis(Graphics2D g2, Axis axis, boolean horizontalAxis)
    {
        int n1 = axis.getMajorTickCount();
        int n2, i, j;

        n2 = axis.getMinorTickCount(-1);
        if (axis.getLabelFirst()) { // Draw first minor tick as a major one (with a label)

            paintMajorTick(g2, axis.getMinorTickValue(0, -1), horizontalAxis);

            for (j = 1; j < n2; j++) {
                paintMinorTick(g2, axis.getMinorTickValue(j, -1), horizontalAxis);
            }
        } else {

            for (j = 0; j < n2; j++) {
                paintMinorTick(g2, axis.getMinorTickValue(j, -1), horizontalAxis);
            }
        }

        for (i = 0; i < n1; i++) {

            paintMajorTick(g2, axis.getMajorTickValue(i), horizontalAxis);
            n2 = axis.getMinorTickCount(i);

            if (i == (n1-1) && axis.getLabelLast()) { // Draw last minor tick as a major one

                paintMajorTick(g2, axis.getMinorTickValue(0, i), horizontalAxis);

                for (j = 1; j < n2; j++) {
                    paintMinorTick(g2, axis.getMinorTickValue(j, i), horizontalAxis);
                }
            } else {

                for (j = 0; j <  n2; j++) {
                    paintMinorTick(g2, axis.getMinorTickValue(j, i), horizontalAxis);
                }
            }
        }
    }

    protected void paintMajorTick(Graphics2D g2, double value, boolean horizontalAxis)
    {
        g2.setPaint(axisPaint);
        g2.setStroke(axisStroke);
        if (horizontalAxis) {
            String label = xAxis.format(value);
            double pos = transformX(value);

            Line2D line = new Line2D.Double(pos, plotBounds.getMaxY(), pos, plotBounds.getMaxY() + majorTickSize);
            g2.draw(line);

            g2.setPaint(labelPaint);
            double width = g2.getFontMetrics().stringWidth(label);
            g2.drawString(label, (float)(pos - (width / 2)), (float)(plotBounds.getMaxY() + (majorTickSize * 1.25) + xTickLabelOffset));
        } else {
            String label = yAxis.format(value);
            double pos = transformY(value);

            Line2D line = new Line2D.Double(plotBounds.getMinX(), pos, plotBounds.getMinX() - majorTickSize, pos);
            g2.draw(line);

            g2.setPaint(labelPaint);
            double width = g2.getFontMetrics().stringWidth(label);
            g2.drawString(label, (float)(plotBounds.getMinX() - width - (majorTickSize * 1.25)), (float)(pos + yTickLabelOffset));
        }
    }

    protected void paintMinorTick(Graphics2D g2, double value, boolean horizontalAxis)
    {

        g2.setPaint(axisPaint);
        g2.setStroke(axisStroke);
        if (horizontalAxis) {
            double pos = transformX(value);

            Line2D line = new Line2D.Double(pos, plotBounds.getMaxY(), pos, plotBounds.getMaxY() + minorTickSize);
            g2.draw(line);
        } else {
            double pos = transformY(value);

            Line2D line = new Line2D.Double(plotBounds.getMinX(), pos, plotBounds.getMinX() - minorTickSize, pos);
            g2.draw(line);
        }
    }

//	private Point2D currentPoint = null;

    public class MMListener extends MouseMotionAdapter {

        public void mouseMoved(MouseEvent me) {
//			currentPoint = me.getPoint();
        }

        public void mouseDragged(MouseEvent me) {

//			selectionEnd = boundedPoint(me.getPoint(), getSize());
//			currentPoint = realPoint(selectionEnd);

//			repaint(200);
        }
    }

    public class MListener extends MouseAdapter {

        Point2D start, finish;

        public void mouseExited(MouseEvent me) {
//			currentPoint = null;
        }

        public void mousePressed(MouseEvent me) {
        }


        public void mouseReleased(MouseEvent me) {
        }

        public void mouseClicked(MouseEvent me) {

            if (plotBounds != null && plotBounds.contains(me.getPoint())) {
                for (Plot plot : plots) {
                    plot.pointClicked(me.getPoint(), me.isShiftDown());
                }


            }
        }
    }
}
