// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.fr.cadastre.wms;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.GridBagLayout;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;

import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.projection.ProjectionRegistry;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.util.GuiHelper;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.Logging;

public class RasterImageGeoreferencer implements MouseListener {

    private int countMouseClicked = 0;
    private int mode = 0;
    private int cGetCorners = 1;
    private int cGetLambertCrosspieces = 2;
    private EastNorth ea1;
    private EastNorth ea2;
    private long mouseClickedTime = 0;
    private WMSLayer wmsLayer;
    private EastNorth georefpoint1;
    private EastNorth georefpoint2;
    private boolean ignoreMouseClick = false;
    private boolean clickOnTheMap = false;

    /**
     * The time which needs to pass between two clicks during georeferencing, in milliseconds
     */
    private int initialClickDelay;

    public void addListener() {
        MainApplication.getMap().mapView.addMouseListener(this);
    }

    /**
     *
     * @param wmsLayer WMS layer
     * @return false if all operations are canceled
     */
    public boolean startCropping(WMSLayer wmsLayer) {
        this.wmsLayer = wmsLayer;
        mode = cGetCorners;
        countMouseClicked = 0;
        initialClickDelay = Config.getPref().getInt("cadastrewms.georef-click-delay", 200);
        mouseClickedTime = System.currentTimeMillis();
        Object[] options = {"OK", "Cancel"};
        int ret = GuiHelper.runInEDTAndWaitAndReturn(() -> JOptionPane.showOptionDialog(null,
                tr("Click first corner for image cropping\n(two points required)"),
                tr("Image cropping"),
                JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
                null, options, options[0]));
        if (ret == JOptionPane.OK_OPTION) {
            mouseClickedTime = System.currentTimeMillis();
        } else
            if (canceledOrRestartCurrAction("image cropping"))
                return startCropping(wmsLayer);
        return true;
    }

    /**
    * @param wmsLayer WMS layer
    * @return false if all operations are canceled
    */
    public boolean startGeoreferencing(WMSLayer wmsLayer) {
        this.wmsLayer = wmsLayer;
        countMouseClicked = 0;
        mode = cGetLambertCrosspieces;
        initialClickDelay = Config.getPref().getInt("cadastrewms.georef-click-delay", 200);
        mouseClickedTime = System.currentTimeMillis();
        Object[] options = {"OK", "Cancel"};
        int ret = JOptionPane.showOptionDialog(null,
                tr("Click first Lambert crosspiece for georeferencing\n(two points required)"),
                tr("Image georeferencing"),
                JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
                null, options, options[0]);
        if (ret == JOptionPane.OK_OPTION) {
            mouseClickedTime = System.currentTimeMillis();
        } else
            if (canceledOrRestartCurrAction("georeferencing"))
                return startGeoreferencing(wmsLayer);
        return true;
    }

    public boolean isRunning() {
        return (countMouseClicked != 0 || mode != 0);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (System.currentTimeMillis() - mouseClickedTime < initialClickDelay) {
            Logging.info("mouse click bounce detected");
            return; // mouse click anti-bounce
        } else
            mouseClickedTime = System.currentTimeMillis();
        if (e.getButton() != MouseEvent.BUTTON1)
            return;
        if (ignoreMouseClick) return; // In case we are currently just allowing zooming to read lambert coordinates
        EastNorth ea = ProjectionRegistry.getProjection().latlon2eastNorth(MainApplication.getMap().mapView.getLatLon(e.getX(), e.getY()));
        Logging.info("click:"+countMouseClicked+" ,"+ea+", mode:"+mode);
        if (clickOnTheMap) {
            clickOnTheMap = false;
            handleNewCoordinates(ea.east(), ea.north());
        } else {
            // ignore clicks outside the image
            if (ea.east() < wmsLayer.getImage(0).min.east() || ea.east() > wmsLayer.getImage(0).max.east()
                    || ea.north() < wmsLayer.getImage(0).min.north() || ea.north() > wmsLayer.getImage(0).max.north()) {
                Logging.info("ignore click outside the image");
                return;
            }
            countMouseClicked++;
            if (mode == cGetCorners) {
                if (countMouseClicked == 1) {
                    ea1 = ea;
                    continueCropping();
                }
                if (countMouseClicked == 2) {
                    wmsLayer.cropImage(ea1, ea);
                    wmsLayer.invalidate();
                    startGeoreferencing(wmsLayer);
                }
            } else if (mode == cGetLambertCrosspieces) {
                if (countMouseClicked == 1) {
                    ea1 = ea;
                    inputLambertPosition(); // This will automatically asks for second point and continue the georeferencing
                }
                if (countMouseClicked == 2) {
                    ea2 = ea;
                    inputLambertPosition(); // This will automatically ends the georeferencing
                }
            }
        }
    }

    /**
    *
    * @return false if all operations are canceled
    */
    private boolean canceledOrRestartCurrAction(String action) {
        Object[] options = {"Cancel", "Retry"};
        int selectedValue = JOptionPane.showOptionDialog(null,
            tr("Do you want to cancel completely\n"+
                "or just retry "+action+" ?"), "",
            JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
            null, options, options[0]);
        countMouseClicked = 0;
        if (selectedValue == 0) { // "Cancel"
            // remove layer
            MainApplication.getLayerManager().removeLayer(wmsLayer);
            wmsLayer = null;
            MainApplication.getMap().mapView.removeMouseListener(this);
            return false;
        }
        return true;
    }

    /**
    * Use point org1 as anchor for scale, then move org1 to dst1, then rotate org2 on dst2
    * around org1/dst1 anchor
    * @param org1 first point at original coordinate system (the grabbed image)
    * @param org2 second point
    * @param dst1 first point at final destination coordinate system (the real east/north coordinate system)
    * @param dst2 second point
    */
    private void affineTransform(EastNorth org1, EastNorth org2, EastNorth dst1, EastNorth dst2) {
        // handle an NPE case I'm not able to reproduce
        if (org1 == null || org2 == null || dst1 == null || dst2 == null) {
            JOptionPane.showMessageDialog(MainApplication.getMainFrame(),
                    tr("Ooops. I failed to catch all coordinates\n"+
                        "correctly. Retry please."));
            Logging.warn("failed to transform: one coordinate missing:"
                        +"org1="+org1+", org2="+org2+", dst1="+dst1+", dst2="+dst2);
            return;
        }
        double angle = dst1.heading(dst2) - org1.heading(org2);
        double proportion = dst1.distance(dst2)/org1.distance(org2);
        // move
        double dx = dst1.getX() - org1.getX();
        double dy = dst1.getY() - org1.getY();
        wmsLayer.getImage(0).shear(dx, dy);
        // rotate : dst1 is anchor for rotation and scale
        wmsLayer.getImage(0).rotate(dst1, angle);
        // scale image from anchor dst1
        wmsLayer.getImage(0).scale(dst1, proportion);
    }

    /**
    * Ends the georeferencing by computing the affine transformation
    */
    private void endGeoreferencing() {
        MainApplication.getMap().mapView.removeMouseListener(this);
        affineTransform(ea1, ea2, georefpoint1, georefpoint2);
        wmsLayer.grabThread.saveNewCache();
        wmsLayer.invalidate();
        actionCompleted();
        clickOnTheMap = false;
        ignoreMouseClick = false;
    }

    private void inputLambertPosition() {
        JLabel labelEnterPosition = new JLabel(
                tr("Enter cadastre east,north position"));
        JLabel labelWarning = new JLabel(
                tr("(Warning: verify north with arrow !!)"));
        JPanel p = new JPanel(new GridBagLayout());
        JLabel labelEast = new JLabel(tr("East"));
        JLabel labelNorth = new JLabel(tr("North"));
        final JTextField inputEast = new JTextField();
        final JTextField inputNorth = new JTextField();
        p.add(labelEnterPosition, GBC.eol());
        p.add(labelWarning, GBC.eol());
        p.add(labelEast, GBC.std().insets(0, 0, 10, 0));
        p.add(inputEast, GBC.eol().fill(GBC.HORIZONTAL).insets(10, 5, 0, 5));
        p.add(labelNorth, GBC.std().insets(0, 0, 10, 0));
        p.add(inputNorth, GBC.eol().fill(GBC.HORIZONTAL).insets(10, 5, 0, 5));
        final Object[] options = {tr("OK"),
                tr("Cancel"),
                tr("I use the mouse")};
        final JOptionPane pane = new JOptionPane(p,
                JOptionPane.INFORMATION_MESSAGE, JOptionPane.YES_NO_CANCEL_OPTION,
                null, options, options[0]);
        String number;
        if (countMouseClicked == 1)
            number = "first";
        else
            number = "second";
        JDialog dialog = pane.createDialog(MainApplication.getMainFrame(), tr(
                "Set {0} Lambert coordinates", number));
        dialog.setModal(false);
        // To avoid mouseClicked from being called during coordinates reading
        ignoreMouseClick = true;
        dialog.setAlwaysOnTop(true);
        dialog.setVisible(true);
        pane.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (JOptionPane.VALUE_PROPERTY.equals(evt.getPropertyName())) {
                    ignoreMouseClick = false;
                    // Cancel
                    if (pane.getValue().equals(options[1])) {
                        if (canceledOrRestartCurrAction("georeferencing"))
                            startGeoreferencing(wmsLayer);
                    }
                    // Click on the map
                    if (pane.getValue().equals(options[2])) {
                        clickOnTheMap = true;
                    } else {
                        // OK (coordinates manually entered)
                        clickOnTheMap = false;
                        if (inputEast.getText().length() != 0
                                && inputNorth.getText().length() != 0) {
                            double e, n;
                            try {
                                e = Double.parseDouble(inputEast.getText());
                                n = Double.parseDouble(inputNorth.getText());
                            } catch (NumberFormatException ex) {
                                return;
                            }
                            handleNewCoordinates(e, n);
                        }
                    }
                }
            }
        });
    }

    /**
    * Ask if cropping should be continued
    * @return false if all operations are canceled
    */
    private boolean continueCropping() {
        Object[] options = {"OK", "Cancel"};
        int ret = JOptionPane.showOptionDialog(null,
                tr("Click second corner for image cropping"),
                tr("Image cropping"),
                JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
                null, options, options[0]);
        if (ret != JOptionPane.OK_OPTION) {
            if (canceledOrRestartCurrAction("image cropping"))
                return startCropping(wmsLayer);
        }
        return true;
    }

    public void transformGeoreferencedImg() {
        georefpoint1 = new EastNorth(wmsLayer.X0, wmsLayer.Y0);
        georefpoint2 = new EastNorth(wmsLayer.X0+wmsLayer.fX*wmsLayer.communeBBox.max.getX(),
                wmsLayer.Y0+wmsLayer.fY*wmsLayer.communeBBox.max.getX());
        ea1 = new EastNorth(wmsLayer.getImage(0).min.east(), wmsLayer.getImage(0).max.north());
        EastNorth ea2 = wmsLayer.getImage(0).max;
        affineTransform(ea1, ea2, georefpoint1, georefpoint2);
        wmsLayer.grabThread.saveNewCache();
        wmsLayer.invalidate();
    }

    /**
     * Ask if georeferencing should be continued
     * @return false if all operations are canceled
     */
    private boolean continueGeoreferencing() {
        Object[] options = {"OK", "Cancel"};
        int ret = JOptionPane.showOptionDialog(null,
                tr("Click second Lambert crosspiece for georeferencing"),
                tr("Image georeferencing"),
                JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
                null, options, options[0]);
        if (ret != JOptionPane.OK_OPTION) {
            if (canceledOrRestartCurrAction("georeferencing"))
                return startGeoreferencing(wmsLayer);
        }
        return true;
    }

    private void handleNewCoordinates(double e, double n) {
        if (countMouseClicked == 1) {
            georefpoint1 = new EastNorth(e, n);
            continueGeoreferencing();
        } else {
            georefpoint2 = new EastNorth(e, n);
            endGeoreferencing();
        }
    }

    private void actionCompleted() {
        countMouseClicked = 0;
        mode = 0;
        mouseClickedTime = System.currentTimeMillis();
    }

    public void actionInterrupted() {
        actionCompleted();
        if (wmsLayer != null) {
            MainApplication.getLayerManager().removeLayer(wmsLayer);
            wmsLayer = null;
        }
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }

    @Override
    public void mousePressed(MouseEvent e) {
    }

    @Override
    public void mouseReleased(MouseEvent e) {
    }
}
