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

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.Component;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.Observer;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
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 org.openstreetmap.josm.actions.AutoScaleAction;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
import org.openstreetmap.josm.data.UndoRedoHandler;
import org.openstreetmap.josm.data.conflict.Conflict;
import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.gui.HelpAwareOptionPane;
import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
import org.openstreetmap.josm.gui.MainApplication;
import org.openstreetmap.josm.gui.dialogs.relation.RelationEditor;
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.plugins.turnrestrictions.preferences.PreferenceKeys;
import org.openstreetmap.josm.plugins.turnrestrictions.qa.IssuesView;
import org.openstreetmap.josm.spi.preferences.Config;
import org.openstreetmap.josm.spi.preferences.PreferenceChangeEvent;
import org.openstreetmap.josm.spi.preferences.PreferenceChangedListener;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.ImageProvider;

public class TurnRestrictionEditor extends JDialog implements NavigationControler {

    /** The property name for the current turn restriction, see {@link #setTurnRestriction(Relation)} and
     * {@link #getTurnRestriction()}
     */
    public static final String TURN_RESTRICTION_PROP = RelationEditor.class.getName() + ".turnRestriction";

    /** the property name for the current relation snapshot, see {@link #getTurnRestrictionSnapshot()}
     */
    public static final String TURN_RESTRICTION_SNAPSHOT_PROP = RelationEditor.class.getName() + ".turnRestrictionSnapshot";

    /**
     * The turn restriction this editor is working on
     */
    protected Relation turnRestriction;

    /**
     * The version of the turn restriction when editing is started.  This is
     * null if a new turn restriction is created.
     */
    protected Relation turnRestrictionSnapshot;

    /** the data layer the turn restriction belongs to */
    private OsmDataLayer layer;

    private JosmSelectionPanel pnlJosmSelection;
    private BasicEditorPanel pnlBasicEditor;
    private TurnRestrictionEditorModel editorModel;
    private JTabbedPane tpEditors;
    private PreferenceChangeHandler preferenceChangeHandler;

    /**
     * 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));

        JButton b;
        pnl.add(b = new JButton(new OKAction()));
        b.setName("btnOK");
        pnl.add(b = new JButton(new CancelAction()));
        b.setName("btnCancel");
        pnl.add(b = new JButton(new ContextSensitiveHelpAction(ht("/Plugin/TurnRestrictions#TurnRestrictionEditor"))));
        b.setName("btnHelp");
        return pnl;
    }

    /**
     * builds the panel which displays the JOSM selection
     */
    protected JPanel buildJOSMSelectionPanel() {
        pnlJosmSelection = new JosmSelectionPanel(layer, editorModel.getJosmSelectionListModel());
        return pnlJosmSelection;
    }

    /**
     * Builds the panel with the editor forms (the left panel in the split pane of
     * this dialog)
     */
    protected JPanel buildEditorPanel() {
        JPanel pnl = new JPanel(new BorderLayout());
        tpEditors = new JTabbedPane();
        JScrollPane pane = new JScrollPane(pnlBasicEditor = new BasicEditorPanel(editorModel));
        pane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        pane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        tpEditors.add(pane);
        tpEditors.setTitleAt(0, tr("Basic"));
        tpEditors.setToolTipTextAt(0, tr("Edit basic attributes of a turn restriction"));

        tpEditors.add(new AdvancedEditorPanel(editorModel));
        tpEditors.setTitleAt(1, tr("Advanced"));
        tpEditors.setToolTipTextAt(1, tr("Edit the raw tags and members of this turn restriction"));

        tpEditors.add(new IssuesView(editorModel.getIssuesModel()));
        tpEditors.setTitleAt(2, tr("Errors/Warnings"));
        tpEditors.setToolTipTextAt(2, tr("Show errors and warnings related to this turn restriction"));

        pnl.add(tpEditors, BorderLayout.CENTER);
        return pnl;
    }

    /**
     * Builds the content panel, i.e. the core area of the dialog with the editor
     * masks and the JOSM selection view
     */
    protected JPanel buildContentPanel() {
        JPanel pnl = new JPanel(new BorderLayout());
        final JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
        pnl.add(sp, BorderLayout.CENTER);
        sp.setLeftComponent(buildEditorPanel());
        sp.setRightComponent(buildJOSMSelectionPanel());
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowOpened(WindowEvent e) {
                // has to be called when the window is visible, otherwise
                // no effect
                sp.setDividerLocation(0.7);
            }
        });

        return pnl;
    }

    /**
     * Creates the toolbar
     *
     * @return the toolbar
     */
    protected JToolBar buildToolBar() {
        JToolBar tb = new JToolBar();
        tb.setFloatable(false);
        tb.add(new ApplyAction());
        tb.addSeparator();
        DeleteAction actDelete = new DeleteAction();
        tb.add(actDelete);
        addPropertyChangeListener(actDelete);
        tb.addSeparator();
        SelectAction actSelect = new SelectAction();
        tb.add(actSelect);
        addPropertyChangeListener(actSelect);

        ZoomToAction actZoomTo = new ZoomToAction();
        tb.add(actZoomTo);
        addPropertyChangeListener(actZoomTo);
        return tb;
    }

    /**
     * Builds the UI
     */
    protected void build() {
        editorModel = new TurnRestrictionEditorModel(getLayer(), this);
        Container c = getContentPane();
        c.setLayout(new BorderLayout());
        c.add(buildToolBar(), BorderLayout.NORTH);
        c.add(buildContentPanel(), BorderLayout.CENTER);
        c.add(buildOkCancelButtonPanel(), BorderLayout.SOUTH);

        editorModel.getIssuesModel().addObserver(new IssuesModelObserver());
        setSize(600, 600);
    }

    /**
    * Creates a new turn restriction editor
    *
    * @param owner the component relative to which the dialog is displayed
    * @param layer  the {@link OsmDataLayer} in whose context a relation is edited. Must not be null.
    * @throws IllegalArgumentException thrown if layer is null
    */
    public TurnRestrictionEditor(Component owner, OsmDataLayer layer) {
        this(owner, layer, null);
    }

     /**
     * Creates a new turn restriction editor
     *
     * @param owner the component relative to which the dialog is displayed
     * @param layer  the {@link OsmDataLayer} in whose context a relation is edited. Must not be null.
     * @param turnRestriction the relation. Can be null if a new relation is to be edited.
     * @throws IllegalArgumentException thrown if layer is null
     */
    public TurnRestrictionEditor(Component owner, OsmDataLayer layer, Relation turnRestriction) throws IllegalArgumentException {
        super(JOptionPane.getFrameForComponent(owner), false /* not modal */);
        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
        this.layer = layer;
        build();
        setTurnRestriction(turnRestriction);
    }

    /**
     * Replies the currently edited turn restriction
     *
     * @return the currently edited relation
     */
    protected Relation getTurnRestriction() {
        return turnRestriction;
    }

    /**
     * <p>Sets the currently edited turn restriction. Creates a snapshot of the current
     * state of the turn restriction. See {@link #getTurnRestrictionSnapshot()}</p>
     *
     * <p>{@code turnRestriction} can be null if a new restriction is created. A turn
     * restriction which isn't assigned to a data set is allowed too. If {@code turnRestriction}
     * is already assigned to a dataset, the dataset of {@link #getLayer()} is required, otherwise
     * a {@link IllegalArgumentException} is thrown.</p>
     *
     * @param turnRestriction the turn restriction
     * @throws IllegalArgumentException thrown if {@code turnRestriction} belongs to a different dataset than
     * that owned by the layer {@link #getLayer()}
     */
    protected void setTurnRestriction(Relation turnRestriction) {
        if (turnRestriction == null) {
            editorModel.populate(new Relation());
        } else if (turnRestriction.getDataSet() == null || turnRestriction.getDataSet() == getLayer().data) {
            editorModel.populate(turnRestriction);
        } else {
            throw new IllegalArgumentException(MessageFormat.format("turnRestriction must belong to layer ''{0}''", getLayer().getName()));
        }
        setTurnRestrictionSnapshot(turnRestriction == null ? null : new Relation(turnRestriction));
        this.turnRestriction = turnRestriction;
        support.firePropertyChange(TURN_RESTRICTION_PROP, null, this.turnRestriction);
        updateTitle();
    }

    /**
     * updates the title of the turn restriction editor
     */
    protected void updateTitle() {
        if (getTurnRestriction() == null || getTurnRestriction().getDataSet() == null) {
            setTitle(tr("Create a new turn restriction in layer ''{0}''", layer.getName()));
        } else if (getTurnRestriction().isNew()) {
            setTitle(tr("Edit a new turn restriction in layer ''{0}''", layer.getName()));
        } else {
            setTitle(tr("Edit turn restriction ''{0}'' in layer ''{1}''", Long.toString(turnRestriction.getId()), layer.getName()));
        }
    }

    /**
     * Replies the {@link OsmDataLayer} in whose context this relation editor is
     * open
     *
     * @return the {@link OsmDataLayer} in whose context this relation editor is
     * open
     */
    protected OsmDataLayer getLayer() {
        return layer;
    }

    /**
     * Replies the state of the edited relation when the editor has been launched
     *
     * @return the state of the edited relation when the editor has been launched
     */
    protected Relation getTurnRestrictionSnapshot() {
        return turnRestrictionSnapshot;
    }

    /**
     * Sets the turn restriction snapshot
     *
     * @param snapshot the snapshot
     */
    protected void setTurnRestrictionSnapshot(Relation snapshot) {
        turnRestrictionSnapshot = snapshot;
        support.firePropertyChange(TURN_RESTRICTION_SNAPSHOT_PROP, null, turnRestrictionSnapshot);
    }

    /**
     * <p>Replies true if the currently edited turn restriction has been changed elsewhere.</p>
     *
     * <p>In this case a turn restriction editor can't apply updates to the turn restriction
     * directly. Rather, it has to create a conflict.</p>
     *
     * @return true if the currently edited turn restriction has been changed elsewhere.
     */
    protected boolean isDirtyTurnRestriction() {
        return !turnRestriction.hasEqualSemanticAttributes(turnRestrictionSnapshot);
    }

    /**
     * Replies the editor model for this editor
     */
    public TurnRestrictionEditorModel getModel() {
        return editorModel;
    }

    @Override
    public void setVisible(boolean visible) {
        if (visible && !isVisible()) {
            pnlJosmSelection.wireListeners();
            editorModel.registerAsEventListener();
            Config.getPref().addPreferenceChangeListener(this.preferenceChangeHandler = new PreferenceChangeHandler());
            pnlBasicEditor.initIconSetFromPreferences(Config.getPref());
        } else if (!visible && isVisible()) {
            pnlJosmSelection.unwireListeners();
            editorModel.unregisterAsEventListener();
            Config.getPref().removePreferenceChangeListener(preferenceChangeHandler);
        }
        super.setVisible(visible);
        if (!visible) {
            dispose();
        }
    }

    /* ----------------------------------------------------------------------- */
    /* property change support                                                 */
    /* ----------------------------------------------------------------------- */
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);

    @Override
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        this.support.addPropertyChangeListener(listener);
    }

    @Override
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        this.support.removePropertyChangeListener(listener);
    }

    /* ----------------------------------------------------------------------- */
    /* interface NavigationControler                                           */
    /* ----------------------------------------------------------------------- */
    @Override
    public void gotoBasicEditor() {
        tpEditors.setSelectedIndex(0);
    }

    @Override
    public void gotoAdvancedEditor() {
        tpEditors.setSelectedIndex(1);
    }

    @Override
    public void gotoBasicEditor(BasicEditorFokusTargets focusTarget) {
        tpEditors.setSelectedIndex(0);
        pnlBasicEditor.requestFocusfor(focusTarget);
    }

    /**
     * The abstract base action for applying the updates of a turn restriction
     * to the dataset.
     */
    abstract class SavingAction extends AbstractAction {
        protected boolean confirmSaveDespiteOfErrorsAndWarnings() {
            int numErrors = editorModel.getIssuesModel().getNumErrors();
            int numWarnings = editorModel.getIssuesModel().getNumWarnings();
            if (numErrors + numWarnings == 0) return true;

            StringBuffer sb = new StringBuffer();
            sb.append("<html>");
            sb.append(trn(
                "There is still an unresolved error or warning identified for this turn restriction. "
                    + "You are recommended to resolve this issue first.",
                  "There are still {0} errors and/or warnings identified for this turn restriction. "
                    + "You are recommended to resolve these issues first.",
                  numErrors + numWarnings,
                  numErrors + numWarnings
            ));
            sb.append("<br>");
            sb.append(tr("Do you want to save anyway?"));
            ButtonSpec[] options = new ButtonSpec[] {
                    new ButtonSpec(
                            tr("Yes, save anyway"),
                            ImageProvider.get("ok"),
                            tr("Save the turn restriction despite of errors and/or warnings"),
                            null // no specific help topic
                    ),
                    new ButtonSpec(
                            tr("No, resolve issues first"),
                            ImageProvider.get("cancel"),
                            tr("Cancel saving and start resolving pending issues first"),
                            null // no specific help topic
                    )
            };

            int ret = HelpAwareOptionPane.showOptionDialog(
                    JOptionPane.getFrameForComponent(TurnRestrictionEditor.this),
                    sb.toString(),
                    tr("Pending errors and warnings"),
                    JOptionPane.WARNING_MESSAGE,
                    null, // no special icon
                    options,
                    options[1], // cancel is default operation
                    HelpUtil.ht("/Plugin/TurnRestrictions#PendingErrorsAndWarnings")
            );
            return ret == 0 /* OK */;
        }

        /**
         * Replies the list of relation members in {@code r} which refer to
         * a deleted or invisible primitives.
         *
         * @param r the relation
         * @return the list of relation members in {@code r} which refer to
         * a deleted or invisible member
         */
        protected List<RelationMember> getDeletedRelationMembers(Relation r) {
            List<RelationMember> ret = new ArrayList<>();
            for (RelationMember rm: r.getMembers()) {
                if (rm.getMember().isDeleted() || !rm.getMember().isVisible()) {
                    ret.add(rm);
                }
            }
            return ret;
        }

        /**
         * Removes all members referring to deleted or invisible primitives
         * from the turn restriction {@code tr}.
         *
         * @param tr  the turn restriction
         */
        protected void removeDeletedMembers(Relation tr) {
            List<RelationMember> members = tr.getMembers();
            for (Iterator<RelationMember> it = members.iterator(); it.hasNext();) {
                RelationMember rm = it.next();
                if (rm.getMember().isDeleted() || !rm.getMember().isVisible()) {
                    it.remove();
                }
            }
            tr.setMembers(members);
        }

        /**
         * <p>Asks the user how to proceed if a turn restriction refers to deleted or invisible
         * primitives.</p>
         *
         * <p>If this method returns true the respective members should be removed and the turn
         * restriction should be saved anyway. If it replies false, the turn restriction must not
         * be saved. </p>
         *
         * @param deletedMembers the list of members referring to deleted or invisible primitives
         * @return the confirmation
         */
        protected boolean confirmSaveTurnRestrictionWithDeletePrimitives(List<RelationMember> deletedMembers) {
            StringBuffer sb = new StringBuffer();
            sb.append("<html>");
            sb.append(trn("This turn restriction refers to an object which was deleted outside "
                       + "of this turn restriction editor:",
                       "This turn restriction refers to {0} objects which were deleted outside "
                       + "of this turn restriction editor:", deletedMembers.size(), deletedMembers.size()));
            sb.append("<ul>");
            for (RelationMember rm: deletedMembers) {
                sb.append("<li>");
                if (!rm.getRole().equals("")) {
                    sb.append(rm.getRole()).append(": ");
                }
                sb.append(rm.getMember().getDisplayName(DefaultNameFormatter.getInstance()));
                sb.append("</li>");
            }
            sb.append(tr("Updates to this turn restriction can''t be saved unless deleted members are removed.<br>"
                    + "How to you want to proceed?"));

            ButtonSpec[] options = new ButtonSpec[] {
                    new ButtonSpec(
                        tr("Remove deleted members and save"),
                        ImageProvider.get("ok"),
                        tr("Remove deleted members and save"),
                        null
                     ),
                      new ButtonSpec(
                            tr("Cancel and return to editor"),
                            ImageProvider.get("cancel"),
                            tr("Cancel and return to editor"),
                            null
                       )
            };

            int ret = HelpAwareOptionPane.showOptionDialog(
                    TurnRestrictionEditor.this,
                    sb.toString(),
                    tr("Deleted members in turn restriction"),
                    JOptionPane.WARNING_MESSAGE,
                    null, // no special icon
                    options,
                    options[1], // cancel is default
                    null // FIXME: provide help topic
            );
            return ret == 0 /* OK button */;
        }

        /**
         * apply updates to a new turn restriction
         */
        protected boolean applyNewTurnRestriction() {
            Relation newTurnRestriction = new Relation();
            editorModel.apply(newTurnRestriction);

            // If the user wanted to create a new turn restriction, but didn't add any members or
            // tags, don't add an empty relation
            if (newTurnRestriction.getMembersCount() == 0 && !newTurnRestriction.hasKeys())
                return true;

            // check whether the turn restriction refers to new
            List<RelationMember> deletedMembers = getDeletedRelationMembers(newTurnRestriction);
            if (!deletedMembers.isEmpty()) {
                if (!confirmSaveTurnRestrictionWithDeletePrimitives(deletedMembers)) {
                    return false;
                }
                removeDeletedMembers(newTurnRestriction);
            }

            UndoRedoHandler.getInstance().add(new AddCommand(getLayer().getDataSet(), newTurnRestriction));

            // make sure everybody is notified about the changes
            //
            TurnRestrictionEditor.this.setTurnRestriction(newTurnRestriction);
            TurnRestrictionEditorManager.getInstance().updateContext(
                    getLayer(),
                    getTurnRestriction(),
                    TurnRestrictionEditor.this
            );
            return true;
        }

        /**
         * Apply the updates for an existing turn restriction which has been changed
         * outside of the turn restriction editor.
         *
         */
        protected void applyExistingConflictingTurnRestriction() {
            Relation toUpdate = new Relation(getTurnRestriction());
            editorModel.apply(toUpdate);
            Conflict<Relation> conflict = new Conflict<>(getTurnRestriction(), toUpdate);
            UndoRedoHandler.getInstance().add(new ConflictAddCommand(getLayer().getDataSet(), conflict));
        }

        /**
         * Apply the updates for an existing turn restriction which has not been changed
         * outside of the turn restriction editor.
         */
        protected void applyExistingNonConflictingTurnRestriction() {
            if (getTurnRestriction().getDataSet() == null) {
                editorModel.apply(getTurnRestriction());
                UndoRedoHandler.getInstance().add(new AddCommand(getLayer().getDataSet(), getTurnRestriction()));
            } else {
                Relation toUpdate = new Relation(getTurnRestriction());
                editorModel.apply(toUpdate);
                UndoRedoHandler.getInstance().add(new ChangeCommand(getTurnRestriction(), toUpdate));
            }
            // this will refresh the snapshot and update the dialog title
            //
            setTurnRestriction(getTurnRestriction());
        }

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

            int ret = HelpAwareOptionPane.showOptionDialog(
                    MainApplication.getMainFrame(),
                    tr("<html>This turn restriction 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
                            null // FIXME: provide help topic
            );
            return ret == 0;
        }

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

    class ApplyAction extends SavingAction {
        ApplyAction() {
            putValue(SHORT_DESCRIPTION, tr("Apply the current updates"));
            new ImageProvider("save").getResource().attachImageIcon(this);
            putValue(NAME, tr("Apply"));
            setEnabled(true);
        }

        public void run() {
            if (!confirmSaveDespiteOfErrorsAndWarnings()) {
                tpEditors.setSelectedIndex(2); // show the errors and warnings
                return;
            }
            if (getTurnRestriction() == null || getTurnRestriction().getDataSet() == null) {
                applyNewTurnRestriction();
                return;
            }

            Relation toUpdate = new Relation(getTurnRestriction());
            editorModel.apply(toUpdate);
            if (TurnRestrictionEditorModel.hasSameMembersAndTags(toUpdate, getTurnRestriction()))
                // nothing to update
                return;

            if (isDirtyTurnRestriction()) {
                if (confirmClosingBecauseOfDirtyState()) {
                    if (getLayer().getConflicts().hasConflictForMy(getTurnRestriction())) {
                        warnDoubleConflict();
                        return;
                    }
                    applyExistingConflictingTurnRestriction();
                    setVisible(false);
                }
            } else {
                applyExistingNonConflictingTurnRestriction();
            }
        }

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

    class OKAction extends SavingAction {
        OKAction() {
            putValue(SHORT_DESCRIPTION, tr("Apply the updates and close the dialog"));
            new ImageProvider("ok").getResource().attachImageIcon(this);
            putValue(NAME, tr("OK"));
            setEnabled(true);
        }

        public void run() {
            if (!confirmSaveDespiteOfErrorsAndWarnings()) {
                tpEditors.setSelectedIndex(2); // show the errors and warnings
                return;
            }
            if (getTurnRestriction() == null || getTurnRestriction().getDataSet() == null) {
                // it's a new turn restriction. Try to save it and close the dialog
                if (applyNewTurnRestriction()) {
                    setVisible(false);
                }
                return;
            }

            Relation toUpdate = new Relation(getTurnRestriction());
            editorModel.apply(toUpdate);
            if (TurnRestrictionEditorModel.hasSameMembersAndTags(toUpdate, getTurnRestriction())) {
                // nothing to update
                setVisible(false);
                return;
            }

            if (isDirtyTurnRestriction()) {
                // the turn restriction this editor is working on has changed outside
                // of the editor.
                if (confirmClosingBecauseOfDirtyState()) {
                    if (getLayer().getConflicts().hasConflictForMy(getTurnRestriction())) {
                        warnDoubleConflict();
                        return;
                    }
                    applyExistingConflictingTurnRestriction();
                } else
                    return;
            } else {
                applyExistingNonConflictingTurnRestriction();
            }
            setVisible(false);
        }

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

    /**
     * Action for canceling the current dialog
     */
    class CancelAction extends AbstractAction {
        CancelAction() {
            putValue(SHORT_DESCRIPTION, tr("Cancel the updates and close the dialog"));
            new ImageProvider("cancel").getResource().attachImageIcon(this);
            putValue(NAME, tr("Cancel"));
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke("ESCAPE"));
            TurnRestrictionEditor.this.getRootPane().registerKeyboardAction(this,
                    KeyStroke.getKeyStroke("ESCAPE"), JComponent.WHEN_IN_FOCUSED_WINDOW);
            setEnabled(true);
        }

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

    class DeleteAction extends AbstractAction implements PropertyChangeListener {
        DeleteAction() {
            putValue(NAME, tr("Delete"));
            putValue(SHORT_DESCRIPTION, tr("Delete this turn restriction"));
            new ImageProvider("dialogs", "delete").getResource().attachImageIcon(this);
            updateEnabledState();
        }

        protected void updateEnabledState() {
            Relation tr = getTurnRestriction();
            setEnabled(tr != null && tr.getDataSet() != null);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            Relation tr = getTurnRestriction();
            if (tr == null || tr.getDataSet() == null || tr.isDeleted()) return;
            org.openstreetmap.josm.actions.mapmode.DeleteAction.deleteRelation(
                    getLayer(),
                    tr
            );
            setVisible(false);
        }

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

    class SelectAction extends AbstractAction implements PropertyChangeListener {
        SelectAction() {
            putValue(NAME, tr("Select"));
            putValue(SHORT_DESCRIPTION, tr("Select this turn restriction"));
            new ImageProvider("dialogs", "select").getResource().attachImageIcon(this);
            updateEnabledState();
        }

        protected void updateEnabledState() {
            Relation tr = getTurnRestriction();
            setEnabled(tr != null && tr.getDataSet() != null);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            Relation tr = getTurnRestriction();
            if (tr == null || tr.getDataSet() == null) return;
            getLayer().data.setSelected(tr);
        }

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

    class ZoomToAction extends AbstractAction implements PropertyChangeListener {
        ZoomToAction() {
            putValue(NAME, tr("Zoom to"));
            putValue(SHORT_DESCRIPTION, tr("Activate the layer this turn restriction belongs to and zoom to it"));
            new ImageProvider("dialogs/autoscale", "data").getResource().attachImageIcon(this);
            updateEnabledState();
        }

        protected void updateEnabledState() {
            Relation tr = getTurnRestriction();
            setEnabled(tr != null && tr.getDataSet() != null);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            if (MainApplication.getLayerManager().getActiveLayer() != getLayer()) {
                MainApplication.getLayerManager().setActiveLayer(getLayer());
            }
            Relation tr = getTurnRestriction();
            if (tr == null || tr.getDataSet() == null) return;
            getLayer().data.setSelected(tr);
            AutoScaleAction.zoomToSelection();
        }

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

    class IssuesModelObserver implements Observer {
        @Override
        public void update(Observable o, Object arg) {
            int numWarnings = editorModel.getIssuesModel().getNumWarnings();
            int numErrors = editorModel.getIssuesModel().getNumErrors();
            String warningText = null;
            if (numWarnings > 0) {
                warningText = trn("{0} warning", "{0} warnings", numWarnings, numWarnings);
            }
            String errorText = null;
            if (numErrors > 0) {
                errorText = trn("{0} error", "{0} errors", numErrors, numErrors);
            }
            String title = "";
            if (errorText != null) {
                title += errorText;
            }
            if (warningText != null) {
                if (title.length() > 0) {
                    title += "/";
                }
                title += warningText;
            }
            if (title.length() == 0) {
                title = tr("no issues");
            }
            tpEditors.setTitleAt(2, title);
            tpEditors.setEnabledAt(2, numWarnings + numErrors > 0);
        }
    }

    /**
     * <p>Listens to changes of the preference {@link PreferenceKeys#ROAD_SIGNS}
     * and refreshes the set of road icons.</p>
     *
     * <p>Listens to changes of the preference {@link PreferenceKeys#SHOW_VIAS_IN_BASIC_EDITOR}
     * and toggles the visibility of the list of via-objects in the Basic
     * Editor.</p>
     *
     */
    class PreferenceChangeHandler implements PreferenceChangedListener {
        public void refreshIconSet() {
            pnlBasicEditor.initIconSetFromPreferences(Config.getPref());
        }

        @Override
        public void preferenceChanged(PreferenceChangeEvent evt) {
            if (evt.getKey().equals(PreferenceKeys.ROAD_SIGNS)) {
                refreshIconSet();
            } else if (evt.getKey().equals(PreferenceKeys.SHOW_VIAS_IN_BASIC_EDITOR)) {
                pnlBasicEditor.initViasVisibilityFromPreferences(Config.getPref());
            }
        }
    }
}
