// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.actions;

import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;

import javax.swing.JOptionPane;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.MoveCommand;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.Notification;
import org.openstreetmap.josm.tools.Shortcut;

/**
 * Distributes the selected nodes to equal distances along a line.
 *
 * @author Teemu Koskinen
 */
public final class DistributeAction extends JosmAction {

    /**
     * Constructs a new {@code DistributeAction}.
     */
    public DistributeAction() {
        super(tr("Distribute Nodes"), "distribute", tr("Distribute the selected nodes to equal distances along a line."),
                Shortcut.registerShortcut("tools:distribute", tr("Tool: {0}", tr("Distribute Nodes")), KeyEvent.VK_B,
                Shortcut.SHIFT), true);
        putValue("help", ht("/Action/DistributeNodes"));
    }

    /**
     * The general algorithm here is to find the two selected nodes
     * that are furthest apart, and then to distribute all other selected
     * nodes along the straight line between these nodes.
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        if (!isEnabled())
            return;
        Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected();
        Collection<Node> nodes = new LinkedList<>();
        Collection<Node> itnodes = new LinkedList<>();
        for (OsmPrimitive osm : sel)
            if (osm instanceof Node) {
                nodes.add((Node)osm);
                itnodes.add((Node)osm);
            }
        // special case if no single nodes are selected and exactly one way is:
        // then use the way's nodes
        if (nodes.isEmpty() && (sel.size() == 1)) {
            for (OsmPrimitive osm : sel)
                if (osm instanceof Way) {
                    nodes.addAll(((Way)osm).getNodes());
                    itnodes.addAll(((Way)osm).getNodes());
                }
        }

        Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes);
        ignoredNodes.addAll(removeNodesWithoutCoordinates(itnodes));
        if (!ignoredNodes.isEmpty()) {
            Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size()));
            ignoredNodes.clear();
        }

        if (nodes.size() < 3) {
            new Notification(
                    tr("Please select at least three nodes."))
                    .setIcon(JOptionPane.INFORMATION_MESSAGE)
                    .setDuration(Notification.TIME_SHORT)
                    .show();
            return;
        }

        // Find from the selected nodes two that are the furthest apart.
        // Let's call them A and B.
        double distance = 0;

        Node nodea = null;
        Node nodeb = null;

        for (Node n : nodes) {
            itnodes.remove(n);
            for (Node m : itnodes) {
                double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth()));
                if (dist > distance) {
                    nodea = n;
                    nodeb = m;
                    distance = dist;
                }
            }
        }

        // Remove the nodes A and B from the list of nodes to move
        nodes.remove(nodea);
        nodes.remove(nodeb);

        // Find out co-ords of A and B
        double ax = nodea.getEastNorth().east();
        double ay = nodea.getEastNorth().north();
        double bx = nodeb.getEastNorth().east();
        double by = nodeb.getEastNorth().north();

        // A list of commands to do
        Collection<Command> cmds = new LinkedList<>();

        // Amount of nodes between A and B plus 1
        int num = nodes.size()+1;

        // Current number of node
        int pos = 0;
        while (!nodes.isEmpty()) {
            pos++;
            Node s = null;

            // Find the node that is furthest from B (i.e. closest to A)
            distance = 0.0;
            for (Node n : nodes) {
                double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth()));
                if (dist > distance) {
                    s = n;
                    distance = dist;
                }
            }

            // First move the node to A's position, then move it towards B
            double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num;
            double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num;

            cmds.add(new MoveCommand(s, dx, dy));

            //remove moved node from the list
            nodes.remove(s);
        }

        // Do it!
        Main.main.undoRedo.add(new SequenceCommand(tr("Distribute Nodes"), cmds));
        Main.map.repaint();
    }

    private Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) {
        Set<Node> result = new HashSet<>();
        for (Iterator<Node> it = col.iterator(); it.hasNext();) {
            Node n = it.next();
            if (n.getCoor() == null) {
                it.remove();
                result.add(n);
            }
        }
        return result;
    }

    @Override
    protected void updateEnabledState() {
        if (getCurrentDataSet() == null) {
            setEnabled(false);
        } else {
            updateEnabledState(getCurrentDataSet().getSelected());
        }
    }

    @Override
    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
        setEnabled(selection != null && !selection.isEmpty());
    }
}
