// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.command;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;

import javax.swing.JLabel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.conflict.ConflictCollection;
import org.openstreetmap.josm.data.osm.BackreferencedDataSet;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.tools.ImageProvider;

/**
 * Physically removes an {@see OsmPrimitive} from the dataset of the edit
 * layer and disconnects any references from {@see Way}s or {@see Relation}s
 * to this primitive.
 *
 * This command is necessary if a local {@see OsmPrimitive} has been deleted on
 * the server by another user and if the local user decides to delete his version
 * too. If he only deleted it "logically" JOSM would try to delete it on the server
 * which would result in an non resolvable conflict.
 *
 */
public class PurgePrimitivesCommand extends ConflictResolveCommand{

    static private final Logger logger = Logger.getLogger(PurgePrimitivesCommand.class.getName());

    /** the primitives to purge */
    private Collection<OsmPrimitive> toPurge;

    /** the set of primitives to purge as consequence of purging
     * {@see #primitive}, including {@see #primitive}
     */
    private Set<OsmPrimitive> purgedPrimitives;

    private Set<OsmPrimitive> origVersionsOfTouchedPrimitives;

    /**
     * the data structure with child->parent references
     */
    private BackreferencedDataSet backreferenceDataSet;

    protected void init(Collection<OsmPrimitive> toPurge) {
        this.toPurge = toPurge;
        this.purgedPrimitives = new HashSet<OsmPrimitive>();
        this.origVersionsOfTouchedPrimitives = new HashSet<OsmPrimitive>();
    }

    /**
     * constructor
     * @param primitive the primitive to purge
     * 
     */
    public PurgePrimitivesCommand(OsmPrimitive primitive) {
        init(Collections.singleton(primitive));
    }

    /**
     * constructor
     * @param layer the OSM data layer
     * @param primitive the primitive to purge
     * 
     */
    public PurgePrimitivesCommand(OsmDataLayer layer, OsmPrimitive primitive) {
        super(layer);
        init(Collections.singleton(primitive));
    }

    /**
     * constructor
     * @param layer the OSM data layer
     * @param primitives the primitives to purge
     * 
     */
    public PurgePrimitivesCommand(OsmDataLayer layer, Collection<OsmPrimitive> primitives) {
        super(layer);
        init(primitives);
    }

    /**
     * Replies a collection with the purged primitives
     * 
     * @return a collection with the purged primitives
     */
    public Collection<OsmPrimitive> getPurgedPrimitives() {
        return purgedPrimitives;
    }

    protected MutableTreeNode getDescription(OsmPrimitive primitive) {
        return new DefaultMutableTreeNode(
                new JLabel(
                        tr("Purged object ''{0}''", primitive.getDisplayName(DefaultNameFormatter.getInstance())),
                        ImageProvider.get("data", "object"),
                        JLabel.HORIZONTAL
                )
        );
    }

    protected MutableTreeNode getDescription(Collection<OsmPrimitive> primitives) {

        DefaultMutableTreeNode root = new DefaultMutableTreeNode(
                tr("Purged {0} objects", primitives.size())
        );
        for (OsmPrimitive p : primitives) {
            root.add(getDescription(p));
        }
        return root;
    }

    @Override
    public MutableTreeNode description() {
        if (purgedPrimitives.size() == 1)
            return getDescription(purgedPrimitives.iterator().next());
        else
            return getDescription(purgedPrimitives);
    }

    /**
     * Purges an {@see OsmPrimitive} <code>child</code> from a {@see DataSet}.
     *
     * @param child the primitive to purge
     * @param hive the hive of {@see OsmPrimitive}s we remember other {@see OsmPrimitive}
     * we have to purge because we purge <code>child</code>.
     *
     */
    protected void removeReferecesToPrimitive(OsmPrimitive child, Set<OsmPrimitive> hive) {
        hive.remove(child);
        for (OsmPrimitive parent: this.backreferenceDataSet.getParents(child)) {
            if (toPurge.contains(parent))
                // parent itself is to be purged. This method is going to be
                // invoked for parent later
                return;
            if (parent instanceof Way) {
                Way w = (Way)parent;
                if (!origVersionsOfTouchedPrimitives.contains(w)) {
                    origVersionsOfTouchedPrimitives.add(w);
                }
                List<Node> wayNodes = w.getNodes();
                wayNodes.remove(child);
                w.setNodes(wayNodes);
                // if a way ends up with less than two nodes we
                // remember it on the "hive"
                //
                if (w.getNodesCount() < 2) {
                    System.out.println(tr("Warning: Purging way {0} because number of nodes dropped below 2. Current is {1}",
                            w.getId(),w.getNodesCount()));
                    hive.add(w);
                }
            } else if (parent instanceof Relation) {
                Relation r = (Relation)parent;
                if (!origVersionsOfTouchedPrimitives.contains(r)) {
                    origVersionsOfTouchedPrimitives.add(r);
                }
                System.out.println(tr("Removing reference from relation {0}",r.getId()));
                r.removeMembersFor(child);
            } else {
                // should not happen. parent can't be a node
            }
        }
    }

    @Override
    public boolean executeCommand() {
        if (backreferenceDataSet == null) {
            backreferenceDataSet = new BackreferencedDataSet();
        }
        HashSet<OsmPrimitive> hive = new HashSet<OsmPrimitive>();

        // iteratively purge the primitive and all primitives
        // which violate invariants after they lose a reference to
        // the primitive (i.e. ways which end up with less than two
        // nodes)
        hive.addAll(toPurge);
        while(! hive.isEmpty()) {
            OsmPrimitive p = hive.iterator().next();
            removeReferecesToPrimitive(p, hive);
            getLayer().data.removePrimitive(p);
            purgedPrimitives.add(p);
            ConflictCollection conflicts = getLayer().getConflicts();
            if (conflicts.hasConflictForMy(p)) {
                rememberConflict(conflicts.getConflictForMy(p));
                conflicts.remove(p);
            }
        }
        // we don't need this any more
        backreferenceDataSet = null;
        return super.executeCommand();
    }

    @Override
    public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
            Collection<OsmPrimitive> added) {
        modified.addAll(origVersionsOfTouchedPrimitives);
    }

    @Override
    public void undoCommand() {
        if (! Main.map.mapView.hasLayer(getLayer())) {
            logger.warning(tr("Can''t undo command ''{0}'' because layer ''{1}'' is not present any more",
                    this.toString(),
                    getLayer().toString()
            ));
            return;
        }
        Main.map.mapView.setActiveLayer(getLayer());

        // restore purged primitives
        //
        for (OsmPrimitive purged : purgedPrimitives) {
            getLayer().data.addPrimitive(purged);
        }
        reconstituteConflicts();

        // will restore the primitives referring to one
        // of the purged primitives
        super.undoCommand();
    }

    /**
     * Use to inject a backreference data set used when the command
     * is executed.
     * 
     * @param ds the backreference data set
     */
    public void setBackreferenceDataSet(BackreferencedDataSet ds) {
        this.backreferenceDataSet = ds;
    }
}
