package ext_tools;

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

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

import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.ChangeCommand;
import org.openstreetmap.josm.command.ChangeNodesCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
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.Way;
import org.openstreetmap.josm.gui.MainApplication;

final class DataSetToCmd {

    /** the source dataset where primitives are merged from */
    private final DataSet sourceDataSet;

    /**
     * A map of all primitives that got replaced with other primitives. Key is
     * the PrimitiveId in their dataset, the value is the PrimitiveId in my
     * dataset
     */
    private final Map<PrimitiveId, OsmPrimitive> mergedMap = new HashMap<>();

    private final LinkedList<Command> cmds = new LinkedList<>();

    /**
     * constructor
     */
    public DataSetToCmd(DataSet sourceDataSet) {
        this.sourceDataSet = sourceDataSet;
        merge();
    }

    /**
     * Merges a primitive <code>other</code> of type
     * <P>
     * onto my primitives.
     *
     * @param <P>
     *            the type of the other primitive
     * @param source
     *            the other primitive
     */
    private void mergePrimitive(OsmPrimitive source) {

        OsmPrimitive target = null;
        switch (source.getType()) {
        case NODE:
            target = new Node();
            break;
        case WAY:
            target = new Way();
            break;
        case RELATION:
            target = new Relation();
            break;
        default:
            throw new AssertionError();
        }
        target.mergeFrom(source);
        mergedMap.put(source.getPrimitiveId(), target);
        cmds.add(new AddCommand(MainApplication.getLayerManager().getEditDataSet(), target));
    }

    private OsmPrimitive getMergeTarget(OsmPrimitive mergeSource) {
        return mergedMap.get(mergeSource.getPrimitiveId());
    }

    /**
     * Postprocess the dataset and fix all merged references to point to the
     * actual data.
     */
    public void fixReferences() {
        for (Way w : sourceDataSet.getWays()) {
            mergeNodeList(w);
        }
        for (Relation r : sourceDataSet.getRelations()) {
            mergeRelationMembers(r);
        }
    }

    /**
     * Merges the node list of a source way onto its target way.
     *
     * @param source
     *            the source way
     * @throws IllegalStateException
     *             thrown if no target way can be found for the source way
     *             or if there isn't a target node for one of the nodes in
     *             the source way
     *
     */
    private void mergeNodeList(Way source) {
        Way target = (Way) getMergeTarget(source);
        if (target == null || target.getDataSet() != null)
            throw new IllegalStateException(tr(
                    "Missing merge target for way with id {0}", source.getUniqueId()));

        List<Node> newNodes = new ArrayList<>(source.getNodesCount());
        for (Node sourceNode : source.getNodes()) {
            Node targetNode = (Node) getMergeTarget(sourceNode);
            if (targetNode == null || targetNode.getDataSet() != null)
                throw new IllegalStateException(tr(
                        "Missing merge target for node with id {0}", sourceNode
                                .getUniqueId()));
            newNodes.add(targetNode);
        }
        cmds.add(new ChangeNodesCommand(MainApplication.getLayerManager().getEditDataSet(), target, newNodes));
    }

    /**
     * Merges the relation members of a source relation onto the corresponding
     * target relation.
     *
     * @param source
     *            the source relation
     * @throws IllegalStateException
     *             thrown if there is no corresponding target relation
     * @throws IllegalStateException
     *             thrown if there isn't a corresponding target object for one
     *             of the relation members in source
     */
    private void mergeRelationMembers(Relation source) throws IllegalStateException {
        Relation target = (Relation) getMergeTarget(source);
        if (target == null)
            throw new IllegalStateException(
                    tr("Missing merge target for relation with id {0}", source
                            .getUniqueId()));
        if (target.getDataSet() != null)
            throw new IllegalStateException(
                    tr("Missing merge target for relation with id {0}", source
                            .getUniqueId()));
        LinkedList<RelationMember> newMembers = new LinkedList<>();
        for (RelationMember sourceMember : source.getMembers()) {
            OsmPrimitive targetMember = getMergeTarget(sourceMember.getMember());
            if (targetMember == null)
                throw new IllegalStateException(tr(
                        "Missing merge target of type {0} with id {1}", sourceMember
                                .getType(), sourceMember.getUniqueId()));
            if (targetMember.getDataSet() != null)
                throw new IllegalStateException(tr(
                        "Missing merge target of type {0} with id {1}", sourceMember
                                .getType(), sourceMember.getUniqueId()));
            newMembers.add(new RelationMember(sourceMember.getRole(), targetMember));
        }
        Relation newRelation = new Relation(target);
        newRelation.setMembers(newMembers);
        cmds.add(new ChangeCommand(MainApplication.getLayerManager().getEditDataSet(), target, newRelation));
    }

    private void merge() {
        for (Node node : sourceDataSet.getNodes()) {
            mergePrimitive(node);
        }
        for (Way way : sourceDataSet.getWays()) {
            mergePrimitive(way);
        }
        for (Relation relation : sourceDataSet.getRelations()) {
            mergePrimitive(relation);
        }
        fixReferences();
    }

    public LinkedList<Command> getCommandList() {
        return cmds;
    }
}
