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

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

import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.Bounds;
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.OsmPrimitiveType;
import org.openstreetmap.josm.data.osm.PrimitiveData;
import org.openstreetmap.josm.data.osm.PrimitiveId;
import org.openstreetmap.josm.data.osm.RelationData;
import org.openstreetmap.josm.data.osm.WayData;
import org.openstreetmap.josm.data.osm.history.History;
import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
import org.openstreetmap.josm.data.osm.history.HistoryDataSetListener;
import org.openstreetmap.josm.data.osm.history.HistoryNode;
import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
import org.openstreetmap.josm.data.osm.history.HistoryRelation;
import org.openstreetmap.josm.data.osm.history.HistoryWay;
import org.openstreetmap.josm.gui.history.HistoryLoadTask;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.io.OsmApi;
import org.openstreetmap.josm.io.OsmServerLocationReader;
import org.openstreetmap.josm.io.OsmServerReader;
import org.openstreetmap.josm.io.OsmTransferException;
import org.openstreetmap.josm.tools.Logging;

/**
 * Task allowing to download OsmChange data (http://wiki.openstreetmap.org/wiki/OsmChange).
 * @since 4530
 */
public class DownloadOsmChangeTask extends DownloadOsmTask {

    private static final String OSM_WEBSITE_PATTERN = "https?://www\\.(osm|openstreetmap)\\.org/changeset/(\\p{Digit}+).*";

    @Override
    public String[] getPatterns() {
        return new String[]{"https?://.*/api/0.6/changeset/\\p{Digit}+/download", // OSM API 0.6 changesets
            OSM_WEBSITE_PATTERN, // OSM changesets
            "https?://.*/.*\\.osc" // Remote .osc files
        };
    }

    @Override
    public String getTitle() {
        return tr("Download OSM Change");
    }

    @Override
    public Future<?> download(boolean newLayer, Bounds downloadArea,
            ProgressMonitor progressMonitor) {
        return null;
    }

    @Override
    public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) {
        final Matcher matcher = Pattern.compile(OSM_WEBSITE_PATTERN).matcher(url);
        if (matcher.matches()) {
            url = OsmApi.getOsmApi().getBaseUrl() + "changeset/" + Long.parseLong(matcher.group(2)) + "/download";
        }
        downloadTask = new DownloadTask(newLayer, new OsmServerLocationReader(url), progressMonitor);
        // Extract .osc filename from URL to set the new layer name
        extractOsmFilename("https?://.*/(.*\\.osc)", url);
        return Main.worker.submit(downloadTask);
    }

    /**
     * OsmChange download task.
     */
    protected class DownloadTask extends DownloadOsmTask.DownloadTask {

        /**
         * Constructs a new {@code DownloadTask}.
         * @param newLayer if {@code true}, force download to a new layer
         * @param reader OSM data reader
         * @param progressMonitor progress monitor
         */
        public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
            super(newLayer, reader, progressMonitor);
        }

        @Override
        protected DataSet parseDataSet() throws OsmTransferException {
            return reader.parseOsmChange(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
        }

        @Override
        protected void finish() {
            super.finish();
            if (isFailed() || isCanceled() || downloadedData == null)
                return; // user canceled download or error occurred
            try {
                // A changeset does not contain all referred primitives, this is the map of incomplete ones
                // For each incomplete primitive, we'll have to get its state at date it was referred
                Map<OsmPrimitive, Date> toLoad = new HashMap<>();
                for (OsmPrimitive p : downloadedData.allNonDeletedPrimitives()) {
                    if (p.isIncomplete()) {
                        Date timestamp = null;
                        for (OsmPrimitive ref : p.getReferrers()) {
                            if (!ref.isTimestampEmpty()) {
                                timestamp = ref.getTimestamp();
                                break;
                            }
                        }
                        toLoad.put(p, timestamp);
                    }
                }
                if (isCanceled()) return;
                // Let's load all required history
                Main.worker.submit(new HistoryLoaderAndListener(toLoad));
            } catch (RejectedExecutionException e) {
                rememberException(e);
                setFailed(true);
            }
        }
    }

    /**
     * Loads history and updates incomplete primitives.
     */
    private static final class HistoryLoaderAndListener extends HistoryLoadTask implements HistoryDataSetListener {

        private final Map<OsmPrimitive, Date> toLoad;

        private HistoryLoaderAndListener(Map<OsmPrimitive, Date> toLoad) {
            this.toLoad = toLoad;
            add(toLoad.keySet());
            // Updating process is done after all history requests have been made
            HistoryDataSet.getInstance().addHistoryDataSetListener(this);
        }

        @Override
        public void historyUpdated(HistoryDataSet source, PrimitiveId id) {
            Map<OsmPrimitive, Date> toLoadNext = new HashMap<>();
            for (Iterator<Entry<OsmPrimitive, Date>> it = toLoad.entrySet().iterator(); it.hasNext();) {
                Entry<OsmPrimitive, Date> entry = it.next();
                OsmPrimitive p = entry.getKey();
                History history = source.getHistory(p.getPrimitiveId());
                Date date = entry.getValue();
                // If the history has been loaded and a timestamp is known
                if (history != null && date != null) {
                    // Lookup for the primitive version at the specified timestamp
                    HistoryOsmPrimitive hp = history.getByDate(date);
                    if (hp != null) {
                        PrimitiveData data;

                        switch (p.getType()) {
                        case NODE:
                            data = ((HistoryNode) hp).fillPrimitiveData(new NodeData());
                            break;
                        case WAY:
                            data = ((HistoryWay) hp).fillPrimitiveData(new WayData());
                            // Find incomplete nodes to load at next run
                            for (Long nodeId : ((HistoryWay) hp).getNodes()) {
                                if (p.getDataSet().getPrimitiveById(nodeId, OsmPrimitiveType.NODE) == null) {
                                    Node n = new Node(nodeId);
                                    p.getDataSet().addPrimitive(n);
                                    toLoadNext.put(n, date);
                                }
                            }
                            break;
                        case RELATION:
                            data = ((HistoryRelation) hp).fillPrimitiveData(new RelationData());
                            break;
                        default: throw new AssertionError("Unknown primitive type");
                        }

                        // Load the history data
                        try {
                            p.load(data);
                            // Forget this primitive
                            it.remove();
                        } catch (AssertionError e) {
                            Logging.log(Logging.LEVEL_ERROR, "Cannot load "+p+':', e);
                        }
                    }
                }
            }
            source.removeHistoryDataSetListener(this);
            if (toLoadNext.isEmpty()) {
                // No more primitive to update. Processing is finished
                // Be sure all updated primitives are correctly drawn
                Main.map.repaint();
            } else {
                // Some primitives still need to be loaded
                // Let's load all required history
                Main.worker.submit(new HistoryLoaderAndListener(toLoadNext));
            }
        }

        @Override
        public void historyDataSetCleared(HistoryDataSet source) {
            // Do nothing
        }
    }
}
