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

import java.awt.Point;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;

/**
 * RelationDialogManager keeps track of the open relation editors.
 *
 */
public class RelationDialogManager extends WindowAdapter implements MapView.LayerChangeListener{

    /** keeps track of open relation editors */
    private static RelationDialogManager relationDialogManager;

    /**
     * Replies the singleton {@link RelationDialogManager}
     *
     * @return the singleton {@link RelationDialogManager}
     */
    public static RelationDialogManager getRelationDialogManager() {
        if (RelationDialogManager.relationDialogManager == null) {
            RelationDialogManager.relationDialogManager = new RelationDialogManager();
            MapView.addLayerChangeListener(RelationDialogManager.relationDialogManager);
        }
        return RelationDialogManager.relationDialogManager;
    }

    /**
     * Helper class for keeping the context of a relation editor. A relation editor
     * is open for a specific relation managed by a specific {@link OsmDataLayer}
     *
     */
    private static class DialogContext {
        public final Relation relation;
        public final OsmDataLayer layer;

        public DialogContext(OsmDataLayer layer, Relation relation) {
            this.layer = layer;
            this.relation = relation;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((layer == null) ? 0 : layer.hashCode());
            result = prime * result + ((relation == null) ? 0 : relation.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            DialogContext other = (DialogContext) obj;
            if (layer == null) {
                if (other.layer != null)
                    return false;
            } else if (!layer.equals(other.layer))
                return false;
            if (relation == null) {
                if (other.relation != null)
                    return false;
            } else if (!relation.equals(other.relation))
                return false;
            return true;
        }

        public boolean matchesLayer(OsmDataLayer layer) {
            if (layer == null) return false;
            return this.layer.equals(layer);
        }

        @Override
        public String toString() {
            return "[Context: layer=" + layer.getName() + ",relation=" + relation.getId() + "]";
        }
    }

    /** the map of open dialogs */
    private final Map<DialogContext, RelationEditor> openDialogs;

    /**
     * constructor
     */
    public RelationDialogManager() {
        openDialogs = new HashMap<>();
    }
    /**
     * Register the relation editor for a relation managed by a
     * {@link OsmDataLayer}.
     *
     * @param layer the layer
     * @param relation the relation
     * @param editor the editor
     */
    public void register(OsmDataLayer layer, Relation relation, RelationEditor editor) {
        if (relation == null) {
            relation = new Relation();
        }
        DialogContext context = new DialogContext(layer, relation);
        openDialogs.put(context, editor);
        editor.addWindowListener(this);
    }

    public void updateContext(OsmDataLayer layer, Relation relation, RelationEditor editor) {
        // lookup the entry for editor and remove it
        //
        for (Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); it.hasNext();) {
            Entry<DialogContext, RelationEditor> entry = it.next();
            if (Objects.equals(entry.getValue(), editor)) {
                it.remove();
                break;
            }
        }
        // don't add a window listener. Editor is already known to the relation dialog manager
        //
        DialogContext context = new DialogContext(layer, relation);
        openDialogs.put(context, editor);
    }

    /**
     * Closes the editor open for a specific layer and a specific relation.
     *
     * @param layer  the layer
     * @param relation the relation
     */
    public void close(OsmDataLayer layer, Relation relation) {
        DialogContext context = new DialogContext(layer, relation);
        RelationEditor editor = openDialogs.get(context);
        if (editor != null) {
            editor.setVisible(false);
        }
    }

    /**
     * Replies true if there is an open relation editor for the relation managed
     * by the given layer. Replies false if relation is null.
     *
     * @param layer  the layer
     * @param relation  the relation. May be null.
     * @return true if there is an open relation editor for the relation managed
     * by the given layer; false otherwise
     */
    public boolean isOpenInEditor(OsmDataLayer layer, Relation relation) {
        if (relation == null) return false;
        DialogContext context = new DialogContext(layer, relation);
        return openDialogs.keySet().contains(context);

    }

    /**
     * Replies the editor for the relation managed by layer. Null, if no such editor
     * is currently open. Returns null, if relation is null.
     *
     * @param layer the layer
     * @param relation the relation
     * @return the editor for the relation managed by layer. Null, if no such editor
     * is currently open.
     *
     * @see #isOpenInEditor(OsmDataLayer, Relation)
     */
    public RelationEditor getEditorForRelation(OsmDataLayer layer, Relation relation) {
        if (relation == null) return null;
        DialogContext context = new DialogContext(layer, relation);
        return openDialogs.get(context);
    }

    /**
     * called when a layer is removed
     *
     */
    @Override
    public void layerRemoved(Layer oldLayer) {
        if (!(oldLayer instanceof OsmDataLayer))
            return;
        OsmDataLayer dataLayer = (OsmDataLayer) oldLayer;

        Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator();
        while (it.hasNext()) {
            Entry<DialogContext, RelationEditor> entry = it.next();
            if (entry.getKey().matchesLayer(dataLayer)) {
                RelationEditor editor = entry.getValue();
                it.remove();
                editor.setVisible(false);
                editor.dispose();
            }
        }
    }

    @Override
    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
        // do nothing
    }

    @Override
    public void layerAdded(Layer newLayer) {
        // do nothing
    }

    @Override
    public void windowClosed(WindowEvent e) {
        RelationEditor editor = (RelationEditor) e.getWindow();
        for (Iterator<Entry<DialogContext, RelationEditor>> it = openDialogs.entrySet().iterator(); it.hasNext();) {
            if (editor.equals(it.next().getValue())) {
                it.remove();
                break;
            }
        }
    }

    /**
     * Replies true, if there is another open {@link RelationEditor} whose
     * upper left corner is close to <code>p</code>.
     *
     * @param p  the reference point to check
     * @return true, if there is another open {@link RelationEditor} whose
     * upper left corner is close to <code>p</code>.
     */
    protected boolean hasEditorWithCloseUpperLeftCorner(Point p, RelationEditor thisEditor) {
        for (RelationEditor editor: openDialogs.values()) {
            if (editor == thisEditor) {
                continue;
            }
            Point corner = editor.getLocation();
            if (p.x >= corner.x -5 && corner.x + 5 >= p.x
                    && p.y >= corner.y -5 && corner.y + 5 >= p.y)
                return true;
        }
        return false;
    }

    /**
     * Positions a {@link RelationEditor} on the screen. Tries to center it on the
     * screen. If it hide another instance of an editor at the same position this
     * method tries to reposition <code>editor</code> by moving it slightly down and
     * slightly to the right.
     *
     * @param editor the editor
     */
    public void positionOnScreen(RelationEditor editor) {
        if (editor == null) return;
        if (!openDialogs.isEmpty()) {
            Point corner = editor.getLocation();
            while (hasEditorWithCloseUpperLeftCorner(corner, editor)) {
                // shift a little, so that the dialogs are not exactly on top of each other
                corner.x += 20;
                corner.y += 20;
            }
            editor.setLocation(corner);
        }
    }

}
