package org.esa.snap.rcp.statistics;

import com.bc.ceres.core.Assert;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.annotations.XYShapeAnnotation;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.ui.RectangleEdge;

import java.awt.Color;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;

import static java.lang.Math.abs;
import static java.lang.Math.min;

/**
 * @author Norman Fomferra
 */
public class PlotAreaSelectionTool extends MouseAdapter {
    public enum AreaType {
        /**
         * Shape is an instance of {@link java.awt.geom.Rectangle2D}, only X-coordinates are valid.
         */
        X_RANGE,
        /**
         * Shape is an instance of {@link java.awt.geom.Rectangle2D}, only Y-coordinates are valid.
         */
        Y_RANGE,
        /**
         * Shape is an instance of {@link java.awt.geom.Rectangle2D}.
         */
        RECTANGLE,
        /**
         * Shape is an instance of {@link java.awt.geom.Ellipse2D}.
         */
        ELLIPSE,
    }

    public interface Action {
        void areaSelected(AreaType areaType, Shape shape);
    }

    private final ChartPanel chartPanel;
    private final Action action;

    private Point point1;
    private Point point2;
    private SelectedArea selectedArea;
    private double triggerDistance;
    private Color fillPaint;
    private AreaType areaType;

    public PlotAreaSelectionTool(ChartPanel chartPanel, Action action) {
        this.chartPanel = chartPanel;
        this.action = action;
        triggerDistance = 4;
        fillPaint = new Color(0, 0, 255, 50);
        areaType = AreaType.ELLIPSE;
    }

    public void install() {
        chartPanel.addMouseListener(this);
        chartPanel.addMouseMotionListener(this);
        chartPanel.setMouseZoomable(false);
    }

    public void uninstall() {
        chartPanel.removeMouseListener(this);
        chartPanel.removeMouseMotionListener(this);
        chartPanel.setMouseZoomable(true);
    }

    public AreaType getAreaType() {
        return areaType;
    }

    public void setAreaType(AreaType areaType) {
        Assert.notNull(areaType, "areaType");
        this.areaType = areaType;
    }

    public double getTriggerDistance() {
        return triggerDistance;
    }

    public void setTriggerDistance(double triggerDistance) {
        this.triggerDistance = triggerDistance;
    }

    public Color getFillPaint() {
        return fillPaint;
    }

    public void setFillPaint(Color fillPaint) {
        Assert.notNull(fillPaint, "fillPaint");
        this.fillPaint = fillPaint;
    }

    @Override
    public void mousePressed(MouseEvent event) {
        if (!isButton1(event)) {
            return;
        }
        point1 = event.getPoint();
        point2 = null;
    }

    @Override
    public void mouseReleased(MouseEvent event) {
        if (!isButton1(event)) {
            return;
        }
        if (selectedArea == null) {
            return;
        }
        // Make sure, action is only triggered if a new area has been selected
        if (point1 == null || point2 == null) {
            return;
        }
        action.areaSelected(areaType, selectedArea.getShape());
        // Ready for a new area to be selected, but the existing area remains visible
        point1 = null;
        point2 = null;
    }

    @Override
    public void mouseDragged(MouseEvent event) {
        if (point1 == null) {
            return;
        }

        if (point2 == null) {
            // first drag event after mousePressed -->
            // then we must check against triggerDistance
            Point p2 = event.getPoint();
            if (Point.distanceSq(point1.getX(), point1.getY(), p2.getX(), p2.getY()) >= triggerDistance * triggerDistance) {
                point2 = p2;
                updateAnnotation();
            }
        } else {
            // already dragging, just update
            point2 = event.getPoint();
            updateAnnotation();
        }
    }

    private void updateAnnotation() {
        removeAnnotation();
        addAnnotation();
    }

    private void addAnnotation() {
        selectedArea = new SelectedArea(createShape(), fillPaint);
        chartPanel.getChart().getXYPlot().addAnnotation(selectedArea);
    }

    public void removeAnnotation() {
        if (selectedArea != null) {
            chartPanel.getChart().getXYPlot().removeAnnotation(selectedArea);
            selectedArea = null;
        }
    }

    private boolean isButton1(MouseEvent event) {
        return event.getButton() == MouseEvent.BUTTON1;
    }

    private Shape createShape() {
        XYPlot plot = chartPanel.getChart().getXYPlot();

        Rectangle2D dataArea = chartPanel.getScreenDataArea();
        PlotOrientation orientation = plot.getOrientation();
        RectangleEdge domainEdge = Plot.resolveDomainAxisLocation(plot.getDomainAxisLocation(), orientation);
        RectangleEdge rangeEdge = Plot.resolveRangeAxisLocation(plot.getRangeAxisLocation(), orientation);

        double vx1 = areaType == AreaType.Y_RANGE ? dataArea.getX() : point1.x;
        double vy1 = areaType == AreaType.X_RANGE ? dataArea.getY() : point1.y;

        double vx2 = areaType == AreaType.Y_RANGE ? dataArea.getX() + dataArea.getWidth() : point2.x;
        double vy2 = areaType == AreaType.X_RANGE ? dataArea.getY() + dataArea.getHeight() : point2.y;

        double x1 = plot.getDomainAxis().java2DToValue(vx1, dataArea, domainEdge);
        double x2 = plot.getDomainAxis().java2DToValue(vx2, dataArea, domainEdge);
        double y1 = plot.getRangeAxis().java2DToValue(vy1, dataArea, rangeEdge);
        double y2 = plot.getRangeAxis().java2DToValue(vy2, dataArea, rangeEdge);

        double dx = abs(x2 - x1);
        double dy = abs(y2 - y1);

        final Shape shape;
        if (areaType == AreaType.ELLIPSE) {
            shape = new Ellipse2D.Double(x1 - dx, y1 - dy, 2.0 * dx, 2.0 * dy);
        } else if (areaType == AreaType.RECTANGLE) {
            shape = new Rectangle2D.Double(x1 - dx, y1 - dy, 2.0 * dx, 2.0 * dy);
        } else if (areaType == AreaType.X_RANGE || areaType == AreaType.Y_RANGE) {
            shape = new Rectangle2D.Double(min(x1, x2), min(y1, y2), dx, dy);
        } else {
            throw new IllegalStateException("areaType = " + areaType);
        }

        return shape;
    }


    private static class SelectedArea extends XYShapeAnnotation  {
        private final Shape shape;
        private SelectedArea(Shape shape, Paint fillPaint) {
            super(shape, null, null, fillPaint);
            this.shape = shape;
        }

        // Base class does not off this method :-(
        public Shape getShape() {
            return shape;
        }
    }

}