// License: GPL. For details, see LICENSE file.
package relcontext.actions;

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

import java.awt.Dialog.ModalityType;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;

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

import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangePropertyCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.MultipolygonBuilder;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.TagMap;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.Shortcut;

import relcontext.ChosenRelation;

/**
 * Creates new multipolygon from selected ways.
 * Choose relation afterwards.
 *
 * @author Zverik
 */
public class CreateMultipolygonAction extends JosmAction {
    private static final String PREF_MULTIPOLY = "reltoolbox.multipolygon.";
    protected transient ChosenRelation chRel;

    public CreateMultipolygonAction(ChosenRelation chRel) {
        super("Multi", "data/multipolygon", tr("Create a multipolygon from selected objects"),
                Shortcut.registerShortcut("reltoolbox:multipolygon", tr("Relation Toolbox: {0}", tr("Create multipolygon")),
                        KeyEvent.VK_A, Shortcut.ALT_CTRL), false);
        this.chRel = chRel;
        updateEnabledState();
    }

    public CreateMultipolygonAction() {
        this(null);
    }

    public static boolean getDefaultPropertyValue(String property) {
        switch (property) {
        case "boundary":
        case "alltags":
        case "allowsplit":
            return false;
        case "boundaryways":
        case "tags":
        case "single":
            return true;
        }
        throw new IllegalArgumentException(property);
    }

    private static boolean getPref(String property) {
        return Config.getPref().getBoolean(PREF_MULTIPOLY + property, getDefaultPropertyValue(property));
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        boolean isBoundary = getPref("boundary");
        DataSet ds = getLayerManager().getEditDataSet();
        Collection<Way> selectedWays = ds.getSelectedWays();
        if (!isBoundary && getPref("tags")) {
            List<Relation> rels = null;
            if (getPref("allowsplit") || selectedWays.size() == 1) {
                if (SplittingMultipolygons.canProcess(selectedWays)) {
                    rels = SplittingMultipolygons.process(selectedWays);
                }
            } else {
                if (TheRing.areAllOfThoseRings(selectedWays)) {
                    List<Command> commands = new ArrayList<>();
                    rels = TheRing.makeManySimpleMultipolygons(ds.getSelectedWays(), commands);
                    if (!commands.isEmpty()) {
                        UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygons from rings"), commands));
                    }
                }
            }
            if (rels != null && !rels.isEmpty()) {
                if (chRel != null) {
                    chRel.set(rels.size() == 1 ? rels.get(0) : null);
                }
                if (rels.size() == 1) {
                    ds.setSelected(rels);
                } else {
                    ds.clearSelection();
                }
                return;
            }
        }

        // for now, just copying standard action
        MultipolygonBuilder mpc = new MultipolygonBuilder();
        String error = mpc.makeFromWays(ds.getSelectedWays());
        if (error != null) {
            JOptionPane.showMessageDialog(MainApplication.getMainFrame(), error);
            return;
        }
        Relation rel = new Relation();
        if (isBoundary) {
            rel.put("type", "boundary");
            rel.put("boundary", "administrative");
        } else {
            rel.put("type", "multipolygon");
        }
        for (MultipolygonBuilder.JoinedPolygon poly : mpc.outerWays) {
            for (Way w : poly.ways) {
                rel.addMember(new RelationMember("outer", w));
            }
        }
        for (MultipolygonBuilder.JoinedPolygon poly : mpc.innerWays) {
            for (Way w : poly.ways) {
                rel.addMember(new RelationMember("inner", w));
            }
        }
        List<Command> list = removeTagsFromInnerWays(rel);
        if (!list.isEmpty() && isBoundary) {
            UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Move tags from ways to relation"), list));
            list = new ArrayList<>();
        }
        if (isBoundary) {
            if (!askForAdminLevelAndName(rel))
                return;
            addBoundaryMembers(rel);
            if (getPref("boundaryways")) {
                list.addAll(fixWayTagsForBoundary(rel));
            }
        }
        list.add(new AddCommand(ds, rel));
        UndoRedoHandler.getInstance().add(new SequenceCommand(tr("Create multipolygon"), list));

        if (chRel != null) {
            chRel.set(rel);
        }

        ds.setSelected(rel);
    }

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

    @Override
    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
        boolean isEnabled = true;
        if (selection == null || selection.isEmpty()) {
            isEnabled = false;
        } else {
            if (!getPref("boundary")) {
                for (OsmPrimitive p : selection) {
                    if (!(p instanceof Way)) {
                        isEnabled = false;
                        break;
                    }
                }
            }
        }
        setEnabled(isEnabled);
    }

    /**
     * Add selected nodes and relations with corresponding roles.
     */
    private void addBoundaryMembers(Relation rel) {
        for (OsmPrimitive p : getLayerManager().getEditDataSet().getSelected()) {
            String role = null;
            if (p.getType() == OsmPrimitiveType.RELATION) {
                role = "subarea";
            } else if (p.getType() == OsmPrimitiveType.NODE) {
                Node n = (Node) p;
                if (!n.isIncomplete()) {
                    if (n.hasKey("place")) {
                        role = "admin_centre";
                    } else {
                        role = "label";
                    }
                }
            }
            if (role != null) {
                rel.addMember(new RelationMember(role, p));
            }
        }
    }

    /**
     * For all untagged ways in relation, add tags boundary and admin_level.
     */
    private List<Command> fixWayTagsForBoundary(Relation rel) {
        List<Command> commands = new ArrayList<>();
        if (!rel.hasKey("boundary") || !rel.hasKey("admin_level"))
            return commands;
        String adminLevelStr = rel.get("admin_level");
        int adminLevel = 0;
        try {
            adminLevel = Integer.parseInt(adminLevelStr);
        } catch (NumberFormatException e) {
            return commands;
        }
        Set<OsmPrimitive> waysBoundary = new HashSet<>();
        Set<OsmPrimitive> waysAdminLevel = new HashSet<>();
        for (OsmPrimitive p : rel.getMemberPrimitives()) {
            if (p instanceof Way) {
                int count = 0;
                if (p.hasKey("boundary") && p.get("boundary").equals("administrative")) {
                    count++;
                }
                if (p.hasKey("admin_level")) {
                    count++;
                }
                if (p.keySet().size() - count == 0) {
                    if (!p.hasKey("boundary")) {
                        waysBoundary.add(p);
                    }
                    if (!p.hasKey("admin_level")) {
                        waysAdminLevel.add(p);
                    } else {
                        try {
                            int oldAdminLevel = Integer.parseInt(p.get("admin_level"));
                            if (oldAdminLevel > adminLevel) {
                                waysAdminLevel.add(p);
                            }
                        } catch (NumberFormatException e) {
                            waysAdminLevel.add(p); // some garbage, replace it
                        }
                    }
                }
            }
        }
        if (!waysBoundary.isEmpty()) {
            commands.add(new ChangePropertyCommand(waysBoundary, "boundary", "administrative"));
        }
        if (!waysAdminLevel.isEmpty()) {
            commands.add(new ChangePropertyCommand(waysAdminLevel, "admin_level", adminLevelStr));
        }
        return commands;
    }

    public static final List<String> DEFAULT_LINEAR_TAGS = Arrays.asList("barrier", "source");

    private static final Set<String> REMOVE_FROM_BOUNDARY_TAGS =
            new TreeSet<>(Arrays.asList("boundary", "boundary_type", "type", "admin_level"));

    /**
     * This method removes tags/value pairs from inner ways that are present in relation or outer ways.
     * It was copypasted from the standard {@link org.openstreetmap.josm.actions.CreateMultipolygonAction}.
     * Todo: rewrite it.
     */
    private List<Command> removeTagsFromInnerWays(Relation relation) {
        Map<String, String> values = new HashMap<>();

        if (relation.hasKeys()) {
            for (String key : relation.keySet()) {
                values.put(key, relation.get(key));
            }
        }

        List<Way> innerWays = new ArrayList<>();
        List<Way> outerWays = new ArrayList<>();

        Set<String> conflictingKeys = new TreeSet<>();

        for (RelationMember m : relation.getMembers()) {

            if (m.hasRole() && "inner".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
                innerWays.add(m.getWay());
            }

            if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay() && m.getWay().hasKeys()) {
                Way way = m.getWay();
                outerWays.add(way);
                for (String key : way.keySet()) {
                    if (!values.containsKey(key)) { //relation values take precedence
                        values.put(key, way.get(key));
                    } else if (!relation.hasKey(key) && !values.get(key).equals(way.get(key))) {
                        conflictingKeys.add(key);
                    }
                }
            }
        }

        // filter out empty key conflicts - we need second iteration
        boolean isBoundary = getPref("boundary");
        if (isBoundary || !getPref("alltags")) {
            for (RelationMember m : relation.getMembers()) {
                if (m.hasRole() && "outer".equals(m.getRole()) && m.isWay()) {
                    for (String key : values.keySet()) {
                        if (!m.getWay().hasKey(key) && !relation.hasKey(key)) {
                            conflictingKeys.add(key);
                        }
                    }
                }
            }
        }

        for (String key : conflictingKeys) {
            values.remove(key);
        }

        for (String linearTag : Config.getPref().getList(PREF_MULTIPOLY + "lineartags", DEFAULT_LINEAR_TAGS)) {
            values.remove(linearTag);
        }

        if ("coastline".equals(values.get("natural"))) {
            values.remove("natural");
        }

        String name = values.get("name");
        if (isBoundary) {
            Set<String> keySet = new TreeSet<>(values.keySet());
            for (String key : keySet) {
                if (!REMOVE_FROM_BOUNDARY_TAGS.contains(key)) {
                    values.remove(key);
                }
            }
        }

        values.put("area", "yes");

        List<Command> commands = new ArrayList<>();
        boolean moveTags = getPref("tags");

        for (Entry<String, String> entry : values.entrySet()) {
            String key = entry.getKey();
            List<OsmPrimitive> affectedWays = new ArrayList<>();
            String value = entry.getValue();

            for (Way way : innerWays) {
                if (way.hasKey(key) && (isBoundary || value.equals(way.get(key)))) {
                    affectedWays.add(way);
                }
            }

            if (moveTags) {
                // remove duplicated tags from outer ways
                for (Way way : outerWays) {
                    if (way.hasKey(key)) {
                        affectedWays.add(way);
                    }
                }
            }

            if (!affectedWays.isEmpty()) {
                commands.add(new ChangePropertyCommand(affectedWays, key, null));
            }
        }

        if (moveTags) {
            // add those tag values to the relation
            if (isBoundary) {
                values.put("name", name);
            }
            boolean fixed = false;
            TagMap tags = relation.getKeys();
            for (Entry<String, String> e: values.entrySet()) {
                final String key = e.getKey();
                final String val = e.getValue();
                if (!tags.containsKey(key) && !"area".equals(key)
                        && (!isBoundary || "admin_level".equals(key) || "name".equals(key))) {
                    if (relation.getDataSet() == null) {
                        relation.put(key, val);
                    } else {
                        tags.put(key, val);
                    }
                    fixed = true;
                }
            }
            if (fixed && relation.getDataSet() != null) {
                commands.add(new ChangePropertyCommand(Collections.singleton(relation), tags));
            }
        }

        return commands;
    }

    /**
     *
     * @param rel relation
     * @return false if user pressed "cancel".
     */
    private boolean askForAdminLevelAndName(Relation rel) {
        String relAL = rel.get("admin_level");
        String relName = rel.get("name");
        if (relAL != null && relName != null)
            return true;

        JPanel panel = new JPanel(new GridBagLayout());
        panel.add(new JLabel(tr("Enter admin level and name for the border relation:")), GBC.eol().insets(0, 0, 0, 5));

        final JTextField admin = new JTextField();
        admin.setText(relAL != null ? relAL : Config.getPref().get(PREF_MULTIPOLY + "lastadmin", ""));
        panel.add(new JLabel(tr("Admin level")), GBC.std());
        panel.add(Box.createHorizontalStrut(10), GBC.std());
        panel.add(admin, GBC.eol().fill(GBC.HORIZONTAL).insets(0, 0, 0, 5));

        final JTextField name = new JTextField();
        if (relName != null) {
            name.setText(relName);
        }
        panel.add(new JLabel(tr("Name")), GBC.std());
        panel.add(Box.createHorizontalStrut(10), GBC.std());
        panel.add(name, GBC.eol().fill(GBC.HORIZONTAL));

        final JOptionPane optionPane = new JOptionPane(panel, JOptionPane.QUESTION_MESSAGE, JOptionPane.OK_CANCEL_OPTION) {
            @Override
            public void selectInitialValue() {
                admin.requestFocusInWindow();
                admin.selectAll();
            }
        };
        final JDialog dlg = optionPane.createDialog(MainApplication.getMainFrame(), tr("Create a new relation"));
        dlg.setModalityType(ModalityType.DOCUMENT_MODAL);

        name.addActionListener(e -> {
            dlg.setVisible(false);
            optionPane.setValue(JOptionPane.OK_OPTION);
        });

        dlg.setVisible(true);

        Object answer = optionPane.getValue();
        dlg.dispose();
        if (answer == null || answer == JOptionPane.UNINITIALIZED_VALUE
                || (answer instanceof Integer && (Integer) answer != JOptionPane.OK_OPTION))
            return false;

        String adminLevel = admin.getText().trim();
        String newName = name.getText().trim();
        if ("10".equals(adminLevel) || (adminLevel.length() == 1 && Character.isDigit(adminLevel.charAt(0)))) {
            rel.put("admin_level", adminLevel);
            Config.getPref().put(PREF_MULTIPOLY + "lastadmin", adminLevel);
        }
        if (!newName.isEmpty()) {
            rel.put("name", newName);
        }
        return true;
    }
}
