/***************************** LICENSE START ***********************************

 Copyright 2012 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include <iostream>
#include <string>
#include <unordered_map>
#include <unordered_set>

#include "mars.h"

#include "MvScanFileType.h"
#include "MvNetCDF.h"

#include <sys/types.h>
#include <dirent.h>
#include <errno.h>

#include "MvLog.h"
#include "MvMiscellaneous.h"

//#include "netcdfcpp.h"


enum eFileType  //-- types checked by local func 'SearchMarsParserBreakers'
{
    eMissingFile,
    eBinaryFile,
    eNumericTextFile,
    eOtherTextFile
};

static const char* MvNetcdfType(const char* file, bool checkStatus = false);

//_______________________________________________________________________
//
// This function is used to check for files that contain only numeric
// values and thus cannot be Metview requests, but which can break
// MARS parser if allowed to be parsed (late 2006 MARS parser was
// modified to allow IDENT to contain '+' and spaces => too long IDENT!).
//
// Accept characters that are used in files containing only numbers:
// digits, plus, minus, decimal point, exponent char e/E and spaces
//

static bool isValidForNumbersOnly(int c)
{
    if (isdigit(c))
        return true;

    if (isspace(c))
        return true;

    if (c == '.' || c == '-' || c == '+')
        return true;

    if (c == 'e' || c == 'E')
        return true;

    return false;
}


//_______________________________________________________________________

#ifndef DOXYGEN_SHOULD_SKIP_THIS
struct File
{  //-- wrapper function always closes the file

    FILE* f_{nullptr};

public:
    File(const char* name, const char* mode) { f_ = fopen(name, mode); }
    ~File() { close(); }
    void close()
    {
        if (f_)
            fclose(f_);
        f_ = nullptr;
    }
};
#endif


//_______________________________________________________________________
//
// This function is used to find files that may break MARS parser,
// e.g. binary files and numeric-values-only text files (like the
// one produced by Cross Section).

static eFileType SearchMarsParserBreakers(const char* fileName)
{
    File f(fileName, "r");  //-- will be closed automatically

    if (!f.f_)
        return eMissingFile;  //-- missing file

    bool isBinary      = false;
    bool isNumbersOnly = true;
    const int CHECKLEN = 4000;  //-- may be a huge PostScript file
                                //-- => check beginning only is ok
    char buf;
    int chr;

    fread(&buf, 1, 1, f.f_);
    chr = (int)buf;

    for (int ci = 0; ci < CHECKLEN; ++ci)  //-- look for "binary" chars
    {
        if (feof(f.f_))
            break;

        if (!isprint(chr) && !isspace(chr)) {
            isBinary = true;  //-- binary file, stop here
            break;
        }

        if (isNumbersOnly)
            isNumbersOnly = isNumbersOnly && isValidForNumbersOnly(chr);

        fread(&buf, 1, 1, f.f_);
        chr = (int)buf;
    }

    if (isBinary)
        return eBinaryFile;  //-- binary files break MARS parser

    if (isNumbersOnly)
        return eNumericTextFile;  //-- 'numbers only' may break MARS parser

    return eOtherTextFile;  //-- other files should be parsable
}


static const char* isWeatherSymbolJson(const File& f)
{
    // TODO: initialise it from the WsItems file or IconClass
    static std::unordered_set<std::string> clsSet = {
        "WS_COLDFRONT", "WS_CONVERGENCELINE", "WS_HIGH", "WS_IMAGE",
        "WS_INSTABILITYLINE", "WS_ITCZ", "WS_ITTDISCONTINUITY",
        "WS_LOW", "WS_OCCLUDED", "WS_QUASISTATIONARY",
        "WS_PLACEMARK", "WS_RIDGE",
        "WS_MARKER", "WS_SHAPE", "WS_SHEARLINE", "WS_STORM", "WS_TEXT", "WS_TROPICALWAVE",
        "WS_TROUGH", "WS_WARMFRONT", "WS_WMOSYMBOL",
        "WS_COLLECTION"};
    static char startBraceCh    = '{';
    static std::string quoteCh  = "\"";
    static std::string classStr = "\"class_ws\":";
    static std::string conv     = "metview-ws";
    if (!f.f_)
        return nullptr;

    // We suppose that the json file has the following structure and the
    // "class_ws" and "convention" keys are located towards the beginning of the file:
    //
    // {
    //    "class_ws": "...",
    //    "convention": "metview-ws"
    //
    //
    // When the file is generated via the Metview Qt Json interface this seems
    // to be the case because the keys are always sorted alphabetically when
    // written to a file. Therefore we only read the first 200 bytes from the file!
    // The goal is to extract value for "class_ws".

    char asciiBuf[200];
    memset(&asciiBuf[0], '\0', 200);
    fread(&asciiBuf[0], 1, 99, f.f_);
    rewind(f.f_);

//    MvLog().dbg() << __func__ << " asciiBuf=" << asciiBuf;
    std::string t(asciiBuf);
    if (metview::startsWith(t, startBraceCh, true) && t.find(conv) != std::string::npos) {
        // the file is probably a metview ws json file
//        MvLog().dbg() << " match 1";
        auto pos = t.find(classStr);
        if (pos != std::string::npos) {
//            MvLog().dbg() << " match2";
            auto sp = t.find(quoteCh, pos + classStr.length());
            if (sp != std::string::npos) {
//                MvLog().dbg() << " match3";
                auto ep = t.find(quoteCh, sp + 1);
                if (ep != std::string::npos) {
                    auto cls = t.substr(sp + 1, ep - sp - 1);
                    auto it = clsSet.find(cls);
//                    MvLog().dbg() << " cls=" << cls;
                    if (it != clsSet.end()) {
                        return (*it).c_str();
                    }
                }
            }
        }
    }
    return nullptr;
}

//inline static bool scan_file_extension(const char* file, int length, const char* ext)
//{
//    int ext_length = strlen(ext);

//    return (length >= ext_length && !strcasecmp(&file[length - ext_length], ext));
//}

static bool get_file_extension(const char* file, std::string& suffix1,
                               std::string& suffix2)
{
    std::string fn(file);
    auto pos = fn.find_last_of(".");
    if (pos != 0 && pos != std::string::npos && pos + 1 != fn.size()) {
        suffix1   = fn.substr(pos);
        auto pos2 = fn.find_last_of(".", pos - 1);
        if (pos != 0 && pos2 != std::string::npos && pos2 + 1 != pos) {
            suffix2 = fn.substr(pos2);
        }
        return true;
    }
    return false;
}

// Checks if a directory is an ODB database

static bool scanDirForOdb(const char* dir)
{
    DIR* dp;
    struct dirent* dirp;
    if ((dp = opendir(dir)) == nullptr) {
        std::cout << "scanDirForOdb() --> Error(" << errno << ") opening " << dir << std::endl;
        return false;
    }

    bool retval = false;
    std::string::size_type pos;
    while ((dirp = readdir(dp)) != nullptr) {
        std::string name(dirp->d_name);

        //std::cout << "dir: " << name;
        if ((pos = name.find(".sch")) != std::string::npos &&
            name.size() >= 5 && pos > 0 && pos + 4 == name.size()) {
            retval = true;
            break;
        }
    }
    closedir(dp);
    return retval;
}

// scan_file_extension
// - checks whether the extension on the given file matches the given
//   extension, and returns the corresponding Metview class name
//   if it does
//_______________________________________________________________________

//
// NOTE: originally it was a  close cousin of scan_file(...) is in
// ./src/libMars/guess.c !!!
//

// store the suffix based mappings in hash tables
static std::unordered_map<std::string, const char*> doubleSuffixMap = {
    {".tar.gz", "TAR_GZ"},
    {".tar.bz", "TAR_BZ"},
    {".tar.bz2", "TAR_BZ2"},
    {".geo.json", "GEOJSON"}};

static std::unordered_map<std::string, const char*> singleSuffixMap = {
    {".tar", "TAR"},
    {".magml", "MAGML"},
    {".html", "ZIP"},
    {".pdf", "PDF"},
    {".png", "PNG"},
    {".gif", "GIF"},
    {".ps", "PSFILE"},
    {".tiff", "TIFF"},
    {".tif", "TIFF"},
    {".svg", "SVG"},
    {".kml", "KML"},
    {".kmz", "KML"},
    {".mv", "MACRO"},
    {".csv", "TABLE"},
    {".tsv", "TABLE"},
    {".vdf", "VAPOR_VDF_FILE"},
    {".xpif", "XPIF"},
    {".gtif", "GEOTIFF"},
    {".geojson", "GEOJSON"},
    {".jpg", "JPEG"},
    {".jpeg", "JPEG"},
    {".py", "PYTHON"}};

static const char* scan_file(const char* file)
{
    static const int cMAXSHIFT        = 8;  //-- search tolerance for word "GRIB" or "BUFR"
    static const int cMINREAD         = 4;  //-- need to be able to read at least this much
//    static const std::string cGEOJSON = ".geojson";
    static const char* cGEOJSON = "GEOJSON";

    File f(file, "r");
    if (!f.f_) {
        return "BAD";  //-- missing or non-readable file
    }

    // Determine type by the suffixes
    std::string suffix1, suffix2;
    bool geojsonBySuffix=false;
    if (get_file_extension(file, suffix1, suffix2)) {
        if (!suffix2.empty()) {
            metview::toLower(suffix2);
            auto it = doubleSuffixMap.find(suffix2);
            if (it != doubleSuffixMap.end()) {
                geojsonBySuffix = (strcmp(it->second, cGEOJSON) == 0);
                if (!geojsonBySuffix) {
                    return it->second;
                }
            }
        }
        if (!suffix1.empty()) {
            metview::toLower(suffix1);
            auto it = singleSuffixMap.find(suffix1);
            if (it != singleSuffixMap.end()) {
                geojsonBySuffix = (strcmp(it->second, cGEOJSON) == 0);
                if (!geojsonBySuffix) {
                    return it->second;
                }
            }
        }
    }

    // Weather symbols with geojson suffix
    if (geojsonBySuffix) {
        auto ws = isWeatherSymbolJson(f);
        if (ws) {
            return ws;
        } else {
            return cGEOJSON;
        }
    }

    //-- binary? only numeric values?
    eFileType fileType = SearchMarsParserBreakers(file);

    union
    {
        char c;
        short s;
        long l;
        char ch[cMAXSHIFT + 6];  //-- must be at least 14 chars
    } buf;
    memset(&buf, 0, sizeof(buf));

    // read the first few bytes
    int readCount = fread((char*)&buf, 1, sizeof(buf), f.f_);

    // not enough bytes for safe tests
    if (readCount < cMINREAD) {
        return fileType == eBinaryFile ? "BINARY" : "NOTE";
        // first check for binary types
    }
    else if (fileType == eBinaryFile) {
        // for GRIB & BUFR allow some leading rubbish
        for (int s = 0; s <= cMAXSHIFT; ++s) {
            if (strncmp(buf.ch + s, "GRIB", 4) == 0)
                return "GRIB";

            if (strncmp(buf.ch + s, "BUFR", 4) == 0)
                return "BUFR";

            if (strncmp(buf.ch + s, "ODA", 3) == 0)
                return "ODB_DB";
        }

        if (strncmp(buf.ch, "METVIEW_VECTOR", 14) == 0)
            return "VECTOR";

        if (strncmp(buf.ch, "TIDE", 4) == 0)
            return "GRIB";

        if (strncmp(buf.ch, "BUDG", 4) == 0)
            return "GRIB";

        if (strncmp(buf.ch, "CDF", 3) == 0)
            return MvNetcdfType(file);

        if (strncmp(buf.ch + 1, "HDF", 3) == 0)
            return "NETCDF";

        if (strncmp(buf.ch + 1, "PNG", 3) == 0)
            return "PNG";

        if (strncmp(buf.ch + 6, "JFIF", 4) == 0)
            return "JPEG";

        if (strncmp(buf.ch + 6, "Exif", 4) == 0)
            return "JPEG";

        // PDF can be binary
        if (strncmp(buf.ch, "%PDF", 4) == 0)
            return "PDF";

        //"NUMERIC" - need to avoid MARS parser!
    }
    else if (fileType == eNumericTextFile) {
        return "NOTE";
        // check for non-request text formats
    }
    else if (fileType == eOtherTextFile) {
        if (strncmp(buf.ch, "%!", 2) == 0) {
            return "PSFILE";
            // PDF can be text
        }
        else if (strncmp(buf.ch, "%PDF", 4) == 0) {
            return "PDF";
        }
        else if (strncmp(buf.ch, "#!", 2) == 0) {
            return "SHELL";
        }
        else if (strncmp(buf.ch, "#GEOPOINTSET", 12) == 0) {
            return "GEOPOINTSET";
        }
        else if (strncmp(buf.ch, "#GEO", 4) == 0) {
            return "GEOPOINTS";
        }
        else if (strncmp(buf.ch, "#LLM", 4) == 0) {
            return "LLMATRIX";
        }
        else if (strncmp(buf.ch, "#LLV", 4) == 0) {
            return "LLVALUE";
        }
        else if (strncmp(buf.ch, "#MACRO", 6) == 0 ||
                 strncmp(buf.ch, "# Macro", 7) == 0 ||
                 strncmp(buf.ch, "#Metview", 8) == 0 ||
                 strncmp(buf.ch, "# Metview", 9) == 0) {
            return "MACRO";
        }
        else if (strncmp(buf.ch, "<magics", 7) == 0) {
            return "MAGML";
        }
        else if (strncmp(buf.ch, "#obstat scatter", 15) == 0) {
            return "OBSTAT_SCATTER";
        }
        else if (strncmp(buf.ch, "#obstat", 7) == 0) {
            return "OBSTAT_CURVE";
        }
    }

    rewind(f.f_);

    // Check for custom ASCII files

    // Weather symbols when we do not have a geojson suffix
    if (fileType == eOtherTextFile && !geojsonBySuffix) {
        auto ws = isWeatherSymbolJson(f);
        if (ws) {
            return ws;
        }
    }

    // FLEXTRA
    // We need at least 311 characters for flextra!
    char asciiBuf[400];
    memset(&asciiBuf[0], '\0', 400);
    fread(&asciiBuf[0], 1, 311, f.f_);
    //Flextra output files
    if (strstr(asciiBuf, "FLEXTRA MODEL OUTPUT") != 0) {
        return "FLEXTRA_FILE";
        //IFS namelist file
    }
    else if (strstr(asciiBuf, "&NAM") != 0 || strstr(asciiBuf, "&NAE") != 0 ||
             strstr(asciiBuf, "&NEM") != 0) {
        return "NAMELIST";
        // RTTOV output files - we may need to adjust this test in the future
    }
    else if (strstr(asciiBuf, "# RTTOV OUTPUT") != 0) {
        return "RTTOV_OUTPUT_FILE";
    }

    rewind(f.f_);

    // if it's binary then it might actually be a GRIB file with some extra
    // header bits at the start e.g. GTS). ecCodes has functionality to
    // bypass these, so if it can get a GRIB handle then it must be a GRIB file.
    // This may not be the most efficient way to find out, but it's a last resort.

    if (fileType == eBinaryFile) {
        grib_handle* gh;
        int ge;
        gh = grib_handle_new_from_file(nullptr, f.f_, &ge);
        if (gh != nullptr) {
            grib_handle_delete(gh);
            return "GRIB";
        }
    }

    //We have to do this check at this point, after
    //FLEXTRA was checked! FLEXTRA v5 generates ASCII files
    //with some rubbish in them and we identify them as BINARY files!!
    if (fileType == eBinaryFile)
        return "BINARY";

    fclose(f.f_);    //-- destructor will not close (end-of-file => !f.f_)
    f.f_ = nullptr;  //-- but after fclose destructor would try and...

    return "NOTE";  //-- unknown text file; make it NOTE
}

//_______________________________________________________________________

std::string ScanFileType(const char* file)
{
    struct stat buf;

    if (stat(file, &buf) < 0)
        return "BAD";

    switch (buf.st_mode & S_IFMT) {
        case S_IFDIR:
            return (scanDirForOdb(file) == true) ? "ODB_DB" : "FOLDER";
        case S_IFREG:
            return scan_file(file);

        default:
            return "SPECIAL";
    }
}

//_______________________________________________________________________

//----
//--- Check if a file is a binary file or a missing file i.e.
//--  does the file have any chance to contain a Metview request!
//-

bool IsBinaryOrMissingFile(const char* fileName)
{
    eFileType t = SearchMarsParserBreakers(fileName);
    return t == eBinaryFile || t == eMissingFile;
}

//_______________________________________________________________________

int MvGribVersion(const char* fileName)
{
    File f(fileName, "r");
    if (!f.f_)
        return -13;  //-- missing or non-readable file

    char buf[10];

    fread(buf, 1, 9, f.f_);

    int gribVersion = buf[7];  //-- octet 8

    return gribVersion;
}

//----------------------------------------------------------------------------
//
// Here we suppose that the file type was identified as ODB previously. Now
// we want to find out the type only!!
//
//----------------------------------------------------------------------------

std::string MvOdbType(const char* file, bool scanBeforeCheck)
{
    std::string t;

    if (scanBeforeCheck) {
        t = ScanFileType(file);
        if (t != "ODB_DB") {
            return t;
        }
    }

    //At this point type has to be ODB_DB
    //We check if it is a file or directory

    struct stat buf;

    if (stat(file, &buf) < 0)
        return "BAD";

    if (buf.st_size == 0)
        return "NOTE";

    switch (buf.st_mode & S_IFMT) {
        case S_IFDIR:
            return "ODB_OLD";
            break;
        case S_IFREG:
            return "ODB_NEW";
            break;
        default:
            return "SPECIAL";
    }

    return "BAD";
}

const char* MvNetcdfType(const char* file, bool checkStatus)
{
    if (checkStatus) {
        struct stat buf;
        if (stat(file, &buf) < 0)
            return "BAD";
        else if (buf.st_size == 0)
            return "NOTE";
    }

    //F	NcFile nc(file);
    MvNetCDF nc(std::string(file), NC_NOWRITE);
    if (!nc.isValid()) {
        return "BINARY";
    }

    //Check for SCM input data
    int n = nc.getNumberOfAttributes();  //Fnum_atts();
    for (int i = 0; i < n; i++) {
        //F NcAtt* att=nc.get_att(i);
        MvNcAtt* att = nc.getAttribute(i);
        if (att != nullptr && att->name() != nullptr &&
            strcmp(att->name(), "dataID") == 0 && att->getNumberOfValues() > 0)  //F num_vals()
        {
            auto vStr = att->as_string(0);
            if (!vStr.empty()) {
                if (vStr.find("scm_") == 0 || vStr.find("SCM_INPUT") == 0) {
                    return "SCM_INPUT_DATA";
                }
                else if (vStr.find("SCM_OUTPUT") == 0) {
                    return "SCM_OUTPUT_DATA";
                }
                else if (vStr.find("NETCDF_RTTOV_INPUT") == 0) {
                    return "NETCDF_RTTOV_INPUT";
                }
            }
        }
        //F		if(att != nullptr)
        //F			delete att;
    }

    return "NETCDF";
}
