/*
 * Plot.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 dr.stats.Variate;

import java.awt.*;
import java.awt.geom.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Plot.java
 * <p/>
 * Description:	Provides an interface for plots. Plots are elements of a
 * chart that renders some data in a particular style. Multiple plots can
 * be added to the same chart to get complex results.
 *
 * @author Andrew Rambaut
 * @author Alexei Drummond
 * @version $Id: Plot.java,v 1.23 2006/01/03 10:26:20 rambaut Exp $
 */

public interface Plot {

    // These constants are used for automatic scaling to select exactly
    // where the axis starts and stops.
    static public final int NO_MARK = 0;
    static public final int POINT_MARK = 1;
    static public final int CROSS_MARK = 2;
    static public final int PLUS_MARK = 3;
    static public final int CIRCLE_MARK = 4;
    static public final int SQUARE_MARK = 5;
    static public final int DIAMOND_MARK = 6;

    /**
     * Set axes
     */
    void setAxes(Axis xAxis, Axis yAxis);

    /**
     * Resets axis ranges (if new data has been added)
     */
    void resetAxes();

    /**
     * Set data
     */
    void setData(List<Double> xData, List<Double> yData);

    /**
     * Set data
     */
    void setData(Variate.N xData, Variate.N yData);

    /**
     * Set line style
     */
    void setLineStyle(Stroke lineStroke, Paint linePaint);

    /**
     * Set line stroke
     */
    void setLineStroke(Stroke lineStroke);

    /**
     * Set line color
     */
    void setLineColor(Paint linePaint);

    /**
     * Get line color
     */
    Paint getLineColor();

    /**
     * Get line stroke
     */
    Stroke getLineStroke();

    /**
     * Set mark style
     */
    void setMarkStyle(int markType, double markSize, Stroke markStroke,
                      Paint markPaint, Paint markFillPaint);

    /**
     * Set mark style
     */
    void setMarkStyle(Shape mark, Stroke markStroke,
                      Paint markPaint, Paint markFillPaint);

    /**
     * Paint actual plot
     */
    void paintPlot(Graphics2D g2, double xScale, double yScale,
                   double xOffset, double yOffset);

    /**
     * Set name
     */
    void setName(String name);

    /**
     * Get name
     */
    String getName();

    /**
     * A point on the plot has been clicked
     */
    void pointClicked(Point2D point, boolean isShiftDown);

    void selectPoint(int index, boolean addToSelection);

    void selectPoints(Rectangle2D dragRectangle, boolean addToSelection);

    void clearSelection();

    void setSelectedPoints(final Collection<Integer> selectedPoints);

    Set<Integer> getSelectedPoints();

    Variate getXData();

    Variate getYData();

    public interface Listener {

        void pointClicked(double x, double y, boolean isShiftDown);

        void markClicked(int index, double x, double y, boolean isShiftDown);

        void selectionChanged(Set<Integer> selectedPoints);

        void rangeXSelected(double lower, double upper);

        void rangeYSelected(double lower, double upper);

        void rangeXYSelected(double lowerX, double lowerY, double upperX, double upperY);

    }

    public class Adaptor implements Listener {
        public void pointClicked(double x, double y, boolean isShiftDown) {
        }

        public void markClicked(int index, double x, double y, boolean isShiftDown) {
        }

        public void selectionChanged(final Set<Integer> selectedPoints) {
        }

        public void rangeXSelected(double lower, double upper) {
        }

        public void rangeYSelected(double lower, double upper) {
        }

        public void rangeXYSelected(double lowerX, double lowerY, double upperX, double upperY) {
        }
    }

    /**
     * AbstractPlot.java
     * <p/>
     * Description:	An abstract base class for plots
     */
    public abstract class AbstractPlot implements Plot {

        protected Axis xAxis, yAxis;
        protected Variate.N xData = null;
        protected Variate.N yData = null;

        protected List<Color> colours = null;

        protected Shape mark;

        protected Stroke lineStroke = new BasicStroke(1.5f);
        protected Paint linePaint = Color.black;

        protected Stroke markStroke = new BasicStroke(0.5f);
        protected Paint markPaint = Color.black;
        protected Paint markFillPaint = Color.black;

        private final Rectangle2D bounds = null;

        private double xScale, yScale, xOffset, yOffset;

        private String name;

        private Set<Integer> selectedPoints = new HashSet<Integer>();

        /**
         * Constructor
         */
        public AbstractPlot() {
        }

        /**
         * Constructor
         */
        public AbstractPlot(Variate.N xData, Variate.N yData) {
            setData(xData, yData);
        }


        /**
         * Constructor
         */
        public AbstractPlot(List<Double> xData, List<Double> yData) {
            setData(xData, yData);
        }


        /**
         * Set data
         */
        public void setData(List<Double> xData, List<Double> yData) {
            Variate.D xd = new Variate.D(xData);
            Variate.D yd = new Variate.D(yData);

            this.xData = xd;
            this.yData = yd;
        }

        /**
         * Set data
         */
        public void setData(Variate.N xData, Variate.N yData) {
            this.xData = xData;
            this.yData = yData;
        }

        /**
         * Set data
         */
        public void setColours(List<Color> colours) {
            this.colours = colours;
        }

        /**
         * Set axes
         */
        public void setAxes(Axis xAxis, Axis yAxis) {
            this.xAxis = xAxis;
            this.yAxis = yAxis;
            setupAxis(xAxis, yAxis, xData, yData);
        }

        /**
         * Resets axis ranges (if new data has been added)
         */
        public void resetAxes() {
            setupAxis(xAxis, yAxis, xData, yData);
        }

        /**
         * Set up the axis with some data
         */
        public void setupAxis(Axis xAxis, Axis yAxis, Variate xData, Variate yData) {
            if (xData != null) {
                if (xAxis instanceof LogAxis) {
                    double minValue = java.lang.Double.POSITIVE_INFINITY;

                    for (int i = 0; i < xData.getCount(); i++) {
                        double value = (Double) xData.get(i);
                        if (value > 0.0 && value < minValue)
                            minValue = value;
                    }

                    xAxis.addRange(minValue, (Double) xData.getMax());
                } else {
                    xAxis.addRange((Double) xData.getMin(), (Double) xData.getMax());
                }
            }
            if (yData != null) {
                if (yAxis instanceof LogAxis) {
                    double minValue = java.lang.Double.POSITIVE_INFINITY;

                    for (int i = 0; i < yData.getCount(); i++) {
                        double value = (Double) yData.get(i);
                        if (value > 0.0 && value < minValue)
                            minValue = value;
                    }

                    yAxis.addRange(minValue, (Double) yData.getMax());
                } else {
                    yAxis.addRange((Double) yData.getMin(), (Double) yData.getMax());
                }
            }
        }

        /**
         * Set line style
         */
        public void setLineStyle(Stroke lineStroke, Paint linePaint) {
            this.lineStroke = lineStroke;
            this.linePaint = linePaint;
        }

        /**
         * Set line stroke
         */
        public void setLineStroke(Stroke lineStroke) {
            this.lineStroke = lineStroke;
        }

        /**
         * Set line color
         */
        public void setLineColor(Paint linePaint) {
            this.linePaint = linePaint;
        }

        public final Paint getLineColor() {
            return linePaint;
        }

        public final Stroke getLineStroke() {
            return lineStroke;
        }

        public final void setName(String name) {
            this.name = name;
        }

        public final String getName() {
            return name;
        }

        /**
         * Set mark style
         */
        public void setMarkStyle(int markType, double markSize, Stroke markStroke,
                                 Paint markPaint, Paint markFillPaint) {

            float w = (float) (markSize / 2.0);

            GeneralPath path;

            switch (markType) {
                case POINT_MARK:
                    path = new GeneralPath();
                    path.moveTo(0, 0);
                    path.lineTo(0, 0);
                    setMarkStyle(path, markStroke, markPaint, markFillPaint);
                    break;
                case CROSS_MARK:
                    path = new GeneralPath();
                    path.moveTo(-w, -w);
                    path.lineTo(w, w);
                    path.moveTo(w, -w);
                    path.lineTo(-w, w);
                    setMarkStyle(path, markStroke, markPaint, markFillPaint);
                    break;
                case PLUS_MARK:
                    path = new GeneralPath();
                    path.moveTo(-w, 0);
                    path.lineTo(w, 0);
                    path.moveTo(0, -w);
                    path.lineTo(0, w);
                    setMarkStyle(path, markStroke, markPaint, markFillPaint);
                    break;
                case CIRCLE_MARK:
                    setMarkStyle(new Ellipse2D.Double(0.0, 0.0, markSize, markSize), markStroke, markPaint, markFillPaint);
                    break;
                case SQUARE_MARK:
                    setMarkStyle(new Rectangle2D.Double(-w, -w, markSize, markSize), markStroke, markPaint, markFillPaint);
                    break;
                case DIAMOND_MARK:
                    path = new GeneralPath();
                    path.moveTo(0, -w);
                    path.lineTo(w, 0);
                    path.lineTo(0, w);
                    path.lineTo(-w, 0);
                    path.closePath();
                    setMarkStyle(path, markStroke, markPaint, markFillPaint);
                    break;
            }


        }

        /**
         * Set mark style
         */
        public void setMarkStyle(Shape mark, Stroke markStroke,
                                 Paint markPaint, Paint markFillPaint) {

            this.mark = mark;
            this.markStroke = markStroke;
            this.markPaint = markPaint;
            this.markFillPaint = markFillPaint;
        }

        public Variate getXData() {
            return xData;
        }

        public Variate getYData() {
            return yData;
        }

        /**
         * Transform a chart co-ordinates into a drawing co-ordinates
         */
        protected double transformX(double value) {
            double tx = xAxis.transform(value);
            if (tx == Double.NaN || tx == Double.NEGATIVE_INFINITY) {
                return Double.NEGATIVE_INFINITY;
            }
            return ((tx - xAxis.transform(xAxis.getMinAxis())) * xScale) + xOffset;
        }

        /**
         * Transform a chart co-ordinates into a drawing co-ordinates
         */
        protected double transformY(double value) {
            double ty = yAxis.transform(value);
            if (ty == Double.NaN || ty == Double.NEGATIVE_INFINITY) {
                return Double.NEGATIVE_INFINITY;
            }
            return ((ty - yAxis.transform(yAxis.getMinAxis())) * yScale) + yOffset;
        }

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

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

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

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

        /**
         * Draw a line transforming co-ordinates to each axis
         */
        protected void drawLine(Graphics2D g2, double x1, double y1, double x2, double y2) {
            Line2D line = new Line2D.Double(transformX(x1), transformY(y1),
                    transformX(x2), transformY(y2));
            g2.draw(line);
        }

        /**
         * Draw a rectangle transforming co-ordinates to each axis
         */
        protected void drawRect(Graphics2D g2, double x1, double y1, double x2, double y2) {

            float tx1 = (float) transformX(x1);
            float ty1 = (float) transformY(y1);
            float tx2 = (float) transformX(x2);
            float ty2 = (float) transformY(y2);

            GeneralPath path = new GeneralPath();
            path.moveTo(tx1, ty1);
            path.lineTo(tx1, ty2);
            path.lineTo(tx2, ty2);
            path.lineTo(tx2, ty1);
            path.closePath();
//			Rectangle2D rect = new Rectangle2D.Double(x, y,	w, h);
            g2.draw(path);
        }

        /**
         * Fill a rectangle transforming co-ordinates to each axis
         */
        protected void fillRect(Graphics2D g2, double x1, double y1, double x2, double y2) {

            float tx1 = (float) transformX(x1);
            float ty1 = (float) transformY(y1);
            float tx2 = (float) transformX(x2);
            float ty2 = (float) transformY(y2);

            GeneralPath path = new GeneralPath();
            path.moveTo(tx1, ty1);
            path.lineTo(tx1, ty2);
            path.lineTo(tx2, ty2);
            path.lineTo(tx2, ty1);
            path.closePath();
//			Rectangle2D rect = new Rectangle2D.Double(x, y,	w, h);
            g2.fill(path);
        }

        /**
         * Paint actual plot
         */
        public void paintPlot(Graphics2D g2, double xScale, double yScale,
                              double xOffset, double yOffset) {
            if (xAxis == null || yAxis == null)
                return;

            this.xScale = xScale;
            this.yScale = yScale;
            this.xOffset = xOffset;
            this.yOffset = yOffset;

            // variable is assigned to itself
            //this.bounds = bounds;

            if (xData != null && yData != null && xData.getCount() > 0)
                paintData(g2, xData, yData);
        }

        /**
         * Paint data series
         */
        abstract protected void paintData(Graphics2D g2, Variate.N xData, Variate.N yData);

        /**
         * A point on the plot has been clicked
         */
        public void pointClicked(Point2D point, boolean isShiftDown) {

            double x = untransformX(point.getX());
            double y = untransformY(point.getY());

            firePointClickedEvent(x, y, isShiftDown);
        }

        public void selectPoints(final Rectangle2D dragRectangle, final boolean addToSelection) {
            if (dragRectangle == null) {
                return;
            }

            if (!addToSelection) {
                selectedPoints.clear();
            }

            double x0 = untransformX(dragRectangle.getX());
            double y0 = untransformY(dragRectangle.getY() + dragRectangle.getHeight());

            double x1 = untransformX(dragRectangle.getX() + dragRectangle.getWidth());
            double y1 = untransformY(dragRectangle.getY());

            for (int i = 0; i < xData.getCount(); i ++) {
                double x = (Double) xData.get(i);
                double y = (Double) yData.get(i);

                if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
                    selectedPoints.add(i);
                }
            }

            fireSelectionChanged();
        }

        public void selectPoint(final int index, final boolean addToSelection) {

            if (!addToSelection) {
                selectedPoints.clear();
            }

            selectedPoints.add(index);

            fireSelectionChanged();
        }

        public void clearSelection() {
            selectedPoints.clear();
            fireSelectionChanged();
        }

        public void setSelectedPoints(final Collection<Integer> selectedPoints) {
            this.selectedPoints.clear();
            this.selectedPoints.addAll(selectedPoints);
        }


        public Set<Integer> getSelectedPoints() {
            return selectedPoints;
        }

        // Listeners

        private final java.util.Vector<Listener> listeners = new java.util.Vector<Listener>();

        /**
         * Add a plot listener
         */
        public void addListener(Listener listener) {
            listeners.add(listener);
        }

        /**
         * Tells plot listeners that a point has been clicked.
         */
        protected void firePointClickedEvent(double x, double y, boolean isShiftDown) {
            for (int i = 0; i < listeners.size(); i++) {
                final Listener listener = listeners.elementAt(i);
                listener.pointClicked(x, y, isShiftDown);
            }
        }

        /**
         * Tells plot listeners that a point has been clicked.
         */
        protected void fireMarkClickedEvent(int index, double x, double y, boolean isShiftDown) {
            for (int i=0; i < listeners.size(); i++) {
                final Listener listener = listeners.elementAt(i);
                listener.markClicked(index, x, y, isShiftDown);
            }
        }

        protected void fireSelectionChanged() {
            for (int i = 0; i < listeners.size(); i++) {
                final Listener listener = listeners.elementAt(i);
                listener.selectionChanged(selectedPoints);
            }
        }


    }
}

