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

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

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Observable;

import javax.swing.table.DefaultTableModel;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
import org.openstreetmap.josm.data.osm.event.DataSetListener;
import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
import org.openstreetmap.josm.data.osm.history.History;
import org.openstreetmap.josm.data.osm.history.HistoryNode;
import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
import org.openstreetmap.josm.data.osm.history.HistoryRelation;
import org.openstreetmap.josm.data.osm.history.HistoryWay;
import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.CheckParameterUtil;

/**
 * This is the model used by the history browser.
 *
 * The model state consists of the following elements:
 * <ul>
 *   <li>the {@see History} of a specific {@see OsmPrimitive}</li>
 *   <li>a dedicated version in this {@see History} called the {@see PointInTimeType#REFERENCE_POINT_IN_TIME}</li>
 *   <li>another version in this {@see History} called the {@see PointInTimeType#CURRENT_POINT_IN_TIME}</li>
 * <ul>
 * {@see HistoryBrowser} always compares the {@see PointInTimeType#REFERENCE_POINT_IN_TIME} with the
 * {@see PointInTimeType#CURRENT_POINT_IN_TIME}.

 * This model provides various {@see TableModel}s for {@see JTable}s used in {@see HistoryBrowser}, for
 * instance:
 * <ul>
 *  <li>{@see #getTagTableModel(PointInTimeType)} replies a {@see TableModel} for the tags of either of
 *   the two selected versions</li>
 *  <li>{@see #getNodeListTableModel(PointInTimeType)} replies a {@see TableModel} for the list of nodes of
 *   the two selected versions (if the current history provides information about a {@see Way}</li>
 *  <li> {@see #getRelationMemberTableModel(PointInTimeType)} replies a {@see TableModel} for the list of relation
 *  members  of the two selected versions (if the current history provides information about a {@see Relation}</li>
 *  </ul>
 *
 * @see HistoryBrowser
 */
public class HistoryBrowserModel extends Observable implements LayerChangeListener, DataSetListener {
    /** the history of an OsmPrimitive */
    private History history;
    private HistoryOsmPrimitive reference;
    private HistoryOsmPrimitive current;
    /**
     * latest isn't a reference of history. It's a clone of the currently edited
     * {@see OsmPrimitive} in the current edit layer.
     */
    private HistoryOsmPrimitive latest;

    private VersionTableModel versionTableModel;
    private TagTableModel currentTagTableModel;
    private TagTableModel referenceTagTableModel;
    private NodeListTableModel currentNodeListTableModel;
    private NodeListTableModel referenceNodeListTableModel;
    private RelationMemberTableModel currentRelationMemberTableModel;
    private RelationMemberTableModel referenceRelationMemberTableModel;

    /**
     * constructor
     */
    public HistoryBrowserModel() {
        versionTableModel = new VersionTableModel();
        currentTagTableModel = new TagTableModel(PointInTimeType.CURRENT_POINT_IN_TIME);
        referenceTagTableModel = new TagTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME);
        currentNodeListTableModel = new NodeListTableModel(PointInTimeType.CURRENT_POINT_IN_TIME);
        referenceNodeListTableModel = new NodeListTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME);
        currentRelationMemberTableModel = new RelationMemberTableModel(PointInTimeType.CURRENT_POINT_IN_TIME);
        referenceRelationMemberTableModel = new RelationMemberTableModel(PointInTimeType.REFERENCE_POINT_IN_TIME);

        if (getEditLayer() != null) {
            getEditLayer().data.addDataSetListener(this);
        }
        MapView.addLayerChangeListener(this);
    }

    /**
     * Creates a new history browser model for a given history.
     *
     * @param history the history. Must not be null.
     * @throws IllegalArgumentException thrown if history is null
     */
    public HistoryBrowserModel(History history) {
        this();
        CheckParameterUtil.ensureParameterNotNull(history, "history");
        setHistory(history);
    }

    /**
     * Replies the current edit layer; null, if there isn't a current edit layer
     * of type {@see OsmDataLayer}.
     *
     * @return the current edit layer
     */
    protected OsmDataLayer getEditLayer() {
        try {
            return Main.map.mapView.getEditLayer();
        } catch(NullPointerException e) {
            return null;
        }
    }

    /**
     * replies the history managed by this model
     * @return the history
     */
    public History getHistory() {
        return history;
    }

    protected boolean hasNewNodes(Way way) {
        for (Node n: way.getNodes()) {
            if (n.isNew()) return true;
        }
        return false;
    }
    protected boolean canShowAsLatest(OsmPrimitive primitive) {
        if (primitive == null) return false;
        if (primitive.isNew()) return false;
        if (history == null) return false;
        // only show latest of the same version if it is
        // modified
        if (history.getByVersion(primitive.getVersion()) != null)
            return primitive.isModified();

        // latest has a higher version than one of the primitives
        // in the history (probably because the history got out of sync
        // with uploaded data) -> show the primitive as latest
        return true;
    }

    /**
     * sets the history to be managed by this model
     *
     * @param history the history
     *
     */
    public void setHistory(History history) {
        this.history = history;
        if (history.getNumVersions() > 0) {
            current = history.getEarliest();
            reference = history.getEarliest();
            setLatest(null);
            if (getEditLayer() != null) {
                OsmPrimitive p = getEditLayer().data.getPrimitiveById(history.getId(), history.getType());
                if (canShowAsLatest(p)) {
                    HistoryOsmPrimitive latest = new HistoryPrimitiveBuilder().build(p);
                    setLatest(latest);
                }
            }
        }
        initTagTableModels();
        fireModelChange();
    }

    protected void fireModelChange() {
        setChanged();
        notifyObservers();
        versionTableModel.fireTableDataChanged();
    }

    /**
     * Replies the table model to be used in a {@see JTable} which
     * shows the list of versions in this history.
     *
     * @return the table model
     */
    public VersionTableModel getVersionTableModel() {
        return versionTableModel;
    }

    protected void initTagTableModels() {
        currentTagTableModel.initKeyList();
        referenceTagTableModel.initKeyList();
    }

    protected void initNodeListTabeModels() {
        currentNodeListTableModel.fireTableDataChanged();
        referenceNodeListTableModel.fireTableDataChanged();
    }

    protected void initMemberListTableModels() {
        currentRelationMemberTableModel.fireTableDataChanged();
        referenceRelationMemberTableModel.fireTableDataChanged();
    }

    /**
     * replies the tag table model for the respective point in time
     *
     * @param pointInTimeType the type of the point in time (must not be null)
     * @return the tag table model
     * @exception IllegalArgumentException thrown, if pointInTimeType is null
     */
    public TagTableModel getTagTableModel(PointInTimeType pointInTimeType) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
            return currentTagTableModel;
        else if (pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME))
            return referenceTagTableModel;

        // should not happen
        return null;
    }

    public NodeListTableModel getNodeListTableModel(PointInTimeType pointInTimeType) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
            return currentNodeListTableModel;
        else if (pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME))
            return referenceNodeListTableModel;

        // should not happen
        return null;
    }

    public RelationMemberTableModel getRelationMemberTableModel(PointInTimeType pointInTimeType) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(pointInTimeType, "pointInTimeType");
        if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
            return currentRelationMemberTableModel;
        else if (pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME))
            return referenceRelationMemberTableModel;

        // should not happen
        return null;
    }

    /**
     * Sets the {@see HistoryOsmPrimitive} which plays the role of a reference point
     * in time (see {@see PointInTimeType}).
     *
     * @param reference the reference history primitive. Must not be null.
     * @throws IllegalArgumentException thrown if reference is null
     * @throws IllegalStateException thrown if this model isn't a assigned a history yet
     * @throws IllegalArgumentException if reference isn't an history primitive for the history managed by this mode
     *
     * @see #setHistory(History)
     * @see PointInTimeType
     */
    public void setReferencePointInTime(HistoryOsmPrimitive reference) throws IllegalArgumentException, IllegalStateException{
        CheckParameterUtil.ensureParameterNotNull(reference, "reference");
        if (history == null)
            throw new IllegalStateException(tr("History not initialized yet. Failed to set reference primitive."));
        if (reference.getId() != history.getId())
            throw new IllegalArgumentException(tr("Failed to set reference. Reference ID {0} does not match history ID {1}.", reference.getId(),  history.getId()));
        HistoryOsmPrimitive primitive = history.getByVersion(reference.getVersion());
        if (primitive == null)
            throw new IllegalArgumentException(tr("Failed to set reference. Reference version {0} not available in history.", reference.getVersion()));

        this.reference = reference;
        initTagTableModels();
        initNodeListTabeModels();
        initMemberListTableModels();
        setChanged();
        notifyObservers();
    }

    /**
     * Sets the {@see HistoryOsmPrimitive} which plays the role of the current point
     * in time (see {@see PointInTimeType}).
     *
     * @param reference the reference history primitive. Must not be null.
     * @throws IllegalArgumentException thrown if reference is null
     * @throws IllegalStateException thrown if this model isn't a assigned a history yet
     * @throws IllegalArgumentException if reference isn't an history primitive for the history managed by this mode
     *
     * @see #setHistory(History)
     * @see PointInTimeType
     */
    public void setCurrentPointInTime(HistoryOsmPrimitive current) throws IllegalArgumentException, IllegalStateException{
        CheckParameterUtil.ensureParameterNotNull(current, "current");
        if (history == null)
            throw new IllegalStateException(tr("History not initialized yet. Failed to set current primitive."));
        if (current.getId() != history.getId())
            throw new IllegalArgumentException(tr("Failed to set reference. Reference ID {0} does not match history ID {1}.", current.getId(),  history.getId()));
        HistoryOsmPrimitive primitive = history.getByVersion(current.getVersion());
        if (primitive == null)
            throw new IllegalArgumentException(tr("Failed to set current primitive. Current version {0} not available in history.", current.getVersion()));
        this.current = current;
        initTagTableModels();
        initNodeListTabeModels();
        initMemberListTableModels();
        setChanged();
        notifyObservers();
    }

    /**
     * Replies the history OSM primitive for the {@see PointInTimeType#CURRENT_POINT_IN_TIME}
     *
     * @return the history OSM primitive for the {@see PointInTimeType#CURRENT_POINT_IN_TIME} (may be null)
     */
    public HistoryOsmPrimitive getCurrentPointInTime() {
        return getPointInTime(PointInTimeType.CURRENT_POINT_IN_TIME);
    }

    /**
     * Replies the history OSM primitive for the {@see PointInTimeType#REFERENCE_POINT_IN_TIME}
     *
     * @return the history OSM primitive for the {@see PointInTimeType#REFERENCE_POINT_IN_TIME} (may be null)
     */
    public HistoryOsmPrimitive getReferencePointInTime() {
        return getPointInTime(PointInTimeType.REFERENCE_POINT_IN_TIME);
    }

    /**
     * replies the history OSM primitive for a given point in time
     *
     * @param type the type of the point in time (must not be null)
     * @return the respective primitive. Can be null.
     * @exception IllegalArgumentException thrown, if type is null
     */
    public HistoryOsmPrimitive getPointInTime(PointInTimeType type) throws IllegalArgumentException  {
        CheckParameterUtil.ensureParameterNotNull(type, "type");
        if (type.equals(PointInTimeType.CURRENT_POINT_IN_TIME))
            return current;
        else if (type.equals(PointInTimeType.REFERENCE_POINT_IN_TIME))
            return reference;

        // should not happen
        return null;
    }

    /**
     * Returns true if <code>primitive</code> is the latest primitive
     * representing the version currently edited in the current data
     * layer.
     *
     * @param primitive the primitive to check
     * @return true if <code>primitive</code> is the latest primitive
     */
    public boolean isLatest(HistoryOsmPrimitive primitive) {
        if (primitive == null) return false;
        return primitive == latest;
    }

    /**
     * The table model for the list of versions in the current history
     *
     */
    public class VersionTableModel extends DefaultTableModel{

        private VersionTableModel() {
        }

        @Override
        public int getRowCount() {
            if (history == null)
                return 0;
            int ret = history.getNumVersions();
            if (latest != null) {
                ret++;
            }
            return ret;
        }

        @Override
        public Object getValueAt(int row, int column) {
            if(history == null)
                return null;
            if (row < history.getNumVersions())
                return history.get(row);
            if (row == history.getNumVersions())
                return latest;
            return null;
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        public void setReferencePointInTime(int row) {
            if (history == null) return;
            if (row == history.getNumVersions()) {
                if (latest != null) {
                    HistoryBrowserModel.this.setReferencePointInTime(latest);
                }
                return;
            }
            if (row < 0 || row > history.getNumVersions()) return;
            HistoryOsmPrimitive reference = history.get(row);
            HistoryBrowserModel.this.setReferencePointInTime(reference);
        }

        public void setCurrentPointInTime(int row) {
            if (history == null) return;
            if (row == history.getNumVersions()) {
                if (latest != null) {
                    HistoryBrowserModel.this.setCurrentPointInTime(latest);
                }
                return;
            }
            if (row < 0 || row > history.getNumVersions()) return;
            HistoryOsmPrimitive current = history.get(row);
            HistoryBrowserModel.this.setCurrentPointInTime(current);
        }

        public boolean isReferencePointInTime(int row) {
            if (history == null) return false;
            if (row == history.getNumVersions())
                return latest == reference;
            if (row < 0 || row > history.getNumVersions()) return false;
            HistoryOsmPrimitive p = history.get(row);
            return p == reference;
        }

        public HistoryOsmPrimitive getPrimitive(int row) {
            return isLatest(row) ? latest : history.get(row);
        }

        public boolean isLatest(int row) {
            return row >= history.getNumVersions();
        }

        public OsmPrimitive getLatest() {
            if (latest == null) return null;
            if (getEditLayer() == null) return null;
            OsmPrimitive p = getEditLayer().data.getPrimitiveById(latest.getId(), latest.getType());
            return p;
        }
    }

    /**
     * The table model for the tags of the version at {@see PointInTimeType#REFERENCE_POINT_IN_TIME}
     * or {@see PointInTimeType#CURRENT_POINT_IN_TIME}
     *
     */
    public class TagTableModel extends DefaultTableModel {

        private ArrayList<String> keys;
        private PointInTimeType pointInTimeType;

        protected void initKeyList() {
            HashSet<String> keySet = new HashSet<String>();
            if (current != null) {
                keySet.addAll(current.getTags().keySet());
            }
            if (reference != null) {
                keySet.addAll(reference.getTags().keySet());
            }
            keys = new ArrayList<String>(keySet);
            Collections.sort(keys);
            fireTableDataChanged();
        }

        protected TagTableModel(PointInTimeType type) {
            pointInTimeType = type;
            initKeyList();
        }

        @Override
        public int getRowCount() {
            if (keys == null) return 0;
            return keys.size();
        }

        @Override
        public Object getValueAt(int row, int column) {
            return keys.get(row);
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        public boolean hasTag(String key) {
            HistoryOsmPrimitive primitive = getPointInTime(pointInTimeType);
            if (primitive == null)
                return false;
            return primitive.hasTag(key);
        }

        public String getValue(String key) {
            HistoryOsmPrimitive primitive = getPointInTime(pointInTimeType);
            if (primitive == null)
                return null;
            return primitive.get(key);
        }

        public boolean oppositeHasTag(String key) {
            PointInTimeType opposite = pointInTimeType.opposite();
            HistoryOsmPrimitive primitive = getPointInTime(opposite);
            if (primitive == null)
                return false;
            return primitive.hasTag(key);
        }

        public String getOppositeValue(String key) {
            PointInTimeType opposite = pointInTimeType.opposite();
            HistoryOsmPrimitive primitive = getPointInTime(opposite);
            if (primitive == null)
                return null;
            return primitive.get(key);
        }

        public boolean hasSameValueAsOpposite(String key) {
            String value = getValue(key);
            String oppositeValue = getOppositeValue(key);
            if (value == null || oppositeValue == null)
                return false;
            return value.equals(oppositeValue);
        }

        public PointInTimeType getPointInTimeType() {
            return pointInTimeType;
        }

        public boolean isCurrentPointInTime() {
            return pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME);
        }

        public boolean isReferencePointInTime() {
            return pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME);
        }
    }

    /**
     * The table model for the nodes of the version at {@see PointInTimeType#REFERENCE_POINT_IN_TIME}
     * or {@see PointInTimeType#CURRENT_POINT_IN_TIME}
     *
     */
    public class NodeListTableModel extends DefaultTableModel {

        private PointInTimeType pointInTimeType;

        private NodeListTableModel(PointInTimeType pointInTimeType) {
            this.pointInTimeType = pointInTimeType;
        }

        @Override
        public int getRowCount() {
            int n = 0;
            if (current != null && current.getType().equals(OsmPrimitiveType.WAY)) {
                n = ((HistoryWay)current).getNumNodes();
            }
            if (reference != null && reference.getType().equals(OsmPrimitiveType.WAY)) {
                n = Math.max(n,((HistoryWay)reference).getNumNodes());
            }
            return n;
        }

        protected HistoryWay getWay() {
            if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME)) {
                if (! current.getType().equals(OsmPrimitiveType.WAY))
                    return null;
                return (HistoryWay)current;
            }
            if (pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME)) {
                if (! reference.getType().equals(OsmPrimitiveType.WAY))
                    return null;
                return (HistoryWay)reference;
            }

            // should not happen
            return null;
        }

        protected HistoryWay getOppositeWay() {
            PointInTimeType opposite = pointInTimeType.opposite();
            if (opposite.equals(PointInTimeType.CURRENT_POINT_IN_TIME)) {
                if (! current.getType().equals(OsmPrimitiveType.WAY))
                    return null;
                return (HistoryWay)current;
            }
            if (opposite.equals(PointInTimeType.REFERENCE_POINT_IN_TIME)) {
                if (! reference.getType().equals(OsmPrimitiveType.WAY))
                    return null;
                return (HistoryWay)reference;
            }

            // should not happen
            return null;
        }

        @Override
        public Object getValueAt(int row, int column) {
            HistoryWay way = getWay();
            if (way == null)
                return null;
            if (row >= way.getNumNodes())
                return null;
            return way.getNodes().get(row);
        }

        public PrimitiveId getNodeId(int row) {
            HistoryWay way = getWay();
            if (way == null) return null;
            if (row > way.getNumNodes()) return null;
            return new SimplePrimitiveId(way.getNodeId(row), OsmPrimitiveType.NODE);
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        public boolean isSameInOppositeWay(int row) {
            HistoryWay thisWay = getWay();
            HistoryWay oppositeWay = getOppositeWay();
            if (thisWay == null || oppositeWay == null)
                return false;
            if (row >= oppositeWay.getNumNodes())
                return false;
            return thisWay.getNodeId(row) == oppositeWay.getNodeId(row);
        }

        public boolean isInOppositeWay(int row) {
            HistoryWay thisWay = getWay();
            HistoryWay oppositeWay = getOppositeWay();
            if (thisWay == null || oppositeWay == null)
                return false;
            return oppositeWay.getNodes().contains(thisWay.getNodeId(row));
        }
    }

    /**
     * The table model for the relation members of the version at {@see PointInTimeType#REFERENCE_POINT_IN_TIME}
     * or {@see PointInTimeType#CURRENT_POINT_IN_TIME}
     *
     */

    public class RelationMemberTableModel extends DefaultTableModel {

        private PointInTimeType pointInTimeType;

        private RelationMemberTableModel(PointInTimeType pointInTimeType) {
            this.pointInTimeType = pointInTimeType;
        }

        @Override
        public int getRowCount() {
            int n = 0;
            if (current != null && current.getType().equals(OsmPrimitiveType.RELATION)) {
                n = ((HistoryRelation)current).getNumMembers();
            }
            if (reference != null && reference.getType().equals(OsmPrimitiveType.RELATION)) {
                n = Math.max(n,((HistoryRelation)reference).getNumMembers());
            }
            return n;
        }

        protected HistoryRelation getRelation() {
            if (pointInTimeType.equals(PointInTimeType.CURRENT_POINT_IN_TIME)) {
                if (! current.getType().equals(OsmPrimitiveType.RELATION))
                    return null;
                return (HistoryRelation)current;
            }
            if (pointInTimeType.equals(PointInTimeType.REFERENCE_POINT_IN_TIME)) {
                if (! reference.getType().equals(OsmPrimitiveType.RELATION))
                    return null;
                return (HistoryRelation)reference;
            }

            // should not happen
            return null;
        }

        protected HistoryRelation getOppositeRelation() {
            PointInTimeType opposite = pointInTimeType.opposite();
            if (opposite.equals(PointInTimeType.CURRENT_POINT_IN_TIME)) {
                if (! current.getType().equals(OsmPrimitiveType.RELATION))
                    return null;
                return (HistoryRelation)current;
            }
            if (opposite.equals(PointInTimeType.REFERENCE_POINT_IN_TIME)) {
                if (! reference.getType().equals(OsmPrimitiveType.RELATION))
                    return null;
                return (HistoryRelation)reference;
            }

            // should not happen
            return null;
        }

        @Override
        public Object getValueAt(int row, int column) {
            HistoryRelation relation = getRelation();
            if (relation == null)
                return null;
            if (row >= relation.getNumMembers())
                return null;
            return relation.getMembers().get(row);
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }

        public boolean isSameInOppositeWay(int row) {
            HistoryRelation thisRelation = getRelation();
            HistoryRelation oppositeRelation = getOppositeRelation();
            if (thisRelation == null || oppositeRelation == null)
                return false;
            if (row >= oppositeRelation.getNumMembers())
                return false;
            return
            thisRelation.getMembers().get(row).getPrimitiveId() == oppositeRelation.getMembers().get(row).getPrimitiveId()
            &&  thisRelation.getMembers().get(row).getRole().equals(oppositeRelation.getMembers().get(row).getRole());
        }

        public boolean isInOppositeWay(int row) {
            HistoryRelation thisRelation = getRelation();
            HistoryRelation oppositeRelation = getOppositeRelation();
            if (thisRelation == null || oppositeRelation == null)
                return false;
            return oppositeRelation.getMembers().contains(thisRelation.getMembers().get(row));
        }
    }

    protected void setLatest(HistoryOsmPrimitive latest) {
        if (latest == null) {
            if (this.current == this.latest) {
                this.current = history.getLatest();
            }
            if (this.reference == this.latest) {
                this.current = history.getLatest();
            }
            this.latest = null;
        } else {
            if (this.current == this.latest) {
                this.current = latest;
            }
            if (this.reference == this.latest) {
                this.reference = latest;
            }
            this.latest = latest;
        }
        fireModelChange();
    }

    /**
     * Removes this model as listener for data change and layer change
     * events.
     *
     */
    public void unlinkAsListener() {
        if (getEditLayer() != null) {
            getEditLayer().data.removeDataSetListener(this);
        }
        MapView.removeLayerChangeListener(this);
    }

    /* ---------------------------------------------------------------------- */
    /* DataSetListener                                                        */
    /* ---------------------------------------------------------------------- */
    public void nodeMoved(NodeMovedEvent event) {
        Node node = event.getNode();
        if (!node.isNew() && node.getId() == history.getId()) {
            setLatest(new HistoryPrimitiveBuilder().build(node));
        }
    }

    public void primitivesAdded(PrimitivesAddedEvent event) {
        for (OsmPrimitive p: event.getPrimitives()) {
            if (canShowAsLatest(p)) {
                setLatest(new HistoryPrimitiveBuilder().build(p));
            }
        }
    }

    public void primitivesRemoved(PrimitivesRemovedEvent event) {
        for (OsmPrimitive p: event.getPrimitives()) {
            if (!p.isNew() && p.getId() == history.getId()) {
                setLatest(null);
            }
        }
    }

    public void relationMembersChanged(RelationMembersChangedEvent event) {
        Relation r = event.getRelation();
        if (!r.isNew() && r.getId() == history.getId()) {
            setLatest(new HistoryPrimitiveBuilder().build(r));
        }
    }

    public void tagsChanged(TagsChangedEvent event) {
        OsmPrimitive prim = event.getPrimitive();
        if (!prim.isNew() && prim.getId() == history.getId()) {
            setLatest(new HistoryPrimitiveBuilder().build(prim));
        }
    }

    public void wayNodesChanged(WayNodesChangedEvent event) {
        Way way = event.getChangedWay();
        if (!way.isNew() && way.getId() == history.getId()) {
            setLatest(new HistoryPrimitiveBuilder().build(way));
        }
    }

    public void dataChanged(DataChangedEvent event) {
        OsmPrimitive primitive = event.getDataset().getPrimitiveById(history.getId(), history.getType());
        HistoryOsmPrimitive latest;
        if (canShowAsLatest(primitive)) {
            latest = new HistoryPrimitiveBuilder().build(primitive);
        } else {
            latest = null;
        }
        setLatest(latest);
        fireModelChange();
    }

    public void otherDatasetChange(AbstractDatasetChangedEvent event) {
        // Irrelevant
    }

    /* ---------------------------------------------------------------------- */
    /* LayerChangeListener                                                    */
    /* ---------------------------------------------------------------------- */
    public void activeLayerChange(Layer oldLayer, Layer newLayer) {
        if (oldLayer != null && oldLayer instanceof OsmDataLayer) {
            OsmDataLayer l = (OsmDataLayer)oldLayer;
            l.data.removeDataSetListener(this);
        }
        if (newLayer == null || ! (newLayer instanceof OsmDataLayer)) {
            latest = null;
            fireModelChange();
            return;
        }
        OsmDataLayer l = (OsmDataLayer)newLayer;
        l.data.addDataSetListener(this);
        OsmPrimitive primitive = l.data.getPrimitiveById(history.getId(), history.getType());
        HistoryOsmPrimitive latest;
        if (canShowAsLatest(primitive)) {
            latest = new HistoryPrimitiveBuilder().build(primitive);
        } else {
            latest = null;
        }
        setLatest(latest);
        fireModelChange();
    }

    public void layerAdded(Layer newLayer) {}
    public void layerRemoved(Layer oldLayer) {}

    /**
     * Creates a {@see HistoryOsmPrimitive} from a {@see OsmPrimitive}
     *
     */
    static class HistoryPrimitiveBuilder extends AbstractVisitor {
        private HistoryOsmPrimitive clone;

        private String getUserName(OsmPrimitive primitive) {
            return primitive.getUser() == null?null:primitive.getUser().getName();
        }

        private long getUserId(OsmPrimitive primitive) {
            return primitive.getUser() == null?0:primitive.getUser().getId();
        }

        public void visit(Node n) {
            clone = new HistoryNode(n.getId(), n.getVersion(), n.isVisible(), getUserName(n), getUserId(n), 0, n.getTimestamp(), n.getCoor());
            clone.setTags(n.getKeys());
        }

        public void visit(Relation r) {
            clone = new HistoryRelation(r.getId(), r.getVersion(), r.isVisible(), getUserName(r), getUserId(r), 0, r.getTimestamp());
            clone.setTags(r.getKeys());
            HistoryRelation hr = (HistoryRelation)clone;
            for (RelationMember rm : r.getMembers()) {
                hr.addMember(new org.openstreetmap.josm.data.osm.history.RelationMember(rm.getRole(), rm.getType(), rm.getUniqueId()));
            }
        }

        public void visit(Way w) {
            clone = new HistoryWay(w.getId(), w.getVersion(), w.isVisible(), getUserName(w), getUserId(w), 0, w.getTimestamp());
            clone.setTags(w.getKeys());
            for (Node n: w.getNodes()) {
                ((HistoryWay)clone).addNode(n.getUniqueId());
            }
        }

        public HistoryOsmPrimitive build(OsmPrimitive primitive) {
            primitive.visit(this);
            return clone;
        }
    }
}
