// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.opendata.core.io;

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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.stream.XMLStreamException;

import org.openstreetmap.josm.data.osm.DataSet;
import org.openstreetmap.josm.gui.progress.ProgressMonitor;
import org.openstreetmap.josm.io.AbstractReader;
import org.openstreetmap.josm.io.Compression;
import org.openstreetmap.josm.io.GeoJSONReader;
import org.openstreetmap.josm.io.IllegalDataException;
import org.openstreetmap.josm.io.OsmServerReader;
import org.openstreetmap.josm.io.OsmTransferException;
import org.openstreetmap.josm.plugins.opendata.core.OdConstants;
import org.openstreetmap.josm.plugins.opendata.core.datasets.AbstractDataSetHandler;
import org.openstreetmap.josm.plugins.opendata.core.io.archive.ArchiveReader;
import org.openstreetmap.josm.plugins.opendata.core.io.archive.SevenZipReader;
import org.openstreetmap.josm.plugins.opendata.core.io.archive.ZipReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.GmlReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.KmlReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.KmzReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.MifReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.ShpReader;
import org.openstreetmap.josm.plugins.opendata.core.io.geographic.TabReader;
import org.openstreetmap.josm.plugins.opendata.core.io.tabular.CsvReader;
import org.openstreetmap.josm.plugins.opendata.core.io.tabular.OdsReader;
import org.openstreetmap.josm.plugins.opendata.core.io.tabular.XlsReader;
import org.openstreetmap.josm.tools.CheckParameterUtil;
import org.openstreetmap.josm.tools.Logging;

public class NetworkReader extends OsmServerReader {

    private final String url;
    private final AbstractDataSetHandler handler;
    private Class<? extends AbstractReader> readerClass;
    private final boolean promptUser;

    private File file;
    private String filename;

    /**
     * File readers
     */
    public static final Map<String, Class<? extends AbstractReader>> FILE_READERS = new HashMap<>();
    static {
        FILE_READERS.put(OdConstants.CSV_EXT, CsvReader.class);
        FILE_READERS.put(OdConstants.KML_EXT, KmlReader.class);
        FILE_READERS.put(OdConstants.KMZ_EXT, KmzReader.class);
        FILE_READERS.put(OdConstants.GML_EXT, GmlReader.class);
        FILE_READERS.put(OdConstants.XLS_EXT, XlsReader.class);
        FILE_READERS.put(OdConstants.ODS_EXT, OdsReader.class);
        FILE_READERS.put(OdConstants.SHP_EXT, ShpReader.class);
        FILE_READERS.put(OdConstants.MIF_EXT, MifReader.class);
        FILE_READERS.put(OdConstants.TAB_EXT, TabReader.class);
        FILE_READERS.put(OdConstants.JSON_EXT, GeoJSONReader.class);
        FILE_READERS.put(OdConstants.GEOJSON_EXT, GeoJSONReader.class);
    }

    public static final Map<String, Class<? extends AbstractReader>> FILE_AND_ARCHIVE_READERS = new HashMap<>(FILE_READERS);
    static {
        FILE_AND_ARCHIVE_READERS.put(OdConstants.ZIP_EXT, ZipReader.class);
        FILE_AND_ARCHIVE_READERS.put(OdConstants.SEVENZIP_EXT, SevenZipReader.class);
    }

    public NetworkReader(String url, AbstractDataSetHandler handler, boolean promptUser) {
        CheckParameterUtil.ensureParameterNotNull(url, "url");
        this.url = url;
        this.handler = handler;
        this.readerClass = null;
        this.promptUser = promptUser;
    }

    public final File getReadFile() {
        return file;
    }

    public final String getReadFileName() {
        return filename;
    }

    private Class<? extends AbstractReader> findReaderByAttachment() {
        String cdisp = this.activeConnection.getResponse().getHeaderField("Content-disposition");
        if (cdisp != null) {
            Matcher m = Pattern.compile("attachment;.?filename=(.*)").matcher(cdisp);
            if (m.matches()) {
                filename = m.group(1);
                return findReaderByExtension(filename);
            }
        }
        return null;
    }

    private Class<? extends AbstractReader> findReaderByContentType() {
        String contentType = this.activeConnection.getResponse().getContentType();
        if (contentType.startsWith("application/zip")) {
            return ZipReader.class;
        } else if (contentType.startsWith("application/x-7z-compressed")) {
            return SevenZipReader.class;
        } else if (contentType.startsWith("application/vnd.ms-excel")) {
            return XlsReader.class;
        } else if (contentType.startsWith("application/geo+json") || contentType.startsWith("application/json")) {
            return GeoJSONReader.class;
        } else if (contentType.startsWith("application/octet-stream")) {
            //return OdsReader.class;//FIXME, can be anything
        } else if (contentType.startsWith("text/csv")) {
            return CsvReader.class;
        } else if (contentType.startsWith("text/plain")) { //TODO: extract charset
            return CsvReader.class;
        } else if (contentType.startsWith("tdyn/html")) {
            //return CsvReader.class;//FIXME, can also be .tar.gz
        } else {
            Logging.warn("Unsupported content type: "+contentType);
        }
        return null;
    }

    private static Class<? extends AbstractReader> findReaderByExtension(String filename) {
        filename = filename.replace("\"", "").toLowerCase(Locale.ROOT);
        for (Map.Entry<String, Class<? extends AbstractReader>> entry : FILE_AND_ARCHIVE_READERS.entrySet()) {
            if (filename.endsWith("."+entry.getKey())) {
                return entry.getValue();
            }
        }
        return null;
    }

    @Override
    public DataSet parseOsm(ProgressMonitor progressMonitor, Compression compression) throws OsmTransferException {
        if (compression != Compression.NONE) {
            Logging.warn("FIXME: unhandled compression"); // FIXME
        }
        return parseOsm(progressMonitor);
    }

    @Override
    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
        InputStream in = null;
        ProgressMonitor instance;
        try {
            in = getInputStreamRaw(url, progressMonitor);
            if (in == null)
                return null;
            progressMonitor.subTask(tr("Downloading data..."));
            if (readerClass == null) {
                readerClass = findReaderByAttachment();
            }
            if (readerClass == null) {
                readerClass = findReaderByExtension(url);
            }
            if (readerClass == null) {
                readerClass = findReaderByContentType();
            }
            if (readerClass == null) {
                throw new OsmTransferException("Cannot find appropriate reader !"); //TODO handler job ?
            } else if (findReaderByExtension(url) != null) {
                filename = url.substring(url.lastIndexOf('/')+1);
            }
            instance = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
            if (readerClass.equals(ZipReader.class) || readerClass.equals(SevenZipReader.class)) {
                ArchiveReader zipReader = readerClass.equals(ZipReader.class)
                        ? new ZipReader(in, handler, promptUser) : new SevenZipReader(in, handler, promptUser);
                        DataSet ds = zipReader.parseDoc(instance);
                        file = zipReader.getReadFile();
                        return ds;
            } else if (readerClass.equals(KmlReader.class)) {
                return KmlReader.parseDataSet(in, instance);
            } else if (readerClass.equals(KmzReader.class)) {
                return KmzReader.parseDataSet(in, instance);
            } else if (readerClass.equals(GeoJSONReader.class)) {
                return GeoJSONReader.parseDataSet(in, instance);
            } else if (readerClass.equals(MifReader.class)) {
                return MifReader.parseDataSet(in, null, handler);
            } else if (readerClass.equals(ShpReader.class)) {
                return ShpReader.parseDataSet(in, null, handler, instance);
            } else if (readerClass.equals(TabReader.class)) {
                return TabReader.parseDataSet(in, null, handler, instance);
            } else if (readerClass.equals(CsvReader.class)) {
                return CsvReader.parseDataSet(in, handler, instance);
            } else if (readerClass.equals(OdsReader.class)) {
                return OdsReader.parseDataSet(in, handler, instance);
            } else if (readerClass.equals(XlsReader.class)) {
                return XlsReader.parseDataSet(in, handler, instance);
            } else if (readerClass.equals(GmlReader.class)) {
                return GmlReader.parseDataSet(in, handler, instance);
            } else {
                throw new IllegalArgumentException("Unsupported reader class: "+readerClass.getName());
            }
        } catch (IOException | XMLStreamException | IllegalDataException e) {
            if (cancel)
                return null;
            throw new OsmTransferException(e);
        } finally {
            progressMonitor.finishTask();
            try {
                activeConnection = null;
                if (in != null) {
                    in.close();
                }
            } catch (IOException e) {
                Logging.trace(e);
            }
        }
    }
}
