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

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

import java.io.IOException;
import java.text.MessageFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.data.osm.DataSetMerger;
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.Way;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.layer.OsmDataLayer;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
import org.openstreetmap.josm.io.OsmServerBackreferenceReader;
import org.openstreetmap.josm.io.OsmServerReader;
import org.openstreetmap.josm.io.OsmTransferException;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.ExceptionUtil;
import org.xml.sax.SAXException;

/**
 * The asynchronous task for downloading referring primitives
 * @since 2923
 */
public class DownloadReferrersTask extends PleaseWaitRunnable {
    private boolean canceled;
    private Exception lastException;
    private OsmServerReader reader;
    /** the target layer */
    private OsmDataLayer targetLayer;
    /** the collection of child primitives */
    private Map<Long, OsmPrimitiveType> children;
    /** the parents */
    private DataSet parents;

    /**
     * constructor
     *
     * @param targetLayer  the target layer for the downloaded primitives. Must not be null.
     * @param children the collection of child primitives for which parents are to be downloaded
     */
    public DownloadReferrersTask(OsmDataLayer targetLayer, Collection<OsmPrimitive> children) {
        super("Download referrers", false /* don't ignore exception*/);
        CheckParameterUtil.ensureParameterNotNull(targetLayer, "targetLayer");
        canceled = false;
        this.children = new HashMap<>();
        if (children != null) {
            for (OsmPrimitive p: children) {
                if (!p.isNew()) {
                    this.children.put(p.getId(), OsmPrimitiveType.from(p));
                }
            }
        }
        this.targetLayer = targetLayer;
        parents = new DataSet();
    }

    /**
     * constructor
     *
     * @param targetLayer  the target layer for the downloaded primitives. Must not be null.
     * @param children  the collection of children for which parents are to be downloaded. Children
     * are specified by their id and  their type.
     */
    public DownloadReferrersTask(OsmDataLayer targetLayer, Map<Long, OsmPrimitiveType> children) {
        super("Download referrers", false /* don't ignore exception*/);
        CheckParameterUtil.ensureParameterNotNull(targetLayer, "targetLayer");
        canceled = false;
        this.children = new HashMap<>();
        if (children != null) {
            for (Entry<Long, OsmPrimitiveType> entry : children.entrySet()) {
                if (entry.getKey() > 0 && entry.getValue() != null) {
                    children.put(entry.getKey(), entry.getValue());
                }
            }
        }
        this.targetLayer = targetLayer;
        parents = new DataSet();
    }

    /**
     * constructor
     *
     * @param targetLayer  the target layer. Must not be null.
     * @param id the primitive id. id &gt; 0 required.
     * @param type the primitive type. type != null required
     * @throws IllegalArgumentException if id &lt;= 0
     * @throws IllegalArgumentException if type == null
     * @throws IllegalArgumentException if targetLayer == null
     */
    public DownloadReferrersTask(OsmDataLayer targetLayer, long id, OsmPrimitiveType type) {
        super("Download referrers", false /* don't ignore exception*/);
        CheckParameterUtil.ensureParameterNotNull(targetLayer, "targetLayer");
        if (id <= 0)
            throw new IllegalArgumentException(MessageFormat.format("Id > 0 required, got {0}", id));
        CheckParameterUtil.ensureParameterNotNull(type, "type");
        canceled = false;
        this.children = new HashMap<>();
        this.children.put(id, type);
        this.targetLayer = targetLayer;
        parents = new DataSet();
    }

    /**
     * constructor
     *
     * @param targetLayer the target layer. Must not be null.
     * @param primitiveId a PrimitiveId object.
     * @throws IllegalArgumentException if id &lt;= 0
     * @throws IllegalArgumentException if targetLayer == null
     */
    public DownloadReferrersTask(OsmDataLayer targetLayer, PrimitiveId primitiveId) {
        this(targetLayer,  primitiveId, null);
    }

    /**
     * constructor
     *
     * @param targetLayer the target layer. Must not be null.
     * @param primitiveId a PrimitiveId object.
     * @param progressMonitor ProgressMonitor to use or null to create a new one.
     * @throws IllegalArgumentException if id &lt;= 0
     * @throws IllegalArgumentException if targetLayer == null
     */
    public DownloadReferrersTask(OsmDataLayer targetLayer, PrimitiveId primitiveId,
            ProgressMonitor progressMonitor) {
        super("Download referrers", progressMonitor, false /* don't ignore exception*/);
        CheckParameterUtil.ensureParameterNotNull(targetLayer, "targetLayer");
        if (primitiveId.isNew())
            throw new IllegalArgumentException(MessageFormat.format(
                    "Cannot download referrers for new primitives (ID {0})", primitiveId.getUniqueId()));
        canceled = false;
        this.children = new HashMap<>();
        this.children.put(primitiveId.getUniqueId(), primitiveId.getType());
        this.targetLayer = targetLayer;
        parents = new DataSet();
    }

    @Override
    protected void cancel() {
        canceled = true;
        synchronized (this) {
            if (reader != null) {
                reader.cancel();
            }
        }
    }

    @Override
    protected void finish() {
        if (canceled)
            return;
        if (lastException != null) {
            ExceptionUtil.explainException(lastException);
            return;
        }

        DataSetMerger visitor = new DataSetMerger(targetLayer.data, parents);
        visitor.merge();
        SwingUtilities.invokeLater(
                new Runnable() {
                    @Override
                    public void run() {
                        targetLayer.onPostDownloadFromServer();
                        if (Main.map != null)
                            Main.map.mapView.repaint();
                    }
                }
        );
        if (visitor.getConflicts().isEmpty())
            return;
        targetLayer.getConflicts().add(visitor.getConflicts());
        JOptionPane.showMessageDialog(
                Main.parent,
                trn("There was {0} conflict during import.",
                        "There were {0} conflicts during import.",
                        visitor.getConflicts().size(),
                        visitor.getConflicts().size()
                ),
                trn("Conflict during download", "Conflicts during download", visitor.getConflicts().size()),
                JOptionPane.WARNING_MESSAGE
        );
        Main.map.conflictDialog.unfurlDialog();
        Main.map.repaint();
    }

    protected void downloadParents(long id, OsmPrimitiveType type, ProgressMonitor progressMonitor) throws OsmTransferException {
        reader = new OsmServerBackreferenceReader(id, type);
        DataSet ds = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
        synchronized (this) { // avoid race condition in cancel()
            reader = null;
        }
        Collection<Way> ways = ds.getWays();

        DataSetMerger merger;
        if (!ways.isEmpty()) {
            Set<Node> nodes = new HashSet<>();
            for (Way w: ways) {
                // Ensure each node is only listed once
                nodes.addAll(w.getNodes());
            }
            // Don't retrieve any nodes we've already grabbed
            nodes.removeAll(targetLayer.data.getNodes());
            if (!nodes.isEmpty()) {
                reader = new MultiFetchServerObjectReader();
                ((MultiFetchServerObjectReader) reader).append(nodes);
                DataSet wayNodes = reader.parseOsm(progressMonitor.createSubTaskMonitor(1, false));
                synchronized (this) { // avoid race condition in cancel()
                    reader = null;
                }
                merger = new DataSetMerger(ds, wayNodes);
                merger.merge();
            }
        }
        merger = new DataSetMerger(parents, ds);
        merger.merge();
    }

    @Override
    protected void realRun() throws SAXException, IOException, OsmTransferException {
        try {
            progressMonitor.setTicksCount(children.size());
            int i = 1;
            for (Entry<Long, OsmPrimitiveType> entry: children.entrySet()) {
                if (canceled)
                    return;
                String msg = "";
                switch(entry.getValue()) {
                case NODE: msg = tr("({0}/{1}) Loading parents of node {2}", i+1, children.size(), entry.getKey()); break;
                case WAY: msg = tr("({0}/{1}) Loading parents of way {2}", i+1, children.size(), entry.getKey()); break;
                case RELATION: msg = tr("({0}/{1}) Loading parents of relation {2}", i+1, children.size(), entry.getKey()); break;
                }
                progressMonitor.subTask(msg);
                downloadParents(entry.getKey(), entry.getValue(), progressMonitor);
                i++;
            }
        } catch (Exception e) {
            if (canceled)
                return;
            lastException = e;
        }
    }
}
