/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.tika.parser.gdal;

//JDK imports
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TemporaryResources;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AbstractParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.external.ExternalParser;
import org.apache.tika.sax.XHTMLContentHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.tika.parser.external.ExternalParser.INPUT_FILE_TOKEN;

//Tika imports
//SAX imports

/**
 * Wraps execution of the <a href="http//gdal.org/">Geospatial Data Abstraction
 * Library (GDAL)</a> <code>gdalinfo</code> tool used to extract geospatial
 * information out of hundreds of geo file formats.
 * <p/>
 * The parser requires the installation of GDAL and for <code>gdalinfo</code> to
 * be located on the path.
 * <p/>
 * Basic information (Size, Coordinate System, Bounding Box, Driver, and
 * resource info) are extracted as metadata, and the remaining metadata patterns
 * are extracted and added.
 * <p/>
 * The output of the command is available from the provided
 * {@link ContentHandler} in the
 * {@link #parse(InputStream, ContentHandler, Metadata, ParseContext)} method.
 */
public class GDALParser extends AbstractParser {

    private static final long serialVersionUID = -3869130527323941401L;
    private static final Logger LOG = LoggerFactory.getLogger(GDALParser.class);

    private String command;

    public GDALParser() {
        setCommand("gdalinfo ${INPUT}");
    }

    public void setCommand(String command) {
        this.command = command;
    }

    public String getCommand() {
        return this.command;
    }

    public String processCommand(InputStream stream) {
        TikaInputStream tis = (TikaInputStream) stream;
        String pCommand = this.command;
        try {
            if (this.command.contains(INPUT_FILE_TOKEN)) {
                pCommand = this.command.replace(INPUT_FILE_TOKEN, tis.getFile()
                        .getPath());
            }
        } catch (Exception e) {
            LOG.warn("exception processing command", e);
        }

        return pCommand;
    }

    @Override
    public Set<MediaType> getSupportedTypes(ParseContext context) {
        Set<MediaType> types = new HashSet<MediaType>();
        types.add(MediaType.application("x-netcdf"));
        types.add(MediaType.application("vrt"));
        types.add(MediaType.image("geotiff"));
        types.add(MediaType.image("nitf"));
        types.add(MediaType.application("x-rpf-toc"));
        types.add(MediaType.application("x-ecrg-toc"));
        types.add(MediaType.image("hfa"));
        types.add(MediaType.image("sar-ceos"));
        types.add(MediaType.image("ceos"));
        types.add(MediaType.application("jaxa-pal-sar"));
        types.add(MediaType.application("gff"));
        types.add(MediaType.application("elas"));
        types.add(MediaType.application("aig"));
        types.add(MediaType.application("aaigrid"));
        types.add(MediaType.application("grass-ascii-grid"));
        types.add(MediaType.application("sdts-raster"));
        types.add(MediaType.application("dted"));
        types.add(MediaType.image("png"));
        types.add(MediaType.image("jpeg"));
        types.add(MediaType.image("raster"));
        types.add(MediaType.application("jdem"));
        types.add(MediaType.image("gif"));
        types.add(MediaType.image("big-gif"));
        types.add(MediaType.image("envisat"));
        types.add(MediaType.image("fits"));
        types.add(MediaType.application("fits"));
        types.add(MediaType.image("bsb"));
        types.add(MediaType.application("xpm"));
        types.add(MediaType.image("bmp"));
        types.add(MediaType.image("x-dimap"));
        types.add(MediaType.image("x-airsar"));
        types.add(MediaType.application("x-rs2"));
        types.add(MediaType.application("x-pcidsk"));
        types.add(MediaType.application("pcisdk"));
        types.add(MediaType.image("x-pcraster"));
        types.add(MediaType.image("ilwis"));
        types.add(MediaType.image("sgi"));
        types.add(MediaType.application("x-srtmhgt"));
        types.add(MediaType.application("leveller"));
        types.add(MediaType.application("terragen"));
        types.add(MediaType.application("x-gmt"));
        types.add(MediaType.application("x-isis3"));
        types.add(MediaType.application("x-isis2"));
        types.add(MediaType.application("x-pds"));
        types.add(MediaType.application("x-til"));
        types.add(MediaType.application("x-ers"));
        types.add(MediaType.application("x-l1b"));
        types.add(MediaType.image("fit"));
        types.add(MediaType.application("x-grib"));
        types.add(MediaType.image("jp2"));
        types.add(MediaType.application("x-rmf"));
        types.add(MediaType.application("x-wcs"));
        types.add(MediaType.application("x-wms"));
        types.add(MediaType.application("x-msgn"));
        types.add(MediaType.application("x-wms"));
        types.add(MediaType.application("x-wms"));
        types.add(MediaType.application("x-rst"));
        types.add(MediaType.application("x-ingr"));
        types.add(MediaType.application("x-gsag"));
        types.add(MediaType.application("x-gsbg"));
        types.add(MediaType.application("x-gs7bg"));
        types.add(MediaType.application("x-cosar"));
        types.add(MediaType.application("x-tsx"));
        types.add(MediaType.application("x-coasp"));
        types.add(MediaType.application("x-r"));
        types.add(MediaType.application("x-map"));
        types.add(MediaType.application("x-pnm"));
        types.add(MediaType.application("x-doq1"));
        types.add(MediaType.application("x-doq2"));
        types.add(MediaType.application("x-envi"));
        types.add(MediaType.application("x-envi-hdr"));
        types.add(MediaType.application("x-generic-bin"));
        types.add(MediaType.application("x-p-aux"));
        types.add(MediaType.image("x-mff"));
        types.add(MediaType.image("x-mff2"));
        types.add(MediaType.image("x-fujibas"));
        types.add(MediaType.application("x-gsc"));
        types.add(MediaType.application("x-fast"));
        types.add(MediaType.application("x-bt"));
        types.add(MediaType.application("x-lan"));
        types.add(MediaType.application("x-cpg"));
        types.add(MediaType.image("ida"));
        types.add(MediaType.application("x-ndf"));
        types.add(MediaType.image("eir"));
        types.add(MediaType.application("x-dipex"));
        types.add(MediaType.application("x-lcp"));
        types.add(MediaType.application("x-gtx"));
        types.add(MediaType.application("x-los-las"));
        types.add(MediaType.application("x-ntv2"));
        types.add(MediaType.application("x-ctable2"));
        types.add(MediaType.application("x-ace2"));
        types.add(MediaType.application("x-snodas"));
        types.add(MediaType.application("x-kro"));
        types.add(MediaType.image("arg"));
        types.add(MediaType.application("x-rik"));
        types.add(MediaType.application("x-usgs-dem"));
        types.add(MediaType.application("x-gxf"));
        types.add(MediaType.application("x-dods"));
        types.add(MediaType.application("x-http"));
        types.add(MediaType.application("x-bag"));
        types.add(MediaType.application("x-hdf"));
        types.add(MediaType.image("x-hdf5-image"));
        types.add(MediaType.application("x-nwt-grd"));
        types.add(MediaType.application("x-nwt-grc"));
        types.add(MediaType.image("adrg"));
        types.add(MediaType.image("x-srp"));
        types.add(MediaType.application("x-blx"));
        types.add(MediaType.application("x-rasterlite"));
        types.add(MediaType.application("x-epsilon"));
        types.add(MediaType.application("x-sdat"));
        types.add(MediaType.application("x-kml"));
        types.add(MediaType.application("x-xyz"));
        types.add(MediaType.application("x-geo-pdf"));
        types.add(MediaType.image("x-ozi"));
        types.add(MediaType.application("x-ctg"));
        types.add(MediaType.application("x-e00-grid"));
        types.add(MediaType.application("x-zmap"));
        types.add(MediaType.application("x-webp"));
        types.add(MediaType.application("x-ngs-geoid"));
        types.add(MediaType.application("x-mbtiles"));
        types.add(MediaType.application("x-ppi"));
        types.add(MediaType.application("x-cappi"));
        return types;
    }

    @Override
    public void parse(InputStream stream, ContentHandler handler,
                      Metadata metadata, ParseContext context) throws IOException,
            SAXException, TikaException {

        if (!ExternalParser.check("gdalinfo")) {
            return;
        }

        // first set up and run GDAL
        // process the command
        TemporaryResources tmp = new TemporaryResources();
        TikaInputStream tis = TikaInputStream.get(stream, tmp);

        String runCommand = processCommand(tis);
        String output = execCommand(new String[]{runCommand});

        // now extract the actual metadata params
        // from the GDAL output in the content stream
        // to do this, we need to literally process the output
        // from the invoked command b/c we can't read metadata and
        // output text from the handler in ExternalParser
        // at the same time, so for now, we can't use the
        // ExternalParser to do this and I've had to bring some of
        // that functionality directly into this class
        // TODO: investigate a way to do both using ExternalParser

        extractMetFromOutput(output, metadata);
        applyPatternsToOutput(output, metadata, getPatterns());

        // make the content handler and provide output there
        // now that we have metadata
        processOutput(handler, metadata, output);
    }

    private Map<Pattern, String> getPatterns() {
        Map<Pattern, String> patterns = new HashMap<Pattern, String>();
        this.addPatternWithColon("Driver", patterns);
        this.addPatternWithColon("Files", patterns);
        this.addPatternWithIs("Size", patterns);
        this.addPatternWithIs("Coordinate System", patterns);
        this.addBoundingBoxPattern("Upper Left", patterns);
        this.addBoundingBoxPattern("Lower Left", patterns);
        this.addBoundingBoxPattern("Upper Right", patterns);
        this.addBoundingBoxPattern("Lower Right", patterns);
        return patterns;
    }

    private void addPatternWithColon(String name, Map<Pattern, String> patterns) {
        patterns.put(
                Pattern.compile(name + "\\:\\s*([A-Za-z0-9/ _\\-\\.]+)\\s*"),
                name);
    }

    private void addPatternWithIs(String name, Map<Pattern, String> patterns) {
        patterns.put(Pattern.compile(name + " is ([A-Za-z0-9\\.,\\s`']+)"),
                name);
    }

    private void addBoundingBoxPattern(String name,
                                       Map<Pattern, String> patterns) {
        patterns.put(
                Pattern.compile(name
                        + "\\s*\\(\\s*([0-9]+\\.[0-9]+\\s*,\\s*[0-9]+\\.[0-9]+\\s*)\\)\\s*"),
                name);
    }

    private void extractMetFromOutput(String output, Metadata met) {
        Scanner scanner = new Scanner(output);
        String currentKey = null;
        String[] headings = {"Subdatasets", "Corner Coordinates"};
        StringBuilder metVal = new StringBuilder();
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            if (line.contains("=") || hasHeadings(line, headings)) {
                if (currentKey != null) {
                    // time to flush this key and met val
                    met.add(currentKey, metVal.toString());
                }
                metVal.setLength(0);

                String[] lineToks = line.split("=");
                currentKey = lineToks[0].trim();
                if (lineToks.length == 2) {
                    metVal.append(lineToks[1]);
                } else {
                    metVal.append("");
                }
            } else {
                metVal.append(line);
            }

        }
    }

    private boolean hasHeadings(String line, String[] headings) {
        if (headings != null && headings.length > 0) {
            for (String heading : headings) {
                if (line.contains(heading)) {
                    return true;
                }
            }
            return false;
        } else return false;
    }

    private void applyPatternsToOutput(String output, Metadata metadata,
                                       Map<Pattern, String> metadataPatterns) {
        Scanner scanner = new Scanner(output);
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            for (Pattern p : metadataPatterns.keySet()) {
                Matcher m = p.matcher(line);
                if (m.find()) {
                    if (metadataPatterns.get(p) != null
                            && !metadataPatterns.get(p).equals("")) {
                        metadata.add(metadataPatterns.get(p), m.group(1));
                    } else {
                        metadata.add(m.group(1), m.group(2));
                    }
                }
            }
        }

    }

    private String execCommand(String[] cmd) throws IOException {
        // Execute
        Process process;
        String output = null;
        if (cmd.length == 1) {
            process = Runtime.getRuntime().exec(cmd[0]);
        } else {
            process = Runtime.getRuntime().exec(cmd);
        }

        try {
            InputStream out = process.getInputStream();

            try {
                output = extractOutput(out);
            } catch (Exception e) {
                LOG.warn("Exception extracting output", e);
                output = "";
            }

        } finally {
            try {
                process.waitFor();
            } catch (InterruptedException ignore) {
            }
        }
        return output;

    }

    private String extractOutput(InputStream stream) throws SAXException,
            IOException {
        StringBuilder sb = new StringBuilder();
        try (Reader reader = new InputStreamReader(stream, UTF_8)) {
            char[] buffer = new char[1024];
            for (int n = reader.read(buffer); n != -1; n = reader.read(buffer)) {
                sb.append(buffer, 0, n);
            }
        }
        return sb.toString();
    }

    private void processOutput(ContentHandler handler, Metadata metadata,
                               String output) throws SAXException, IOException {
        XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata);
        InputStream stream = new ByteArrayInputStream(output.getBytes(UTF_8));
        try (Reader reader = new InputStreamReader(stream, UTF_8)) {
            xhtml.startDocument();
            xhtml.startElement("p");
            char[] buffer = new char[1024];
            for (int n = reader.read(buffer); n != -1; n = reader.read(buffer)) {
                xhtml.characters(buffer, 0, n);
            }
            xhtml.endElement("p");

        } finally {
            xhtml.endDocument();
        }

    }

}
