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

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

import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Future;
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.Bounds.ParseMethod;
import org.openstreetmap.josm.data.gpx.GpxData;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.gui.layer.GpxLayer;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.gui.layer.markerlayer.MarkerLayer;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.gui.progress.ProgressTaskId;
import org.openstreetmap.josm.gui.progress.ProgressTaskIds;
import org.openstreetmap.josm.io.BoundingBoxDownloader;
import org.openstreetmap.josm.io.GpxImporter;
import org.openstreetmap.josm.io.GpxImporter.GpxImporterData;
import org.openstreetmap.josm.io.OsmServerLocationReader;
import org.openstreetmap.josm.io.OsmServerReader;
import org.openstreetmap.josm.io.OsmTransferException;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.xml.sax.SAXException;

/**
 * Task allowing to download GPS data.
 */
public class DownloadGpsTask extends AbstractDownloadTask {

    private DownloadTask downloadTask;

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

    private static final String PATTERN_TRACKPOINTS_BBOX = "https?://.*/api/0.6/trackpoints\\?bbox=.*,.*,.*,.*";

    private static final String PATTERN_EXTERNAL_GPX_SCRIPT = "https?://.*exportgpx.*";
    private static final String PATTERN_EXTERNAL_GPX_FILE = "https?://.*/(.*\\.gpx)";

    protected String newLayerName = null;

    @Override
    public String[] getPatterns() {
        return new String[] {PATTERN_EXTERNAL_GPX_FILE, PATTERN_EXTERNAL_GPX_SCRIPT, PATTERN_TRACE_ID, PATTERN_TRACKPOINTS_BBOX};
    }

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

    @Override
    public Future<?> download(boolean newLayer, Bounds downloadArea, ProgressMonitor progressMonitor) {
        downloadTask = new DownloadTask(newLayer,
                new BoundingBoxDownloader(downloadArea), progressMonitor);
        // We need submit instead of execute so we can wait for it to finish and get the error
        // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
        return Main.worker.submit(downloadTask);
    }

    @Override
    public Future<?> loadUrl(boolean newLayer, String url, ProgressMonitor progressMonitor) {
        CheckParameterUtil.ensureParameterNotNull(url, "url");
        if (url.matches(PATTERN_TRACE_ID)
         || url.matches(PATTERN_EXTERNAL_GPX_SCRIPT)
         || url.matches(PATTERN_EXTERNAL_GPX_FILE)) {
            downloadTask = new DownloadTask(newLayer,
                    new OsmServerLocationReader(url), progressMonitor);
            // Extract .gpx filename from URL to set the new layer name
            Matcher matcher = Pattern.compile(PATTERN_EXTERNAL_GPX_FILE).matcher(url);
            newLayerName = matcher.matches() ? matcher.group(1) : null;
            // We need submit instead of execute so we can wait for it to finish and get the error
            // message if necessary. If no one calls getErrorMessage() it just behaves like execute.
            return Main.worker.submit(downloadTask);

        } else if (url.matches(PATTERN_TRACKPOINTS_BBOX)) {
            String[] table = url.split("\\?|=|&");
            for (int i = 0; i < table.length; i++) {
                if ("bbox".equals(table[i]) && i < table.length-1)
                    return download(newLayer, new Bounds(table[i+1], ",", ParseMethod.LEFT_BOTTOM_RIGHT_TOP), progressMonitor);
            }
        }
        return null;
    }

    @Override
    public void cancel() {
        if (downloadTask != null) {
            downloadTask.cancel();
        }
    }

    class DownloadTask extends PleaseWaitRunnable {
        private OsmServerReader reader;
        private GpxData rawData;
        private final boolean newLayer;

        public DownloadTask(boolean newLayer, OsmServerReader reader, ProgressMonitor progressMonitor) {
            super(tr("Downloading GPS data"), progressMonitor, false);
            this.reader = reader;
            this.newLayer = newLayer;
        }

        @Override
        public void realRun() throws IOException, SAXException, OsmTransferException {
            try {
                if (isCanceled())
                    return;
                ProgressMonitor subMonitor = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
                rawData = reader.parseRawGps(subMonitor);
            } catch (Exception e) {
                if (isCanceled())
                    return;
                if (e instanceof OsmTransferException) {
                    rememberException(e);
                } else {
                    rememberException(new OsmTransferException(e));
                }
            }
        }

        @Override
        protected void finish() {
            if (isCanceled() || isFailed())
                return;
            if (rawData == null)
                return;
            String name = newLayerName != null ? newLayerName : tr("Downloaded GPX Data");

            GpxImporterData layers = GpxImporter.loadLayers(rawData, reader.isGpxParsedProperly(), name,
                    tr("Markers from {0}", name));

            GpxLayer gpxLayer = addOrMergeLayer(layers.getGpxLayer(), findGpxMergeLayer());
            addOrMergeLayer(layers.getMarkerLayer(), findMarkerMergeLayer(gpxLayer));

            layers.getPostLayerTask().run();
        }

        private <L extends Layer> L addOrMergeLayer(L layer, L mergeLayer) {
            if (layer == null) return null;
            if (newLayer || mergeLayer == null) {
                Main.main.addLayer(layer);
                return layer;
            } else {
                mergeLayer.mergeFrom(layer);
                Main.map.repaint();
                return mergeLayer;
            }
        }

        private GpxLayer findGpxMergeLayer() {
            if (!Main.isDisplayingMapView())
                return null;
            boolean merge = Main.pref.getBoolean("download.gps.mergeWithLocal", false);
            Layer active = Main.map.mapView.getActiveLayer();
            if (active instanceof GpxLayer && (merge || ((GpxLayer) active).data.fromServer))
                return (GpxLayer) active;
            for (GpxLayer l : Main.map.mapView.getLayersOfType(GpxLayer.class)) {
                if (merge || l.data.fromServer)
                    return l;
            }
            return null;
        }

        private MarkerLayer findMarkerMergeLayer(GpxLayer fromLayer) {
            if (!Main.isDisplayingMapView())
                return null;
            for (MarkerLayer l : Main.map.mapView.getLayersOfType(MarkerLayer.class)) {
                if (fromLayer != null && l.fromLayer == fromLayer)
                    return l;
            }
            return null;
        }

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

        @Override
        public ProgressTaskId canRunInBackground() {
            return ProgressTaskIds.DOWNLOAD_GPS;
        }
    }

    @Override
    public String getConfirmationMessage(URL url) {
        // TODO
        return null;
    }

    @Override
    public boolean isSafeForRemotecontrolRequests() {
        return true;
    }

    /**
     * Determines if the given URL denotes an OSM gpx-related API call.
     * @param url The url to check
     * @return true if the url matches "Trace ID" API call or "Trackpoints bbox" API call, false otherwise
     * @see GpxData#fromServer
     * @since 5745
     */
    public static final boolean isFromServer(String url) {
        return url != null && (url.matches(PATTERN_TRACE_ID) || url.matches(PATTERN_TRACKPOINTS_BBOX));
    }
}
