/*
 * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option)
 * any later version.
 * This program 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 General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, see http://www.gnu.org/licenses/
 */

package org.esa.snap.rcp.actions.interactors;

import com.bc.ceres.glayer.swing.LayerCanvas;
import com.bc.ceres.grender.Rendering;
import com.bc.ceres.grender.Viewport;
import com.bc.ceres.swing.figure.ViewportInteractor;
import org.esa.snap.core.datamodel.GeoCoding;
import org.esa.snap.core.datamodel.GeoPos;
import org.esa.snap.core.datamodel.PixelPos;
import org.esa.snap.core.dataop.maptransf.Ellipsoid;
import org.esa.snap.core.util.math.MathUtils;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.Dialogs;
import org.esa.snap.ui.ModalDialog;
import org.esa.snap.ui.product.ProductSceneView;
import org.openide.util.ImageUtilities;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import java.awt.BasicStroke;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Stroke;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

/**
 * A tool representing the range finder.
 *
 * @author Sabine Embacher
 * @author Ralf Quast
 * @author Marco Zuehlke
 */
class RangeFinderInteractor extends ViewportInteractor {

    public static final String TITLE = "Range Finder Tool";

    private static class ModelPoint extends Point2D.Double {

        private static ModelPoint create(Viewport viewport, Point point) {
            return new ModelPoint(viewport.getViewToModelTransform().transform(point, new Double()));
        }

        private ModelPoint() {
            super();
        }

        private ModelPoint(Point2D p) {
            super(p.getX(), p.getY());
        }

        private Point toViewPoint(Viewport viewport) {
            return (Point) viewport.getModelToViewTransform().transform(this, new Point());
        }
    }

    private final List<ModelPoint> modelPointList;
    private final ModelPoint currentModelPoint;
    private final Cursor cursor;

    private RangeFinderOverlay overlay;

    public RangeFinderInteractor() {
        modelPointList = new ArrayList<>();
        currentModelPoint = new ModelPoint();
        ImageIcon cursorIcon = ImageUtilities.loadImageIcon("org/esa/snap/rcp/cursors/RangeFinder.gif", false);
        cursor = createRangeFinderCursor(cursorIcon);
    }

    @Override
    public Cursor getCursor() {
        return cursor;
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        handleDragAndMove(e);
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        handleDragAndMove(e);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        final ProductSceneView view = getProductSceneView(e);
        if (view == null) {
            return;
        }
        if (overlay != null && view != overlay.view) {
            removeOverlay();
        }
        if (overlay == null) {
            createOverlay(view);
        }
        if (e.getClickCount() == 1) {
            final Point viewPoint = e.getPoint();
            final ModelPoint modelPoint = ModelPoint.create(view.getViewport(), viewPoint);
            modelPointList.add(modelPoint);
            currentModelPoint.setLocation(modelPoint);
            overlay.repaint();
        }
        if (e.getClickCount() == 2 && modelPointList.size() > 0) {
            showDetailsDialog(view);
            modelPointList.clear();
            removeOverlay();
        }
    }

    private void handleDragAndMove(MouseEvent e) {
        if (modelPointList.size() > 0 && overlay != null) {
            final ProductSceneView view = getProductSceneView(e);
            if (view != null) {
                final Point viewPoint = e.getPoint();
                final ModelPoint modelPoint = ModelPoint.create(view.getViewport(), viewPoint);
                currentModelPoint.setLocation(modelPoint);
                overlay.repaint();
            }
        }
    }

    private void createOverlay(ProductSceneView view) {
        overlay = new RangeFinderOverlay(view);
        view.getLayerCanvas().addOverlay(overlay);
    }

    private void removeOverlay() {
        overlay.view.getLayerCanvas().removeOverlay(overlay);
        overlay = null;
    }

    private ProductSceneView getProductSceneView(MouseEvent event) {
        final Component eventComponent = event.getComponent();
        if (eventComponent instanceof ProductSceneView) {
            return (ProductSceneView) eventComponent;
        }
        final Container parentComponent = eventComponent.getParent();
        if (parentComponent instanceof ProductSceneView) {
            return (ProductSceneView) parentComponent;
        }
        // Case: Scroll bars are displayed
        if (parentComponent.getParent() instanceof ProductSceneView) {
            return (ProductSceneView) parentComponent.getParent();
        }
        return null;
    }

    private void showDetailsDialog(ProductSceneView view) {
        //todo [multisize_products] ask for scenerastertransform instead of geocoding
        GeoCoding geoCoding = view.getRaster().getGeoCoding();
        if (geoCoding == null) {
            Dialogs.showInformation(TITLE, String.format("No geo-coding information for %s.", view.getRaster().getName()), null);
            return;
        }

        float distance = 0;
        float distanceError = 0;
        final AffineTransform m2i = view.getBaseImageLayer().getModelToImageTransform();

        final Point imagePoint1 = new Point();
        final Point imagePoint2 = new Point();
        final DistanceData[] distanceData = new DistanceData[modelPointList.size() - 1];
        for (int i = 0; i < distanceData.length; i++) {
            m2i.transform(modelPointList.get(i), imagePoint1);
            m2i.transform(modelPointList.get(i + 1), imagePoint2);

            final DistanceData segmentData = new DistanceData(geoCoding, imagePoint1, imagePoint2);
            distance += segmentData.distance;
            distanceError += segmentData.distanceError;
            distanceData[i] = segmentData;
        }


        final JButton detailsButton = new JButton("Details...");
        detailsButton.addActionListener(e -> {
            final Window parentWindow = SwingUtilities.getWindowAncestor(detailsButton);
            createDetailsDialog(parentWindow, distanceData).show();
        });

        final JPanel buttonPane = new JPanel(new BorderLayout());
        buttonPane.add(detailsButton, BorderLayout.EAST);

        final JPanel messagePane = new JPanel(new BorderLayout(0, 6));
        messagePane.add(new JLabel("Distance: " + distance + " +/- " + distanceError + " km"));
        messagePane.add(buttonPane, BorderLayout.SOUTH);

        JOptionPane.showMessageDialog(SnapApp.getDefault().getMainFrame(), messagePane,
                                      TITLE,
                                      JOptionPane.INFORMATION_MESSAGE);
    }

    private static ModalDialog createDetailsDialog(final Window parentWindow, final DistanceData[] dds) {
        float distance = 0;
        float distanceError = 0;

        final StringBuilder message = new StringBuilder();
        for (int i = 0; i < dds.length; i++) {
            final DistanceData dd = dds[i];
            distance += dd.distance;
            distanceError += dd.distanceError;
            message.append(
                    "Distance between points " + i + " to " + (i + 1) + " in pixels:\n" +
                    "XH[" + dd.xH + "] to XN[" + dd.xN + "]: " + Math.abs(dd.xH - dd.xN) + "\n" +
                    "YH[" + dd.yH + "] to YN[" + dd.yN + "]: " + Math.abs(dd.yH - dd.yN) + "\n" +
                    "\n" +
                    "LonH: " + dd.lonH + "   LatH: " + dd.latH + "\n" +
                    "LonN: " + dd.lonN + "   LatN: " + dd.latN + "\n" +
                    "\n" +
                    "LamH: " + dd.lamH + "   PhiH: " + dd.phiH + "\n" +
                    "LamN: " + dd.lamN + "   PhiN: " + dd.phiN + "\n" +
                    "\n" +
                    "Mean earth radius used: " + DistanceData.MEAN_EARTH_RADIUS_KM + " km" + "\n" +
                    "\n" +
                    "Distance: " + dd.distance + " +/- " + dd.distanceError + " km\n" +
                    "\n\n"
            );
        }
        message.insert(0, "Total distance: " + distance + " +/- " + distanceError + " km\n" +
                          "\n" +
                          "computed as described below:\n\n");

        final JScrollPane content = new JScrollPane(new JTextArea(message.toString()));
        content.setPreferredSize(new Dimension(300, 150));

        final ModalDialog detailsWindow = new ModalDialog(parentWindow, TITLE + " - Details", ModalDialog.ID_OK, null);
        detailsWindow.setContent(content);
        return detailsWindow;
    }

    private static Cursor createRangeFinderCursor(ImageIcon cursorIcon) {
        Toolkit defaultToolkit = Toolkit.getDefaultToolkit();
        final String cursorName = "rangeFinder";

        // this is necessary because on some systems the cursor is scaled but not the 'hot spot'
        Dimension bestCursorSize = defaultToolkit.getBestCursorSize(cursorIcon.getIconWidth(), cursorIcon.getIconHeight());
        Point hotSpot = new Point((7 * bestCursorSize.width) / cursorIcon.getIconWidth(),
                                  (7 * bestCursorSize.height) / cursorIcon.getIconHeight());

        return defaultToolkit.createCustomCursor(cursorIcon.getImage(), hotSpot, cursorName);
    }

    private class RangeFinderOverlay implements LayerCanvas.Overlay {

        private final ProductSceneView view;

        RangeFinderOverlay(ProductSceneView view) {
            this.view = view;
        }

        void repaint() {
            view.getLayerCanvas().repaint();
        }

        @Override
        public void paintOverlay(LayerCanvas canvas, Rendering rendering) {
            if (modelPointList.size() == 0) {
                return;
            }
            Graphics2D g2d = rendering.getGraphics();
            final Stroke strokeOld = g2d.getStroke();
            g2d.setStroke(new BasicStroke(1.0f));
            final Color colorOld = g2d.getColor();
            g2d.setColor(Color.white);
            g2d.translate(0.5, 0.5);

            final int r = 3;
            final int r2 = r * 2;

            Point viewPoint1 = null;
            Point viewPoint2 = null;
            final Viewport viewport = canvas.getViewport();
            for (final ModelPoint modelPoint : modelPointList) {
                viewPoint1 = modelPoint.toViewPoint(viewport);

                g2d.drawOval(viewPoint1.x - r, viewPoint1.y - r, r2, r2);
                g2d.drawLine(viewPoint1.x, viewPoint1.y - r2, viewPoint1.x, viewPoint1.y - r);
                g2d.drawLine(viewPoint1.x, viewPoint1.y + r2, viewPoint1.x, viewPoint1.y + r);
                g2d.drawLine(viewPoint1.x - r2, viewPoint1.y, viewPoint1.x - r, viewPoint1.y);
                g2d.drawLine(viewPoint1.x + r2, viewPoint1.y, viewPoint1.x + r, viewPoint1.y);

                if (viewPoint2 != null) {
                    g2d.drawLine(viewPoint1.x, viewPoint1.y, viewPoint2.x, viewPoint2.y);
                }

                viewPoint2 = viewPoint1;
            }
            if (viewPoint1 != null) {
                viewPoint2 = currentModelPoint.toViewPoint(canvas.getViewport());
                g2d.drawLine(viewPoint1.x, viewPoint1.y, viewPoint2.x, viewPoint2.y);
            }

            g2d.translate(-0.5, -0.5);
            g2d.setStroke(strokeOld);
            g2d.setColor(colorOld);
        }
    }

    private static class DistanceData {

        final static double MIN_EARTH_RADIUS = Ellipsoid.WGS_84.getSemiMinor();
        final static double MAX_EARTH_RADIUS = Ellipsoid.WGS_84.getSemiMajor();
        final static double MEAN_EARTH_RADIUS_M = 6371000;
        final static double MEAN_EARTH_RADIUS_KM = MEAN_EARTH_RADIUS_M * 0.001;
        final static double MEAN_ERROR_FACTOR = MIN_EARTH_RADIUS / MAX_EARTH_RADIUS;

        final int xH;
        final int yH;
        final int xN;
        final int yN;
        final double lonH;
        final double latH;
        final double lonN;
        final double latN;
        final double lamH;
        final double phiH;
        final double lamN;
        final double phiN;
        final double distance;
        final double distanceError;

        public DistanceData(GeoCoding geoCoding, final Point pH, final Point pN) {
            this.xH = pH.x;
            this.yH = pH.y;
            this.xN = pN.x;
            this.yN = pN.y;
            final GeoPos geoPosH = geoCoding.getGeoPos(new PixelPos(xH, yH), null);
            final GeoPos geoPosN = geoCoding.getGeoPos(new PixelPos(xN, yN), null);
            this.lonH = geoPosH.getLon();
            this.latH = geoPosH.getLat();
            this.lonN = geoPosN.getLon();
            this.latN = geoPosN.getLat();
            this.lamH = (MathUtils.DTOR * lonH);
            this.phiH = (MathUtils.DTOR * latH);
            this.lamN = (MathUtils.DTOR * lonN);
            this.phiN = (MathUtils.DTOR * latN);
            this.distance = MathUtils.sphereDistance(MEAN_EARTH_RADIUS_KM, lamH, phiH, lamN, phiN);
            this.distanceError = distance * (1 - MEAN_ERROR_FACTOR);
        }
    }
}
