// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.osm.visitor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.NodeData;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.PrimitiveData;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationData;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.RelationMemberData;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.data.osm.WayData;
import org.openstreetmap.josm.tools.CheckParameterUtil;

/**
 * MergeSourceBuildingVisitor helps to build the "hull" of a collection of {@link OsmPrimitive}s
 * which shall be merged into another layer. The "hull" is slightly bigger than the original
 * collection. It includes, for instance the nodes of a way in the original collection even though
 * these nodes might not be present explicitly in the original collection. The "hull" also includes
 * incomplete {@link OsmPrimitive}s which are referred to by relations in the original collection. And
 * it turns {@link OsmPrimitive} referred to by {@link Relation}s in the original collection into
 * incomplete {@link OsmPrimitive}s in the "hull", if they are not themselves present in the
 * original collection.
 *
 */
public class MergeSourceBuildingVisitor extends AbstractVisitor {
    private DataSet selectionBase;
    private DataSet hull;
    private Map<OsmPrimitive, PrimitiveData> mappedPrimitives;

    /**
     * Creates the visitor. The visitor starts to build the "hull" from
     * the currently selected primitives in the dataset <code>selectionBase</code>,
     * i.e. from {@link DataSet#getSelected()}.
     *
     * @param selectionBase the dataset. Must not be null.
     * @exception IllegalArgumentException thrown if selectionBase is null
     *
     */
    public MergeSourceBuildingVisitor(DataSet selectionBase) throws IllegalArgumentException {
        CheckParameterUtil.ensureParameterNotNull(selectionBase, "selectionBase");
        this.selectionBase = selectionBase;
        this.hull = new DataSet();
        this.mappedPrimitives = new HashMap<>();
    }

    protected boolean isInSelectionBase(OsmPrimitive primitive) {
        return selectionBase.getAllSelected().contains(primitive);
    }

    protected boolean isAlreadyRemembered(OsmPrimitive primitive) {
        return mappedPrimitives.keySet().contains(primitive);
    }

    /**
     * Remebers a node in the "hull"
     *
     * @param n the node
     */
    protected void rememberNode(Node n) {
        if (isAlreadyRemembered(n))
            return;
        mappedPrimitives.put(n, n.save());
    }

    /**
     * remembers a way in the hull
     *
     * @param w the way
     */
    protected void rememberWay(Way w) {
        if (isAlreadyRemembered(w))
            return;
        WayData clone = w.save();
        List<Long> newNodes = new ArrayList<>(w.getNodesCount());
        for (Node n: w.getNodes()) {
            newNodes.add(mappedPrimitives.get(n).getUniqueId());
        }
        clone.setNodes(newNodes);
        mappedPrimitives.put(w, clone);
    }

    /**
     * Remembers a relation in the hull
     *
     * @param r the relation
     */
    protected void rememberRelation(Relation r) {
        RelationData clone;
        if (isAlreadyRemembered(r)) {
            clone = (RelationData)mappedPrimitives.get(r);
        } else {
            clone = r.save();
            mappedPrimitives.put(r, clone);
        }

        List<RelationMemberData> newMembers = new ArrayList<>();
        for (RelationMember member: r.getMembers()) {
            newMembers.add(
                    new RelationMemberData(member.getRole(), mappedPrimitives.get(member.getMember())));

        }
        clone.setMembers(newMembers);
    }

    protected void rememberRelationPartial(Relation r) {
        if (isAlreadyRemembered(r))
            return;
        RelationData clone = r.save();
        clone.getMembers().clear();
        mappedPrimitives.put(r, clone);
    }

    protected void rememberIncomplete(OsmPrimitive primitive) {
        if (isAlreadyRemembered(primitive))
            return;
        PrimitiveData clone = primitive.save();
        clone.setIncomplete(true);
        mappedPrimitives.put(primitive, clone);
    }

    @Override
    public void visit(Node n) {
        rememberNode(n);
    }

    @Override
    public void visit(Way w) {
        // remember all nodes this way refers to ...
        //
        for (Node n: w.getNodes()) {
            n.accept(this);
        }
        // ... and the way itself
        rememberWay(w);
    }

    @Override
    public void visit(Relation r) {
        // first, remember all primitives members refer to (only if necessary, see
        // below)
        //
        rememberRelationPartial(r);
        for (RelationMember member: r.getMembers()) {
            if (isAlreadyRemembered(member.getMember())) {
                // referred primitive already remembered
                //
                continue;
            }
            if (isInSelectionBase(member.getMember()) || member.getMember().isNew()) {
                member.getMember().accept(this);
            } else {
                rememberIncomplete(member.getMember());
            }
        }
        rememberRelation(r);
    }

    protected void buildHull() {
        // Create all primitives first
        for (PrimitiveData primitive: mappedPrimitives.values()) {
            OsmPrimitive newPrimitive = hull.getPrimitiveById(primitive);
            boolean created = newPrimitive == null;
            if (created) {
                newPrimitive = primitive.getType().newInstance(primitive.getUniqueId(), true);
            }
            if (newPrimitive instanceof Node && !primitive.isIncomplete()) {
                newPrimitive.load(primitive);
            }
            if (created) {
                hull.addPrimitive(newPrimitive);
            }
        }
        // Then ways and relations
        for (PrimitiveData primitive : mappedPrimitives.values()) {
            if (!(primitive instanceof NodeData) && !primitive.isIncomplete()) {
                hull.getPrimitiveById(primitive).load(primitive);
            }
        }
    }

    public DataSet build() {
        for (OsmPrimitive primitive: selectionBase.getAllSelected()) {
            primitive.accept(this);
        }
        buildHull();
        return hull;
    }
}
