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

import java.text.MessageFormat;
import java.util.List;
import java.util.Observable;
import java.util.Set;

import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;

import org.openstreetmap.josm.data.osm.DataSet;
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.TagCollection;
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.DatasetEventManager;
import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
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.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.tagging.TagEditorModel;
import org.openstreetmap.josm.gui.tagging.TagModel;
import org.openstreetmap.josm.plugins.turnrestrictions.qa.IssuesModel;
import org.openstreetmap.josm.tools.CheckParameterUtil;

/**
 * This is the model for the turn restriction editor. It keeps the editing state
 * for a single turn restriction.
 *
 */
public class TurnRestrictionEditorModel extends Observable implements DataSetListener {

    /**
     * Replies true if {@code tp1} and {@code tp2} have the same tags and
     * the same members
     *
     * @param tp1 a turn restriction. Must not be null.
     * @param tp2 a turn restriction . Must not be null.
     * @return true if {@code tp1} and {@code tp2} have the same tags and
     * the same members
     * @throws IllegalArgumentException thrown if {@code tp1} is null
     * @throws IllegalArgumentException thrown if {@code tp2} is null
     */
    public static boolean hasSameMembersAndTags(Relation tp1, Relation tp2) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(tp1, "tp1");
        CheckParameterUtil.ensureParameterNotNull(tp2, "tp2");
        if (!TagCollection.from(tp1).asSet().equals(TagCollection.from(tp2).asSet())) return false;
        if (tp1.getMembersCount() != tp2.getMembersCount()) return false;
        for (int i = 0; i < tp1.getMembersCount(); i++) {
            if (!tp1.getMember(i).equals(tp2.getMember(i))) return false;
        }
        return true;
    }

    private OsmDataLayer layer;
    private final TagEditorModel tagEditorModel = new TagEditorModel();
    private RelationMemberEditorModel memberModel;
    private IssuesModel issuesModel;
    private NavigationControler navigationControler;
    private JosmSelectionListModel selectionModel;

    /**
     * Creates a model in the context of a {@link OsmDataLayer}
     *
     * @param layer the layer. Must not be null.
     * @param navigationControler control to direct the user to specific UI components. Must not be null
     * @throws IllegalArgumentException thrown if {@code layer} is null
     */
    public TurnRestrictionEditorModel(OsmDataLayer layer, NavigationControler navigationControler) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
        CheckParameterUtil.ensureParameterNotNull(navigationControler, "navigationControler");
        this.layer = layer;
        this.navigationControler = navigationControler;
        memberModel = new RelationMemberEditorModel(layer);
        memberModel.addTableModelListener(new RelationMemberModelListener());
        issuesModel = new IssuesModel(this);
        addObserver(issuesModel);
        tagEditorModel.addTableModelListener(new TagEditorModelObserver());
        selectionModel = new JosmSelectionListModel(layer);
    }

    /**
     * Replies the model for the currently selected JOSM primitives
     */
    public JosmSelectionListModel getJosmSelectionListModel() {
        return selectionModel;
    }

    /**
     * Sets the way participating in the turn restriction in a given role.
     *
     * @param role the role. Must not be null.
     * @param way the way which participates in the turn restriction in the respective role.
     * null, to remove the way with the given role.
     * @exception IllegalArgumentException thrown if role is null
     */
    public void setTurnRestrictionLeg(TurnRestrictionLegRole role, Way way) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(role, "role");
        switch(role) {
        case FROM:
            memberModel.setFromPrimitive(way);
            break;
        case TO:
            memberModel.setToPrimitive(way);
            break;
        }
    }

    /**
     * Sets the way participating in the turn restriction in a given role.
     *
     * @param role the role. Must not be null.
     * @param wayId the id of the way to set
     * @exception IllegalArgumentException thrown if role is null
     * @exception IllegalArgumentException thrown if wayId != null isn't the id of a way
     * @exception IllegalStateException thrown the no way with this id was found in the dataset
     */
    public void setTurnRestrictionLeg(TurnRestrictionLegRole role, PrimitiveId wayId) {
        CheckParameterUtil.ensureParameterNotNull(role, "role");
        if (wayId == null) {
            setTurnRestrictionLeg(role, (Way) null);
            return;
        }
        if (!wayId.getType().equals(OsmPrimitiveType.WAY)) {
            throw new IllegalArgumentException(
                    MessageFormat.format("parameter ''wayId'' of type {0} expected, got {1}", OsmPrimitiveType.WAY, wayId.getType()));
        }

        OsmPrimitive p = layer.data.getPrimitiveById(wayId);
        if (p == null) {
            throw new IllegalStateException(MessageFormat.format("didn''t find way with id {0} in layer ''{1}''", wayId, layer.getName()));
        }
        setTurnRestrictionLeg(role, (Way) p);
    }

    /**
     * <p>"Officially" a turn restriction should have exactly one member with
     * role {@link TurnRestrictionLegRole#FROM FROM} and one member with role {@link TurnRestrictionLegRole#TO TO},
     * both referring to an OSM {@link Way}. In order to deals with turn restrictions where these
     * integrity constraints are violated, this model also supports relation with multiple or no
     * 'from' or 'to' members.</p>
     *
     * <p>Replies the turn restriction legs with role {@code role}. If no leg with this
     * role exists, an empty set is returned. If multiple legs exists, the set of referred
     * primitives is returned.</p>
     *
     * @param role the role. Must not be null.
     * @return the set of turn restriction legs with role {@code role}. The empty set, if
     * no such turn restriction leg exists
     * @throws IllegalArgumentException thrown if role is null
     */
    public Set<OsmPrimitive> getTurnRestrictionLeg(TurnRestrictionLegRole role) {
        CheckParameterUtil.ensureParameterNotNull(role, "role");
        switch(role) {
        case FROM: return memberModel.getFromPrimitives();
        case TO: return memberModel.getToPrimitives();
        }
        // should not happen
        return null;
    }

    /**
     * Initializes the model from a relation representing a turn
     * restriction
     *
     * @param turnRestriction the turn restriction
     */
    protected void initFromTurnRestriction(Relation turnRestriction) {

        // populate the member model
        memberModel.populate(turnRestriction);

        // make sure we have a restriction tag
        TagCollection tags = TagCollection.from(turnRestriction);
        tags.setUniqueForKey("type", "restriction");
        tagEditorModel.initFromTags(tags);

        setChanged();
        notifyObservers();
    }

    /**
     * Populates the turn restriction editor model with a turn restriction.
     * {@code turnRestriction} is an arbitrary relation. A tag type=restriction
     * isn't required. If it is missing, it is added here. {@code turnRestriction}
     * must not be null and it must belong to a dataset.
     *
     * @param turnRestriction the turn restriction
     * @throws IllegalArgumentException thrown if turnRestriction is null
     * @throws IllegalArgumentException thrown if turnRestriction doesn't belong to a dataset
     */
    public void populate(Relation turnRestriction) {
        CheckParameterUtil.ensureParameterNotNull(turnRestriction, "turnRestriction");
        if (turnRestriction.getDataSet() != null && turnRestriction.getDataSet() != layer.data) {
            throw new IllegalArgumentException(
                // don't translate - it's a technical message
                MessageFormat.format("turnRestriction {0} must not belong to a different dataset than the dataset of layer ''{1}''",
                        turnRestriction.getId(), layer.getName())
            );
        }
        initFromTurnRestriction(turnRestriction);
    }


    /**
     * Applies the current state in the model to a turn restriction
     *
     * @param turnRestriction the turn restriction. Must not be null.
     */
    public void apply(Relation turnRestriction) {
        CheckParameterUtil.ensureParameterNotNull(turnRestriction, "turnRestriction");
        TagCollection tags = tagEditorModel.getTagCollection();
        turnRestriction.removeAll();
        tags.applyTo(turnRestriction);
        memberModel.applyTo(turnRestriction);
    }

    /**
     * Replies the current tag value for the tag <tt>restriction</tt>.
     * The empty tag, if there isn't a tag <tt>restriction</tt>.
     *
     * @return the tag value
     */
    public String getRestrictionTagValue() {
        TagCollection tags = tagEditorModel.getTagCollection();
        if (!tags.hasTagsFor("restriction")) return "";
        return tags.getJoinedValues("restriction");
    }

    /**
     * Sets the current value for the restriction tag. If {@code value} is
     * null or an empty string, the restriction tag is removed.
     *
     * @param value the value of the restriction tag
     */
    public void setRestrictionTagValue(String value) {
        if (value == null || value.trim().equals("")) {
            tagEditorModel.delete("restriction");
        } else {
            TagModel tm = tagEditorModel.get("restriction");
            if (tm != null) {
                tm.setValue(value);
            } else {
                tagEditorModel.prepend(new TagModel("restriction", value.trim().toLowerCase()));
            }
        }
        setChanged();
        notifyObservers();
    }

    /**
     * Replies the list of 'via' objects. The return value is an
     * unmodifiable list.
     *
     * @return the list of 'via' objects
     */
    public List<OsmPrimitive> getVias() {
        return memberModel.getVias();
    }

    /**
     * <p>Sets the list of vias for the edited turn restriction.</p>
     *
     * <p>If {@code vias} is null, all vias are removed. All primitives
     * in {@code vias} must be assigned to a dataset and the dataset
     * must be equal to the dataset of this editor model, see {@link OsmPrimitive#getDataSet()}</p>
     *
     * <p>null values in {@code vias} are skipped.</p>
     *
     * @param vias the list of vias
     * @throws IllegalArgumentException thrown if one of the via objects belongs to the wrong dataset
     */
    public void setVias(List<OsmPrimitive> vias) throws IllegalArgumentException {
        memberModel.setVias(vias);
    }

    /**
     * Replies the layer in whose context this editor is working
     *
     * @return the layer in whose context this editor is working
     */
    public OsmDataLayer getLayer() {
        return layer;
    }

    /**
     * Registers this model with global event sources like {@link DatasetEventManager}
     */
    public void registerAsEventListener() {
        DatasetEventManager.getInstance().addDatasetListener(this, FireMode.IN_EDT);
    }

    /**
     * Removes this model as listener from global event sources like  {@link DatasetEventManager}
     */
    public void unregisterAsEventListener() {
        DatasetEventManager.getInstance().removeDatasetListener(this);
    }

    /**
     * Replies the tag  editor model
     *
     * @return the tag  editor model
     */
    public TagEditorModel getTagEditorModel() {
        return tagEditorModel;
    }

    /**
     * Replies the editor model for the relation members
     *
     * @return the editor model for the relation members
     */
    public RelationMemberEditorModel getRelationMemberEditorModel() {
        return memberModel;
    }

    /**
     * Replies the model for the open issues in this turn restriction
     * editor.
     *
     * @return the model for the open issues in this turn restriction
     * editor
     */
    public IssuesModel getIssuesModel() {
        return issuesModel;
    }

    public NavigationControler getNavigationControler() {
        return navigationControler;
    }

    /**
     * Replies the current value of the tag "except", or the empty string
     * if the tag doesn't exist.
     */
    public ExceptValueModel getExcept() {
        TagModel tag = tagEditorModel.get("except");
        if (tag == null) return new ExceptValueModel("");
        return new ExceptValueModel(tag.getValue());
    }

    /**
     * Sets the current value of the tag "except". Removes the
     * tag is {@code value} is null or consists of white
     * space only.
     *
     * @param value the new value for 'except'
     */
    public void setExcept(ExceptValueModel value) {
        if (value == null || value.getValue().equals("")) {
            if (tagEditorModel.get("except") != null) {
                tagEditorModel.delete("except");
                setChanged();
                notifyObservers();
            }
            return;
        }
        TagModel tag = tagEditorModel.get("except");
        if (tag == null) {
            tagEditorModel.prepend(new TagModel("except", value.getValue()));
            setChanged();
            notifyObservers();
        } else {
            if (!tag.getValue().equals(value.getValue())) {
                tag.setValue(value.getValue().trim());
                setChanged();
                notifyObservers();
            }
        }
    }

    /* ----------------------------------------------------------------------------------------- */
    /* interface DataSetListener                                                                 */
    /* ----------------------------------------------------------------------------------------- */
    protected boolean isAffectedByDataSetUpdate(DataSet ds, List<? extends OsmPrimitive> updatedPrimitives) {
        if (ds != layer.data) return false;
        if (updatedPrimitives == null || updatedPrimitives.isEmpty()) return false;
        Set<OsmPrimitive> myPrimitives = memberModel.getMemberPrimitives();
        int size1 = myPrimitives.size();
        myPrimitives.retainAll(updatedPrimitives);
        return size1 != myPrimitives.size();
    }

    @Override
    public void dataChanged(DataChangedEvent event) {
        // refresh the views
        setChanged();
        notifyObservers();
    }

    @Override
    public void nodeMoved(NodeMovedEvent event) {
        // may affect the display name of node in the list of vias
        if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
            setChanged();
            notifyObservers();
        }
    }

    @Override
    public void otherDatasetChange(AbstractDatasetChangedEvent event) {/* irrelevant in this context */}

    @Override
    public void primitivesAdded(PrimitivesAddedEvent event) {/* irrelevant in this context */}

    @Override
    public void primitivesRemoved(PrimitivesRemovedEvent event) {
        // relevant for the state of this model but not handled here. When the
        // state of this model is applied to the dataset we check whether the
        // the turn restriction refers to deleted or invisible primitives
    }

    @Override
    public void relationMembersChanged(RelationMembersChangedEvent event) {/* irrelevant in this context */}

    @Override
    public void tagsChanged(TagsChangedEvent event) {
        // may affect the display name of 'from', 'to' or 'via' elements
        if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
            setChanged();
            notifyObservers();
        }
    }

    @Override
    public void wayNodesChanged(WayNodesChangedEvent event) {
        // may affect the display name of 'from', 'to' or 'via' elements
        if (isAffectedByDataSetUpdate(event.getDataset(), event.getPrimitives())) {
            setChanged();
            notifyObservers();
        }
    }

    class RelationMemberModelListener implements TableModelListener {
        @Override
        public void tableChanged(TableModelEvent e) {
            setChanged();
            notifyObservers();
        }
    }

    /* ----------------------------------------------------------------------------------------- */
    /* inner classes                                                                             */
    /* ----------------------------------------------------------------------------------------- */
    class TagEditorModelObserver implements TableModelListener {
        @Override
        public void tableChanged(TableModelEvent e) {
            setChanged();
            notifyObservers();
        }
    }
}
