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

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

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JToolBar;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.CopyAction;
import org.openstreetmap.josm.actions.ExpertToggleAction;
import org.openstreetmap.josm.actions.JosmAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveData;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Tag;
import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
import org.openstreetmap.josm.gui.MainMenu;
import org.openstreetmap.josm.gui.SideButton;
import org.openstreetmap.josm.gui.help.ContextSensitiveHelpAction;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.tagging.PresetHandler;
import org.openstreetmap.josm.gui.tagging.TagEditorModel;
import org.openstreetmap.josm.gui.tagging.TagEditorPanel;
import org.openstreetmap.josm.gui.tagging.TaggingPreset;
import org.openstreetmap.josm.gui.tagging.TaggingPresetType;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletingTextField;
import org.openstreetmap.josm.gui.tagging.ac.AutoCompletionList;
import org.openstreetmap.josm.io.OnlineResource;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Shortcut;
import org.openstreetmap.josm.tools.WindowGeometry;

/**
 * This dialog is for editing relations.
 * @since 343
 */
public class GenericRelationEditor extends RelationEditor  {
    /** the tag table and its model */
    private TagEditorPanel tagEditorPanel;
    private ReferringRelationsBrowser referrerBrowser;
    private ReferringRelationsBrowserModel referrerModel;

    /** the member table */
    private MemberTable memberTable;
    private MemberTableModel memberTableModel;

    /** the model for the selection table */
    private SelectionTable selectionTable;
    private SelectionTableModel selectionTableModel;

    private AutoCompletingTextField tfRole;

    /** the menu item in the windows menu. Required to properly
     * hide on dialog close.
     */
    private JMenuItem windowMenuItem;
    /**
     * Button for performing the {@link org.openstreetmap.josm.gui.dialogs.relation.GenericRelationEditor.SortBelowAction}.
     */
    private JButton sortBelowButton;

    /**
     * Creates a new relation editor for the given relation. The relation will be saved if the user
     * selects "ok" in the editor.
     *
     * If no relation is given, will create an editor for a new relation.
     *
     * @param layer the {@link OsmDataLayer} the new or edited relation belongs to
     * @param relation relation to edit, or null to create a new one.
     * @param selectedMembers a collection of members which shall be selected initially
     */
    public GenericRelationEditor(OsmDataLayer layer, Relation relation, Collection<RelationMember> selectedMembers) {
        super(layer, relation, selectedMembers);

        setRememberWindowGeometry(getClass().getName() + ".geometry",
                WindowGeometry.centerInWindow(Main.parent, new Dimension(700, 650)));

        final PresetHandler presetHandler = new PresetHandler() {

            @Override
            public void updateTags(List<Tag> tags) {
                tagEditorPanel.getModel().updateTags(tags);
            }

            @Override
            public Collection<OsmPrimitive> getSelection() {
                Relation relation = new Relation();
                tagEditorPanel.getModel().applyToPrimitive(relation);
                return Collections.<OsmPrimitive>singletonList(relation);
            }
        };

        // init the various models
        //
        memberTableModel = new MemberTableModel(getLayer(), presetHandler);
        memberTableModel.register();
        selectionTableModel = new SelectionTableModel(getLayer());
        selectionTableModel.register();
        referrerModel = new ReferringRelationsBrowserModel(relation);

        tagEditorPanel = new TagEditorPanel(presetHandler);

        // populate the models
        //
        if (relation != null) {
            tagEditorPanel.getModel().initFromPrimitive(relation);
            this.memberTableModel.populate(relation);
            if (!getLayer().data.getRelations().contains(relation)) {
                // treat it as a new relation if it doesn't exist in the
                // data set yet.
                setRelation(null);
            }
        } else {
            tagEditorPanel.getModel().clear();
            this.memberTableModel.populate(null);
        }
        tagEditorPanel.getModel().ensureOneTag();

        JSplitPane pane = buildSplitPane(relation);
        pane.setPreferredSize(new Dimension(100, 100));

        JPanel pnl = new JPanel();
        pnl.setLayout(new BorderLayout());
        pnl.add(pane, BorderLayout.CENTER);
        pnl.setBorder(BorderFactory.createRaisedBevelBorder());

        getContentPane().setLayout(new BorderLayout());
        JTabbedPane tabbedPane = new JTabbedPane();
        tabbedPane.add(tr("Tags and Members"), pnl);
        referrerBrowser = new ReferringRelationsBrowser(getLayer(), referrerModel);
        tabbedPane.add(tr("Parent Relations"), referrerBrowser);
        tabbedPane.add(tr("Child Relations"), new ChildRelationBrowser(getLayer(), relation));
        tabbedPane.addChangeListener(
                new ChangeListener() {
                    @Override
                    public void stateChanged(ChangeEvent e) {
                        JTabbedPane sourceTabbedPane = (JTabbedPane) e.getSource();
                        int index = sourceTabbedPane.getSelectedIndex();
                        String title = sourceTabbedPane.getTitleAt(index);
                        if (title.equals(tr("Parent Relations"))) {
                            referrerBrowser.init();
                        }
                    }
                }
        );

        getContentPane().add(buildToolBar(), BorderLayout.NORTH);
        getContentPane().add(tabbedPane, BorderLayout.CENTER);
        getContentPane().add(buildOkCancelButtonPanel(), BorderLayout.SOUTH);

        setSize(findMaxDialogSize());

        addWindowListener(
                new WindowAdapter() {
                    @Override
                    public void windowOpened(WindowEvent e) {
                        cleanSelfReferences();
                    }
                }
        );
        registerCopyPasteAction(tagEditorPanel.getPasteAction(),
                "PASTE_TAGS",
                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), KeyEvent.VK_V, Shortcut.CTRL_SHIFT).getKeyStroke());
        registerCopyPasteAction(new PasteMembersAction(), "PASTE_MEMBERS", Shortcut.getPasteKeyStroke());
        registerCopyPasteAction(new CopyMembersAction(), "COPY_MEMBERS", Shortcut.getCopyKeyStroke());

        tagEditorPanel.setNextFocusComponent(memberTable);
        selectionTable.setFocusable(false);
        memberTableModel.setSelectedMembers(selectedMembers);
        HelpUtil.setHelpContext(getRootPane(), ht("/Dialog/RelationEditor"));
    }

    /**
     * Creates the toolbar
     *
     * @return the toolbar
     */
    protected JToolBar buildToolBar() {
        JToolBar tb  = new JToolBar();
        tb.setFloatable(false);
        tb.add(new ApplyAction());
        tb.add(new DuplicateRelationAction());
        DeleteCurrentRelationAction deleteAction = new DeleteCurrentRelationAction();
        addPropertyChangeListener(deleteAction);
        tb.add(deleteAction);
        return tb;
    }

    /**
     * builds the panel with the OK and the Cancel button
     *
     * @return the panel with the OK and the Cancel button
     */
    protected JPanel buildOkCancelButtonPanel() {
        JPanel pnl = new JPanel();
        pnl.setLayout(new FlowLayout(FlowLayout.CENTER));

        pnl.add(new SideButton(new OKAction()));
        pnl.add(new SideButton(new CancelAction()));
        pnl.add(new SideButton(new ContextSensitiveHelpAction(ht("/Dialog/RelationEditor"))));
        return pnl;
    }

    /**
     * builds the panel with the tag editor
     *
     * @return the panel with the tag editor
     */
    protected JPanel buildTagEditorPanel() {
        JPanel pnl = new JPanel();
        pnl.setLayout(new GridBagLayout());

        GridBagConstraints gc = new GridBagConstraints();
        gc.gridx = 0;
        gc.gridy = 0;
        gc.gridheight = 1;
        gc.gridwidth = 1;
        gc.fill = GridBagConstraints.HORIZONTAL;
        gc.anchor = GridBagConstraints.FIRST_LINE_START;
        gc.weightx = 1.0;
        gc.weighty = 0.0;
        pnl.add(new JLabel(tr("Tags")), gc);

        gc.gridx = 0;
        gc.gridy = 1;
        gc.fill = GridBagConstraints.BOTH;
        gc.anchor = GridBagConstraints.CENTER;
        gc.weightx = 1.0;
        gc.weighty = 1.0;
        pnl.add(tagEditorPanel, gc);
        return pnl;
    }

    /**
     * builds the panel for the relation member editor
     *
     * @return the panel for the relation member editor
     */
    protected JPanel buildMemberEditorPanel() {
        final JPanel pnl = new JPanel(new GridBagLayout());
        // setting up the member table
        memberTable = new MemberTable(getLayer(), getRelation(), memberTableModel);
        memberTable.addMouseListener(new MemberTableDblClickAdapter());
        memberTableModel.addMemberModelListener(memberTable);

        final JScrollPane scrollPane = new JScrollPane(memberTable);

        GridBagConstraints gc = new GridBagConstraints();
        gc.gridx = 0;
        gc.gridy = 0;
        gc.gridwidth = 2;
        gc.fill = GridBagConstraints.HORIZONTAL;
        gc.anchor = GridBagConstraints.FIRST_LINE_START;
        gc.weightx = 1.0;
        gc.weighty = 0.0;
        pnl.add(new JLabel(tr("Members")), gc);

        gc.gridx = 0;
        gc.gridy = 1;
        gc.gridheight = 2;
        gc.gridwidth = 1;
        gc.fill = GridBagConstraints.VERTICAL;
        gc.anchor = GridBagConstraints.NORTHWEST;
        gc.weightx = 0.0;
        gc.weighty = 1.0;
        pnl.add(buildLeftButtonPanel(), gc);

        gc.gridx = 1;
        gc.gridy = 1;
        gc.gridheight = 1;
        gc.fill = GridBagConstraints.BOTH;
        gc.anchor = GridBagConstraints.CENTER;
        gc.weightx = 0.6;
        gc.weighty = 1.0;
        pnl.add(scrollPane, gc);

        // --- role editing
        JPanel p3 = new JPanel(new FlowLayout(FlowLayout.LEFT));
        p3.add(new JLabel(tr("Apply Role:")));
        tfRole = new AutoCompletingTextField(10);
        tfRole.setToolTipText(tr("Enter a role and apply it to the selected relation members"));
        tfRole.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                tfRole.selectAll();
            }
        });
        tfRole.setAutoCompletionList(new AutoCompletionList());
        tfRole.addFocusListener(
                new FocusAdapter() {
                    @Override
                    public void focusGained(FocusEvent e) {
                        AutoCompletionList list = tfRole.getAutoCompletionList();
                        if (list != null) {
                            list.clear();
                            getLayer().data.getAutoCompletionManager().populateWithMemberRoles(list, getRelation());
                        }
                    }
                }
        );
        tfRole.setText(Main.pref.get("relation.editor.generic.lastrole", ""));
        p3.add(tfRole);
        SetRoleAction setRoleAction = new SetRoleAction();
        memberTableModel.getSelectionModel().addListSelectionListener(setRoleAction);
        tfRole.getDocument().addDocumentListener(setRoleAction);
        tfRole.addActionListener(setRoleAction);
        memberTableModel.getSelectionModel().addListSelectionListener(
                new ListSelectionListener() {
                    @Override
                    public void valueChanged(ListSelectionEvent e) {
                        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
                    }
                }
        );
        tfRole.setEnabled(memberTable.getSelectedRowCount() > 0);
        SideButton btnApply = new SideButton(setRoleAction);
        btnApply.setPreferredSize(new Dimension(20, 20));
        btnApply.setText("");
        p3.add(btnApply);

        gc.gridx = 1;
        gc.gridy = 2;
        gc.fill = GridBagConstraints.HORIZONTAL;
        gc.anchor = GridBagConstraints.LAST_LINE_START;
        gc.weightx = 1.0;
        gc.weighty = 0.0;
        pnl.add(p3, gc);

        JPanel pnl2 = new JPanel();
        pnl2.setLayout(new GridBagLayout());

        gc.gridx = 0;
        gc.gridy = 0;
        gc.gridheight = 1;
        gc.gridwidth = 3;
        gc.fill = GridBagConstraints.HORIZONTAL;
        gc.anchor = GridBagConstraints.FIRST_LINE_START;
        gc.weightx = 1.0;
        gc.weighty = 0.0;
        pnl2.add(new JLabel(tr("Selection")), gc);

        gc.gridx = 0;
        gc.gridy = 1;
        gc.gridheight = 1;
        gc.gridwidth = 1;
        gc.fill = GridBagConstraints.VERTICAL;
        gc.anchor = GridBagConstraints.NORTHWEST;
        gc.weightx = 0.0;
        gc.weighty = 1.0;
        pnl2.add(buildSelectionControlButtonPanel(), gc);

        gc.gridx = 1;
        gc.gridy = 1;
        gc.weightx = 1.0;
        gc.weighty = 1.0;
        gc.fill = GridBagConstraints.BOTH;
        pnl2.add(buildSelectionTablePanel(), gc);

        final JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        splitPane.setLeftComponent(pnl);
        splitPane.setRightComponent(pnl2);
        splitPane.setOneTouchExpandable(false);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowOpened(WindowEvent e) {
                // has to be called when the window is visible, otherwise
                // no effect
                splitPane.setDividerLocation(0.6);
            }
        });

        JPanel pnl3 = new JPanel();
        pnl3.setLayout(new BorderLayout());
        pnl3.add(splitPane, BorderLayout.CENTER);

        return pnl3;
    }

    /**
     * builds the panel with the table displaying the currently selected primitives
     *
     * @return panel with current selection
     */
    protected JPanel buildSelectionTablePanel() {
        JPanel pnl = new JPanel(new BorderLayout());
        MemberRoleCellEditor ce = (MemberRoleCellEditor) memberTable.getColumnModel().getColumn(0).getCellEditor();
        selectionTable = new SelectionTable(selectionTableModel, new SelectionTableColumnModel(memberTableModel));
        selectionTable.setMemberTableModel(memberTableModel);
        selectionTable.setRowHeight(ce.getEditor().getPreferredSize().height);
        pnl.add(new JScrollPane(selectionTable), BorderLayout.CENTER);
        return pnl;
    }

    /**
     * builds the {@link JSplitPane} which divides the editor in an upper and a lower half
     *
     * @return the split panel
     */
    protected JSplitPane buildSplitPane(Relation relation) {
        final JSplitPane pane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        pane.setTopComponent(buildTagEditorPanel());
        pane.setBottomComponent(buildMemberEditorPanel());
        pane.setOneTouchExpandable(true);
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowOpened(WindowEvent e) {
                // has to be called when the window is visible, otherwise
                // no effect
                pane.setDividerLocation(0.3);
            }
        });
        return pane;
    }

    /**
     * build the panel with the buttons on the left
     *
     * @return left button panel
     */
    protected JToolBar buildLeftButtonPanel() {
        JToolBar tb = new JToolBar();
        tb.setOrientation(JToolBar.VERTICAL);
        tb.setFloatable(false);

        // -- move up action
        MoveUpAction moveUpAction = new MoveUpAction();
        memberTableModel.getSelectionModel().addListSelectionListener(moveUpAction);
        tb.add(moveUpAction);
        memberTable.getActionMap().put("moveUp", moveUpAction);

        // -- move down action
        MoveDownAction moveDownAction = new MoveDownAction();
        memberTableModel.getSelectionModel().addListSelectionListener(moveDownAction);
        tb.add(moveDownAction);
        memberTable.getActionMap().put("moveDown", moveDownAction);

        tb.addSeparator();

        // -- edit action
        EditAction editAction = new EditAction();
        memberTableModel.getSelectionModel().addListSelectionListener(editAction);
        tb.add(editAction);

        // -- delete action
        RemoveAction removeSelectedAction = new RemoveAction();
        memberTable.getSelectionModel().addListSelectionListener(removeSelectedAction);
        tb.add(removeSelectedAction);
        memberTable.getActionMap().put("removeSelected", removeSelectedAction);

        tb.addSeparator();
        // -- sort action
        SortAction sortAction = new SortAction();
        memberTableModel.addTableModelListener(sortAction);
        tb.add(sortAction);
        final SortBelowAction sortBelowAction = new SortBelowAction();
        memberTableModel.addTableModelListener(sortBelowAction);
        memberTableModel.getSelectionModel().addListSelectionListener(sortBelowAction);
        sortBelowButton = tb.add(sortBelowAction);

        // -- reverse action
        ReverseAction reverseAction = new ReverseAction();
        memberTableModel.addTableModelListener(reverseAction);
        tb.add(reverseAction);

        tb.addSeparator();

        // -- download action
        DownloadIncompleteMembersAction downloadIncompleteMembersAction = new DownloadIncompleteMembersAction();
        memberTable.getModel().addTableModelListener(downloadIncompleteMembersAction);
        tb.add(downloadIncompleteMembersAction);
        memberTable.getActionMap().put("downloadIncomplete", downloadIncompleteMembersAction);

        // -- download selected action
        DownloadSelectedIncompleteMembersAction downloadSelectedIncompleteMembersAction = new DownloadSelectedIncompleteMembersAction();
        memberTable.getModel().addTableModelListener(downloadSelectedIncompleteMembersAction);
        memberTable.getSelectionModel().addListSelectionListener(downloadSelectedIncompleteMembersAction);
        tb.add(downloadSelectedIncompleteMembersAction);

        InputMap inputMap = memberTable.getInputMap(MemberTable.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        inputMap.put((KeyStroke) removeSelectedAction.getValue(AbstractAction.ACCELERATOR_KEY), "removeSelected");
        inputMap.put((KeyStroke) moveUpAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveUp");
        inputMap.put((KeyStroke) moveDownAction.getValue(AbstractAction.ACCELERATOR_KEY), "moveDown");
        inputMap.put((KeyStroke) downloadIncompleteMembersAction.getValue(AbstractAction.ACCELERATOR_KEY), "downloadIncomplete");

        return tb;
    }

    /**
     * build the panel with the buttons for adding or removing the current selection
     *
     * @return control buttons panel for selection/members
     */
    protected JToolBar buildSelectionControlButtonPanel() {
        JToolBar tb = new JToolBar(JToolBar.VERTICAL);
        tb.setFloatable(false);

        // -- add at start action
        AddSelectedAtStartAction addSelectionAction = new AddSelectedAtStartAction();
        selectionTableModel.addTableModelListener(addSelectionAction);
        tb.add(addSelectionAction);

        // -- add before selected action
        AddSelectedBeforeSelection addSelectedBeforeSelectionAction = new AddSelectedBeforeSelection();
        selectionTableModel.addTableModelListener(addSelectedBeforeSelectionAction);
        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedBeforeSelectionAction);
        tb.add(addSelectedBeforeSelectionAction);

        // -- add after selected action
        AddSelectedAfterSelection addSelectedAfterSelectionAction = new AddSelectedAfterSelection();
        selectionTableModel.addTableModelListener(addSelectedAfterSelectionAction);
        memberTableModel.getSelectionModel().addListSelectionListener(addSelectedAfterSelectionAction);
        tb.add(addSelectedAfterSelectionAction);

        // -- add at end action
        AddSelectedAtEndAction addSelectedAtEndAction = new AddSelectedAtEndAction();
        selectionTableModel.addTableModelListener(addSelectedAtEndAction);
        tb.add(addSelectedAtEndAction);

        tb.addSeparator();

        // -- select members action
        SelectedMembersForSelectionAction selectMembersForSelectionAction = new SelectedMembersForSelectionAction();
        selectionTableModel.addTableModelListener(selectMembersForSelectionAction);
        memberTableModel.addTableModelListener(selectMembersForSelectionAction);
        tb.add(selectMembersForSelectionAction);

        // -- select action
        SelectPrimitivesForSelectedMembersAction selectAction = new SelectPrimitivesForSelectedMembersAction();
        memberTable.getSelectionModel().addListSelectionListener(selectAction);
        tb.add(selectAction);

        tb.addSeparator();

        // -- remove selected action
        RemoveSelectedAction removeSelectedAction = new RemoveSelectedAction();
        selectionTableModel.addTableModelListener(removeSelectedAction);
        tb.add(removeSelectedAction);

        return tb;
    }

    @Override
    protected Dimension findMaxDialogSize() {
        return new Dimension(700, 650);
    }

    @Override
    public void setVisible(boolean visible) {
        if (visible) {
            tagEditorPanel.initAutoCompletion(getLayer());
        }
        super.setVisible(visible);
        if (visible) {
            sortBelowButton.setVisible(ExpertToggleAction.isExpert());
            RelationDialogManager.getRelationDialogManager().positionOnScreen(this);
            if (windowMenuItem == null) {
                addToWindowMenu();
            }
            tagEditorPanel.requestFocusInWindow();
        } else {
            // make sure all registered listeners are unregistered
            //
            memberTable.stopHighlighting();
            selectionTableModel.unregister();
            memberTableModel.unregister();
            memberTable.unlinkAsListener();
            if (windowMenuItem != null) {
                Main.main.menu.windowMenu.remove(windowMenuItem);
                windowMenuItem = null;
            }
            dispose();
        }
    }

    /** adds current relation editor to the windows menu (in the "volatile" group) o*/
    protected void addToWindowMenu() {
        String name = getRelation() == null ? tr("New Relation") : getRelation().getLocalName();
        final String tt = tr("Focus Relation Editor with relation ''{0}'' in layer ''{1}''",
                name, getLayer().getName());
        name = tr("Relation Editor: {0}", name == null ? getRelation().getId() : name);
        final JMenu wm = Main.main.menu.windowMenu;
        final JosmAction focusAction = new JosmAction(name, "dialogs/relationlist", tt, null, false, false) {
            @Override
            public void actionPerformed(ActionEvent e) {
                final RelationEditor r = (RelationEditor) getValue("relationEditor");
                r.setVisible(true);
            }
        };
        focusAction.putValue("relationEditor", this);
        windowMenuItem = MainMenu.add(wm, focusAction, MainMenu.WINDOW_MENU_GROUP.VOLATILE);
    }

    /**
     * checks whether the current relation has members referring to itself. If so,
     * warns the users and provides an option for removing these members.
     *
     */
    protected void cleanSelfReferences() {
        List<OsmPrimitive> toCheck = new ArrayList<>();
        toCheck.add(getRelation());
        if (memberTableModel.hasMembersReferringTo(toCheck)) {
            int ret = ConditionalOptionPaneUtil.showOptionDialog(
                    "clean_relation_self_references",
                    Main.parent,
                    tr("<html>There is at least one member in this relation referring<br>"
                            + "to the relation itself.<br>"
                            + "This creates circular dependencies and is discouraged.<br>"
                            + "How do you want to proceed with circular dependencies?</html>"),
                            tr("Warning"),
                            JOptionPane.YES_NO_OPTION,
                            JOptionPane.WARNING_MESSAGE,
                            new String[]{tr("Remove them, clean up relation"), tr("Ignore them, leave relation as is")},
                            tr("Remove them, clean up relation")
            );
            switch(ret) {
            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
            case JOptionPane.CLOSED_OPTION:
            case JOptionPane.NO_OPTION:
                return;
            case JOptionPane.YES_OPTION:
                memberTableModel.removeMembersReferringTo(toCheck);
                break;
            }
        }
    }

    private void registerCopyPasteAction(AbstractAction action, Object actionName, KeyStroke shortcut) {
        int mods = shortcut.getModifiers();
        int code = shortcut.getKeyCode();
        if (code != KeyEvent.VK_INSERT && (mods == 0 || mods == InputEvent.SHIFT_DOWN_MASK)) {
            Main.info(tr("Sorry, shortcut \"{0}\" can not be enabled in Relation editor dialog"), shortcut);
            return;
        }
        getRootPane().getActionMap().put(actionName, action);
        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
        // Assign also to JTables because they have their own Copy&Paste implementation (which is disabled in this case but eats key shortcuts anyway)
        memberTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
        memberTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
        memberTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
        selectionTable.getInputMap(JComponent.WHEN_FOCUSED).put(shortcut, actionName);
        selectionTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(shortcut, actionName);
        selectionTable.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shortcut, actionName);
    }

    static class AddAbortException extends Exception {
    }

    static boolean confirmAddingPrimitive(OsmPrimitive primitive) throws AddAbortException {
        String msg = tr("<html>This relation already has one or more members referring to<br>"
                + "the object ''{0}''<br>"
                + "<br>"
                + "Do you really want to add another relation member?</html>",
                primitive.getDisplayName(DefaultNameFormatter.getInstance())
            );
        int ret = ConditionalOptionPaneUtil.showOptionDialog(
                "add_primitive_to_relation",
                Main.parent,
                msg,
                tr("Multiple members referring to same object."),
                JOptionPane.YES_NO_CANCEL_OPTION,
                JOptionPane.WARNING_MESSAGE,
                null,
                null
        );
        switch(ret) {
        case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
        case JOptionPane.YES_OPTION:
            return true;
        case JOptionPane.NO_OPTION:
        case JOptionPane.CLOSED_OPTION:
            return false;
        case JOptionPane.CANCEL_OPTION:
            throw new AddAbortException();
        }
        // should not happen
        return false;
    }

    static void warnOfCircularReferences(OsmPrimitive primitive) {
        String msg = tr("<html>You are trying to add a relation to itself.<br>"
                + "<br>"
                + "This creates circular references and is therefore discouraged.<br>"
                + "Skipping relation ''{0}''.</html>",
                primitive.getDisplayName(DefaultNameFormatter.getInstance()));
        JOptionPane.showMessageDialog(
                Main.parent,
                msg,
                tr("Warning"),
                JOptionPane.WARNING_MESSAGE);
    }

    /**
     * Adds primitives to a given relation.
     * @param orig The relation to modify
     * @param primitivesToAdd The primitives to add as relation members
     * @return The resulting command
     * @throws IllegalArgumentException if orig is null
     */
    public static Command addPrimitivesToRelation(final Relation orig, Collection<? extends OsmPrimitive> primitivesToAdd) {
        CheckParameterUtil.ensureParameterNotNull(orig, "orig");
        try {
            final Collection<TaggingPreset> presets = TaggingPreset.getMatchingPresets(
                    EnumSet.of(TaggingPresetType.RELATION), orig.getKeys(), false);
            Relation relation = new Relation(orig);
            boolean modified = false;
            for (OsmPrimitive p : primitivesToAdd) {
                if (p instanceof Relation && orig.equals(p)) {
                    warnOfCircularReferences(p);
                    continue;
                } else if (MemberTableModel.hasMembersReferringTo(relation.getMembers(), Collections.singleton(p))
                        && !confirmAddingPrimitive(p)) {
                    continue;
                }
                final Set<String> roles = findSuggestedRoles(presets, p);
                relation.addMember(new RelationMember(roles.size() == 1 ? roles.iterator().next() : "", p));
                modified = true;
            }
            return modified ? new ChangeCommand(orig, relation) : null;
        } catch (AddAbortException ign) {
            return null;
        }
    }

    protected static Set<String> findSuggestedRoles(final Collection<TaggingPreset> presets, OsmPrimitive p) {
        final Set<String> roles = new HashSet<>();
        for (TaggingPreset preset : presets) {
            String role = preset.suggestRoleForOsmPrimitive(p);
            if (role != null && !role.isEmpty()) {
                roles.add(role);
            }
        }
        return roles;
    }

    abstract class AddFromSelectionAction extends AbstractAction {
        protected boolean isPotentialDuplicate(OsmPrimitive primitive) {
            return memberTableModel.hasMembersReferringTo(Collections.singleton(primitive));
        }

        protected List<OsmPrimitive> filterConfirmedPrimitives(List<OsmPrimitive> primitives) throws AddAbortException {
            if (primitives == null || primitives.isEmpty())
                return primitives;
            List<OsmPrimitive> ret = new ArrayList<>();
            ConditionalOptionPaneUtil.startBulkOperation("add_primitive_to_relation");
            for (OsmPrimitive primitive : primitives) {
                if (primitive instanceof Relation && getRelation() != null && getRelation().equals(primitive)) {
                    warnOfCircularReferences(primitive);
                    continue;
                }
                if (isPotentialDuplicate(primitive)) {
                    if (confirmAddingPrimitive(primitive)) {
                        ret.add(primitive);
                    }
                    continue;
                } else {
                    ret.add(primitive);
                }
            }
            ConditionalOptionPaneUtil.endBulkOperation("add_primitive_to_relation");
            return ret;
        }
    }

    class AddSelectedAtStartAction extends AddFromSelectionAction implements TableModelListener {
        public AddSelectedAtStartAction() {
            putValue(SHORT_DESCRIPTION,
                    tr("Add all objects selected in the current dataset before the first member"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copystartright"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(selectionTableModel.getRowCount() > 0);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
                memberTableModel.addMembersAtBeginning(toAdd);
            } catch (AddAbortException ex) {
                // do nothing
            }
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            refreshEnabled();
        }
    }

    class AddSelectedAtEndAction extends AddFromSelectionAction implements TableModelListener {
        public AddSelectedAtEndAction() {
            putValue(SHORT_DESCRIPTION, tr("Add all objects selected in the current dataset after the last member"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyendright"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(selectionTableModel.getRowCount() > 0);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
                memberTableModel.addMembersAtEnd(toAdd);
            } catch (AddAbortException ex) {
                // do nothing
            }
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            refreshEnabled();
        }
    }

    class AddSelectedBeforeSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
        public AddSelectedBeforeSelection() {
            putValue(SHORT_DESCRIPTION,
                    tr("Add all objects selected in the current dataset before the first selected member"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copybeforecurrentright"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(selectionTableModel.getRowCount() > 0
                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
                memberTableModel.addMembersBeforeIdx(toAdd, memberTableModel
                        .getSelectionModel().getMinSelectionIndex());
            } catch (AddAbortException ex) {
                // do nothing
            }

        }

        @Override
        public void tableChanged(TableModelEvent e) {
            refreshEnabled();
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            refreshEnabled();
        }
    }

    class AddSelectedAfterSelection extends AddFromSelectionAction implements TableModelListener, ListSelectionListener {
        public AddSelectedAfterSelection() {
            putValue(SHORT_DESCRIPTION,
                    tr("Add all objects selected in the current dataset after the last selected member"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/conflict", "copyaftercurrentright"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(selectionTableModel.getRowCount() > 0
                    && memberTableModel.getSelectionModel().getMinSelectionIndex() >= 0);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                List<OsmPrimitive> toAdd = filterConfirmedPrimitives(selectionTableModel.getSelection());
                memberTableModel.addMembersAfterIdx(toAdd, memberTableModel
                        .getSelectionModel().getMaxSelectionIndex());
            } catch (AddAbortException ex) {
                // do nothing
            }
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            refreshEnabled();
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            refreshEnabled();
        }
    }

    class RemoveSelectedAction extends AbstractAction implements TableModelListener {
        public RemoveSelectedAction() {
            putValue(SHORT_DESCRIPTION, tr("Remove all members referring to one of the selected objects"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "deletemembers"));
            updateEnabledState();
        }

        protected void updateEnabledState() {
            DataSet ds = getLayer().data;
            if (ds == null || ds.getSelected().isEmpty()) {
                setEnabled(false);
                return;
            }
            // only enable the action if we have members referring to the
            // selected primitives
            //
            setEnabled(memberTableModel.hasMembersReferringTo(ds.getSelected()));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.removeMembersReferringTo(selectionTableModel.getSelection());
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    /**
     * Selects  members in the relation editor which refer to primitives in the current
     * selection of the context layer.
     *
     */
    class SelectedMembersForSelectionAction extends AbstractAction implements TableModelListener {
        public SelectedMembersForSelectionAction() {
            putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectmembers"));
            updateEnabledState();
        }

        protected void updateEnabledState() {
            boolean enabled = selectionTableModel.getRowCount() > 0
            &&  !memberTableModel.getChildPrimitives(getLayer().data.getSelected()).isEmpty();

            if (enabled) {
                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to {0} objects in the current selection",
                        memberTableModel.getChildPrimitives(getLayer().data.getSelected()).size()));
            } else {
                putValue(SHORT_DESCRIPTION, tr("Select relation members which refer to objects in the current selection"));
            }
            setEnabled(enabled);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.selectMembersReferringTo(getLayer().data.getSelected());
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    /**
     * Selects primitives in the layer this editor belongs to. The selected primitives are
     * equal to the set of primitives the currently selected relation members refer to.
     *
     */
    class SelectPrimitivesForSelectedMembersAction extends AbstractAction implements ListSelectionListener {
        public SelectPrimitivesForSelectedMembersAction() {
            putValue(SHORT_DESCRIPTION, tr("Select objects for selected relation members"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "selectprimitives"));
            updateEnabledState();
        }

        protected void updateEnabledState() {
            setEnabled(memberTable.getSelectedRowCount() > 0);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            getLayer().data.setSelected(memberTableModel.getSelectedChildPrimitives());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }
    }

    class SortAction extends AbstractAction implements TableModelListener {
        public SortAction() {
            String tooltip = tr("Sort the relation members");
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort"));
            putValue(NAME, tr("Sort"));
            Shortcut sc = Shortcut.registerShortcut("relationeditor:sort", tr("Relation Editor: Sort"),
                KeyEvent.VK_END, Shortcut.ALT);
            sc.setAccelerator(this);
            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.sort();
        }

        protected void updateEnabledState() {
            setEnabled(memberTableModel.getRowCount() > 0);
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    class SortBelowAction extends AbstractAction implements TableModelListener, ListSelectionListener {
        public SortBelowAction() {
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "sort_below"));
            putValue(NAME, tr("Sort below"));
            putValue(SHORT_DESCRIPTION, tr("Sort the selected relation members and all members below"));
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.sortBelow();
        }

        protected void updateEnabledState() {
            setEnabled(memberTableModel.getRowCount() > 0 && !memberTableModel.getSelectionModel().isSelectionEmpty());
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }
    }

    class ReverseAction extends AbstractAction implements TableModelListener {
        public ReverseAction() {
            putValue(SHORT_DESCRIPTION, tr("Reverse the order of the relation members"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "reverse"));
            putValue(NAME, tr("Reverse"));
        //  Shortcut.register Shortcut("relationeditor:reverse", tr("Relation Editor: Reverse"),
        //      KeyEvent.VK_END, Shortcut.ALT)
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.reverse();
        }

        protected void updateEnabledState() {
            setEnabled(memberTableModel.getRowCount() > 0);
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    class MoveUpAction extends AbstractAction implements ListSelectionListener {
        public MoveUpAction() {
            String tooltip = tr("Move the currently selected members up");
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "moveup"));
            Shortcut sc = Shortcut.registerShortcut("relationeditor:moveup", tr("Relation Editor: Move Up"),
                KeyEvent.VK_UP, Shortcut.ALT);
            sc.setAccelerator(this);
            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
            setEnabled(false);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.moveUp(memberTable.getSelectedRows());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            setEnabled(memberTableModel.canMoveUp(memberTable.getSelectedRows()));
        }
    }

    class MoveDownAction extends AbstractAction implements ListSelectionListener {
        public MoveDownAction() {
            String tooltip = tr("Move the currently selected members down");
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "movedown"));
            Shortcut sc = Shortcut.registerShortcut("relationeditor:movedown", tr("Relation Editor: Move Down"),
                KeyEvent.VK_DOWN, Shortcut.ALT);
            sc.setAccelerator(this);
            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
            setEnabled(false);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.moveDown(memberTable.getSelectedRows());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            setEnabled(memberTableModel.canMoveDown(memberTable.getSelectedRows()));
        }
    }

    class RemoveAction extends AbstractAction implements ListSelectionListener {
        public RemoveAction() {
            String tooltip = tr("Remove the currently selected members from this relation");
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
            putValue(NAME, tr("Remove"));
            Shortcut sc = Shortcut.registerShortcut("relationeditor:remove", tr("Relation Editor: Remove"),
                KeyEvent.VK_DELETE, Shortcut.ALT);
            sc.setAccelerator(this);
            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
            setEnabled(false);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTableModel.remove(memberTable.getSelectedRows());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            setEnabled(memberTableModel.canRemove(memberTable.getSelectedRows()));
        }
    }

    class DeleteCurrentRelationAction extends AbstractAction implements PropertyChangeListener{
        public DeleteCurrentRelationAction() {
            putValue(SHORT_DESCRIPTION, tr("Delete the currently edited relation"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "delete"));
            putValue(NAME, tr("Delete"));
            updateEnabledState();
        }

        public void run() {
            Relation toDelete = getRelation();
            if (toDelete == null)
                return;
            org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation(
                    getLayer(),
                    toDelete
            );
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            run();
        }

        protected void updateEnabledState() {
            setEnabled(getRelationSnapshot() != null);
        }

        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getPropertyName().equals(RELATION_SNAPSHOT_PROP)) {
                updateEnabledState();
            }
        }
    }

    abstract class SavingAction extends AbstractAction {
        /**
         * apply updates to a new relation
         */
        protected void applyNewRelation() {
            final Relation newRelation = new Relation();
            tagEditorPanel.getModel().applyToPrimitive(newRelation);
            memberTableModel.applyToRelation(newRelation);
            List<RelationMember> newMembers = new ArrayList<>();
            for (RelationMember rm: newRelation.getMembers()) {
                if (!rm.getMember().isDeleted()) {
                    newMembers.add(rm);
                }
            }
            if (newRelation.getMembersCount() != newMembers.size()) {
                newRelation.setMembers(newMembers);
                String msg = tr("One or more members of this new relation have been deleted while the relation editor\n" +
                "was open. They have been removed from the relation members list.");
                JOptionPane.showMessageDialog(Main.parent, msg, tr("Warning"), JOptionPane.WARNING_MESSAGE);
            }
            // If the user wanted to create a new relation, but hasn't added any members or
            // tags, don't add an empty relation
            if (newRelation.getMembersCount() == 0 && !newRelation.hasKeys())
                return;
            Main.main.undoRedo.add(new AddCommand(getLayer(), newRelation));

            // make sure everybody is notified about the changes
            //
            getLayer().data.fireSelectionChanged();
            GenericRelationEditor.this.setRelation(newRelation);
            RelationDialogManager.getRelationDialogManager().updateContext(
                    getLayer(),
                    getRelation(),
                    GenericRelationEditor.this
            );
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    // Relation list gets update in EDT so selecting my be postponed to following EDT run
                    Main.map.relationListDialog.selectRelation(newRelation);
                }
            });
        }

        /**
         * Apply the updates for an existing relation which has been changed
         * outside of the relation editor.
         *
         */
        protected void applyExistingConflictingRelation() {
            Relation editedRelation = new Relation(getRelation());
            tagEditorPanel.getModel().applyToPrimitive(editedRelation);
            memberTableModel.applyToRelation(editedRelation);
            Conflict<Relation> conflict = new Conflict<>(getRelation(), editedRelation);
            Main.main.undoRedo.add(new ConflictAddCommand(getLayer(), conflict));
        }

        /**
         * Apply the updates for an existing relation which has not been changed
         * outside of the relation editor.
         *
         */
        protected void applyExistingNonConflictingRelation() {
            Relation editedRelation = new Relation(getRelation());
            tagEditorPanel.getModel().applyToPrimitive(editedRelation);
            memberTableModel.applyToRelation(editedRelation);
            Main.main.undoRedo.add(new ChangeCommand(getRelation(), editedRelation));
            getLayer().data.fireSelectionChanged();
            // this will refresh the snapshot and update the dialog title
            //
            setRelation(getRelation());
        }

        protected boolean confirmClosingBecauseOfDirtyState() {
            ButtonSpec[] options = new ButtonSpec[] {
                    new ButtonSpec(
                            tr("Yes, create a conflict and close"),
                            ImageProvider.get("ok"),
                            tr("Click to create a conflict and close this relation editor") ,
                            null /* no specific help topic */
                    ),
                    new ButtonSpec(
                            tr("No, continue editing"),
                            ImageProvider.get("cancel"),
                            tr("Click to return to the relation editor and to resume relation editing") ,
                            null /* no specific help topic */
                    )
            };

            int ret = HelpAwareOptionPane.showOptionDialog(
                    Main.parent,
                    tr("<html>This relation has been changed outside of the editor.<br>"
                            + "You cannot apply your changes and continue editing.<br>"
                            + "<br>"
                            + "Do you want to create a conflict and close the editor?</html>"),
                            tr("Conflict in data"),
                            JOptionPane.WARNING_MESSAGE,
                            null,
                            options,
                            options[0], // OK is default
                            "/Dialog/RelationEditor#RelationChangedOutsideOfEditor"
            );
            return ret == 0;
        }

        protected void warnDoubleConflict() {
            JOptionPane.showMessageDialog(
                    Main.parent,
                    tr("<html>Layer ''{0}'' already has a conflict for object<br>"
                            + "''{1}''.<br>"
                            + "Please resolve this conflict first, then try again.</html>",
                            getLayer().getName(),
                            getRelation().getDisplayName(DefaultNameFormatter.getInstance())
                    ),
                    tr("Double conflict"),
                    JOptionPane.WARNING_MESSAGE
            );
        }
    }

    class ApplyAction extends SavingAction {
        public ApplyAction() {
            putValue(SHORT_DESCRIPTION, tr("Apply the current updates"));
            putValue(SMALL_ICON, ImageProvider.get("save"));
            putValue(NAME, tr("Apply"));
            setEnabled(true);
        }

        public void run() {
            if (getRelation() == null) {
                applyNewRelation();
            } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
                    || tagEditorPanel.getModel().isDirty()) {
                if (isDirtyRelation()) {
                    if (confirmClosingBecauseOfDirtyState()) {
                        if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
                            warnDoubleConflict();
                            return;
                        }
                        applyExistingConflictingRelation();
                        setVisible(false);
                    }
                } else {
                    applyExistingNonConflictingRelation();
                }
            }
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            run();
        }
    }

    class OKAction extends SavingAction {
        public OKAction() {
            putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog"));
            putValue(SMALL_ICON, ImageProvider.get("ok"));
            putValue(NAME, tr("OK"));
            setEnabled(true);
        }

        public void run() {
            Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
            memberTable.stopHighlighting();
            if (getRelation() == null) {
                applyNewRelation();
            } else if (!memberTableModel.hasSameMembersAs(getRelationSnapshot())
                    || tagEditorPanel.getModel().isDirty()) {
                if (isDirtyRelation()) {
                    if (confirmClosingBecauseOfDirtyState()) {
                        if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
                            warnDoubleConflict();
                            return;
                        }
                        applyExistingConflictingRelation();
                    } else
                        return;
                } else {
                    applyExistingNonConflictingRelation();
                }
            }
            setVisible(false);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            run();
        }
    }

    class CancelAction extends SavingAction {
        public CancelAction() {
            putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog"));
            putValue(SMALL_ICON, ImageProvider.get("cancel"));
            putValue(NAME, tr("Cancel"));

            getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
            getRootPane().getActionMap().put("ESCAPE", this);
            setEnabled(true);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            memberTable.stopHighlighting();
            TagEditorModel tagModel = tagEditorPanel.getModel();
            Relation snapshot = getRelationSnapshot();
            if ((!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty())
             && !(snapshot == null && tagModel.getTags().isEmpty())) {
                //give the user a chance to save the changes
                int ret = confirmClosingByCancel();
                if (ret == 0) { //Yes, save the changes
                    //copied from OKAction.run()
                    Main.pref.put("relation.editor.generic.lastrole", tfRole.getText());
                    if (getRelation() == null) {
                        applyNewRelation();
                    } else if (!memberTableModel.hasSameMembersAs(snapshot) || tagModel.isDirty()) {
                        if (isDirtyRelation()) {
                            if (confirmClosingBecauseOfDirtyState()) {
                                if (getLayer().getConflicts().hasConflictForMy(getRelation())) {
                                    warnDoubleConflict();
                                    return;
                                }
                                applyExistingConflictingRelation();
                            } else
                                return;
                        } else {
                            applyExistingNonConflictingRelation();
                        }
                    }
                } else if (ret == 2) //Cancel, continue editing
                    return;
                //in case of "No, discard", there is no extra action to be performed here.
            }
            setVisible(false);
        }

        protected int confirmClosingByCancel() {
            ButtonSpec[] options = new ButtonSpec[] {
                    new ButtonSpec(
                            tr("Yes, save the changes and close"),
                            ImageProvider.get("ok"),
                            tr("Click to save the changes and close this relation editor") ,
                            null /* no specific help topic */
                    ),
                    new ButtonSpec(
                            tr("No, discard the changes and close"),
                            ImageProvider.get("cancel"),
                            tr("Click to discard the changes and close this relation editor") ,
                            null /* no specific help topic */
                    ),
                    new ButtonSpec(
                            tr("Cancel, continue editing"),
                            ImageProvider.get("cancel"),
                            tr("Click to return to the relation editor and to resume relation editing") ,
                            null /* no specific help topic */
                    )
            };

            return HelpAwareOptionPane.showOptionDialog(
                    Main.parent,
                    tr("<html>The relation has been changed.<br>"
                            + "<br>"
                            + "Do you want to save your changes?</html>"),
                            tr("Unsaved changes"),
                            JOptionPane.WARNING_MESSAGE,
                            null,
                            options,
                            options[0], // OK is default,
                            "/Dialog/RelationEditor#DiscardChanges"
            );
        }
    }

    class AddTagAction extends AbstractAction {
        public AddTagAction() {
            putValue(SHORT_DESCRIPTION, tr("Add an empty tag"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "add"));
            setEnabled(true);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            tagEditorPanel.getModel().appendNewTag();
        }
    }

    class DownloadIncompleteMembersAction extends AbstractAction implements TableModelListener {
        public DownloadIncompleteMembersAction() {
            String tooltip = tr("Download all incomplete members");
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincomplete"));
            putValue(NAME, tr("Download Members"));
            Shortcut sc = Shortcut.registerShortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
                KeyEvent.VK_HOME, Shortcut.ALT);
            sc.setAccelerator(this);
            putValue(SHORT_DESCRIPTION, Main.platform.makeTooltip(tooltip, sc));
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (!isEnabled())
                return;
            Main.worker.submit(new DownloadRelationMemberTask(
                    getRelation(),
                    memberTableModel.getIncompleteMemberPrimitives(),
                    getLayer(),
                    GenericRelationEditor.this)
            );
        }

        protected void updateEnabledState() {
            setEnabled(memberTableModel.hasIncompleteMembers() && !Main.isOffline(OnlineResource.OSM_API));
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    class DownloadSelectedIncompleteMembersAction extends AbstractAction implements ListSelectionListener, TableModelListener{
        public DownloadSelectedIncompleteMembersAction() {
            putValue(SHORT_DESCRIPTION, tr("Download selected incomplete members"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs/relation", "downloadincompleteselected"));
            putValue(NAME, tr("Download Members"));
        //  Shortcut.register Shortcut("relationeditor:downloadincomplete", tr("Relation Editor: Download Members"),
        //      KeyEvent.VK_K, Shortcut.ALT)
            updateEnabledState();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (!isEnabled())
                return;
            Main.worker.submit(new DownloadRelationMemberTask(
                    getRelation(),
                    memberTableModel.getSelectedIncompleteMemberPrimitives(),
                    getLayer(),
                    GenericRelationEditor.this)
            );
        }

        protected void updateEnabledState() {
            setEnabled(memberTableModel.hasIncompleteSelectedMembers() && !Main.isOffline(OnlineResource.OSM_API));
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            updateEnabledState();
        }

        @Override
        public void tableChanged(TableModelEvent e) {
            updateEnabledState();
        }
    }

    class SetRoleAction extends AbstractAction implements ListSelectionListener, DocumentListener {
        public SetRoleAction() {
            putValue(SHORT_DESCRIPTION, tr("Sets a role for the selected members"));
            putValue(SMALL_ICON, ImageProvider.get("apply"));
            putValue(NAME, tr("Apply Role"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(memberTable.getSelectedRowCount() > 0);
        }

        protected boolean isEmptyRole() {
            return tfRole.getText() == null || tfRole.getText().trim().isEmpty();
        }

        protected boolean confirmSettingEmptyRole(int onNumMembers) {
            String message = "<html>"
                + trn("You are setting an empty role on {0} object.",
                        "You are setting an empty role on {0} objects.", onNumMembers, onNumMembers)
                        + "<br>"
                        + tr("This is equal to deleting the roles of these objects.") +
                        "<br>"
                        + tr("Do you really want to apply the new role?") + "</html>";
            String[] options = new String[] {
                    tr("Yes, apply it"),
                    tr("No, do not apply")
            };
            int ret = ConditionalOptionPaneUtil.showOptionDialog(
                    "relation_editor.confirm_applying_empty_role",
                    Main.parent,
                    message,
                    tr("Confirm empty role"),
                    JOptionPane.YES_NO_OPTION,
                    JOptionPane.WARNING_MESSAGE,
                    options,
                    options[0]
            );
            switch(ret) {
            case JOptionPane.YES_OPTION:
            case ConditionalOptionPaneUtil.DIALOG_DISABLED_OPTION:
                return true;
            default:
                return false;
            }
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (isEmptyRole()) {
                if (!confirmSettingEmptyRole(memberTable.getSelectedRowCount()))
                    return;
            }
            memberTableModel.updateRole(memberTable.getSelectedRows(), tfRole.getText());
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            refreshEnabled();
        }

        @Override
        public void changedUpdate(DocumentEvent e) {
            refreshEnabled();
        }

        @Override
        public void insertUpdate(DocumentEvent e) {
            refreshEnabled();
        }

        @Override
        public void removeUpdate(DocumentEvent e) {
            refreshEnabled();
        }
    }

    /**
     * Creates a new relation with a copy of the current editor state.
     */
    class DuplicateRelationAction extends AbstractAction {
        public DuplicateRelationAction() {
            putValue(SHORT_DESCRIPTION, tr("Create a copy of this relation and open it in another editor window"));
            // FIXME provide an icon
            putValue(SMALL_ICON, ImageProvider.get("duplicate"));
            putValue(NAME, tr("Duplicate"));
            setEnabled(true);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            Relation copy = new Relation();
            tagEditorPanel.getModel().applyToPrimitive(copy);
            memberTableModel.applyToRelation(copy);
            RelationEditor editor = RelationEditor.getEditor(getLayer(), copy, memberTableModel.getSelectedMembers());
            editor.setVisible(true);
        }
    }

    /**
     * Action for editing the currently selected relation.
     */
    class EditAction extends AbstractAction implements ListSelectionListener {
        public EditAction() {
            putValue(SHORT_DESCRIPTION, tr("Edit the relation the currently selected relation member refers to"));
            putValue(SMALL_ICON, ImageProvider.get("dialogs", "edit"));
            refreshEnabled();
        }

        protected void refreshEnabled() {
            setEnabled(memberTable.getSelectedRowCount() == 1
                    && memberTableModel.isEditableRelation(memberTable.getSelectedRow()));
        }

        protected Collection<RelationMember> getMembersForCurrentSelection(Relation r) {
            Collection<RelationMember> members = new HashSet<>();
            Collection<OsmPrimitive> selection = getLayer().data.getSelected();
            for (RelationMember member: r.getMembers()) {
                if (selection.contains(member.getMember())) {
                    members.add(member);
                }
            }
            return members;
        }

        public void run() {
            int idx = memberTable.getSelectedRow();
            if (idx < 0)
                return;
            OsmPrimitive primitive = memberTableModel.getReferredPrimitive(idx);
            if (!(primitive instanceof Relation))
                return;
            Relation r = (Relation) primitive;
            if (r.isIncomplete())
                return;

            RelationEditor editor = RelationEditor.getEditor(getLayer(), r, getMembersForCurrentSelection(r));
            editor.setVisible(true);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (!isEnabled())
                return;
            run();
        }

        @Override
        public void valueChanged(ListSelectionEvent e) {
            refreshEnabled();
        }
    }

    class PasteMembersAction extends AddFromSelectionAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                List<PrimitiveData> primitives = Main.pasteBuffer.getDirectlyAdded();
                DataSet ds = getLayer().data;
                List<OsmPrimitive> toAdd = new ArrayList<>();
                boolean hasNewInOtherLayer = false;

                for (PrimitiveData primitive: primitives) {
                    OsmPrimitive primitiveInDs = ds.getPrimitiveById(primitive);
                    if (primitiveInDs != null) {
                        toAdd.add(primitiveInDs);
                    } else if (!primitive.isNew()) {
                        OsmPrimitive p = primitive.getType().newInstance(primitive.getUniqueId(), true);
                        ds.addPrimitive(p);
                        toAdd.add(p);
                    } else {
                        hasNewInOtherLayer = true;
                        break;
                    }
                }

                if (hasNewInOtherLayer) {
                    JOptionPane.showMessageDialog(Main.parent,
                            tr("Members from paste buffer cannot be added because they are not included in current layer"));
                    return;
                }

                toAdd = filterConfirmedPrimitives(toAdd);
                int index = memberTableModel.getSelectionModel().getMaxSelectionIndex();
                if (index == -1) {
                    index = memberTableModel.getRowCount() - 1;
                }
                memberTableModel.addMembersAfterIdx(toAdd, index);

                tfRole.requestFocusInWindow();

            } catch (AddAbortException ex) {
                // Do nothing
            }
        }
    }

    class CopyMembersAction extends AbstractAction {
        @Override
        public void actionPerformed(ActionEvent e) {
            Set<OsmPrimitive> primitives = new HashSet<>();
            for (RelationMember rm: memberTableModel.getSelectedMembers()) {
                primitives.add(rm.getMember());
            }
            if (!primitives.isEmpty()) {
                CopyAction.copy(getLayer(), primitives);
            }
        }
    }

    class MemberTableDblClickAdapter extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
                new EditAction().run();
            }
        }
    }
}
