/***************************** 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 "ObjectInfo.h"

#include <sstream>

#include <dirent.h>
#include <MvRequest.h>
#include <MvPath.hpp>
#include <Assertions.hpp>
#include <MvScanFileType.h>
#include <MvRequestUtil.hpp>

#include "ObjectList.h"
#include "PlotModConst.h"
#include "MacroConverter.h"
#include "MvIcon.h"

using ExpandedMap = std::map<Cached, Cached>;
using ExpandedIterator = ExpandedMap::iterator;

NamesMap ObjectInfo::namesMap_;

// -- METHOD :
//
// -- PURPOSE:
//
// -- INPUT  :
//
// -- OUTPUT :
void ObjectInfo::PutNewLine(const Cached& newLine)
{
    description_.push_back(newLine);
}

void ObjectInfo::ConvertRequestToMacro(MvRequest& inRequest,
                                       MacroConversion lastComma, Cached varName,
                                       Cached macroName, std::set<Cached> skipSet,
                                       bool onlySub)
{
    // Initializations
    ExpandedMap expandedMap;
    int id = 1;
    MvRequest tmpRequest = inRequest;
    int count = inRequest.countParameters();
    int i = 0;

    // Compute maximum parameters length names
    int size = MaximumParameterNameLength(inRequest);

    // Loop all parameters
    for (i = 0; i < count; i++) {
        const char* param = inRequest.getParameter(i);

        const char* value = nullptr;
        inRequest.getValue(value, param, 0);
        if (!value) {
            // No value, so check if there is a subrequest
            MvRequest subReq = inRequest.getSubrequest(param);
            if (!subReq)
                continue;

            // There is a subrequest
            value = "#";
        }

        // Strip out the debug information and anything that should be skipped
        if ((strncmp(param, "_", 1) == 0) || (skipSet.find(param) != skipSet.end())) {
            tmpRequest.unsetParam(param);
            continue;
        }

        if (inRequest.countValues(param) > 1)
            continue;

        MvRequest subRequest;
        if (strcmp(value, "#") == 0)  // Subrequest
        {
            subRequest = inRequest.getSubrequest(param);
            bool subExpanded = false;
            Cached varName;

            // Can we follow subRequest also ?
            if ((const char*)subRequest("_NAME")) {
                std::string fullName = MakeUserPath((const char*)subRequest("_NAME"));
                if (FileCanBeOpened(fullName.c_str(), "r")) {
                    Cached returnName = Convert(subRequest);
                    if (strcmp(returnName, "") != 0) {
                        varName = returnName;
                        subExpanded = true;
                    }
                }
            }

            // Could not follow the subRequest, expand it as it is.
            if (!subExpanded) {
                varName = Cached(param) + id++;
                ConvertRequestToMacro(subRequest, PUT_END, varName,
                                      ObjectList::MacroName(subRequest.getVerb()));
            }

            tmpRequest.unsetParam(param);
            expandedMap[param] = varName;
        }

        else if (ObjectInfo::IsAName(value)) {
            // Try to std::find out if this is a file reference.
            // We need to get the full path
            std::string tmpName = value;
            if (metview::IsParameterSet(inRequest, "_NAME"))
                tmpName = std::string(mdirname(inRequest("_NAME"))) + "/" + value;

            std::string refName = MakeUserPath(tmpName);
            if (access(refName.c_str(), R_OK) == 0) {
                // If it is a DATA (binary) file, creates a dummy request.
                // The crucial information needed is parameter _NAME
                if (!IsBinaryOrMissingFile(refName.c_str()))
                    subRequest.read(refName.c_str());
                else
                    subRequest.setVerb("DATA");

                if (!metview::IsParameterSet(subRequest, "_NAME"))
                    subRequest("_NAME") = tmpName.c_str();

                Cached returnName = Convert(subRequest);
                if (strcmp(returnName, "") != 0) {
                    tmpRequest.unsetParam(param);
                    expandedMap[param] = returnName;
                }
            }
        }
    }

    if (onlySub == false) {
        // Now handle the fields that are not subrequests or references.
        // We have expanded all, now we need to print the header for the
        // original request.
        // Convert macroName to lower case.
        char* macrolower = new char[strlen(macroName) + 1];
        strcpy(macrolower, macroName);
        for (i = 0; i < (int)strlen(macrolower); i++)
            macrolower[i] = tolower(macrolower[i]);

        PutNewLine("");
        PutNewLine(ObjectInfo::SpaceToUnderscore(varName) + Cached(" = ") + Cached(macrolower) + Cached("("));

        delete[] macrolower;

        count = tmpRequest.countParameters();
        for (i = 0; i < count; i++) {
            const char* param = tmpRequest.getParameter(i);
            int n = tmpRequest.countValues(param);
            Cached valueList;

            // is there more than one value ?
            if (n > 1)
                valueList = Cached("[ ");  // add a list

            if (n == 0)
                continue;

            for (int j = 0; j < n; j++) {
                // put a "comma" before the new value
                if (j > 0)
                    valueList = valueList + Cached(",");

                // add a new value to the list
                const char* val = nullptr;
                tmpRequest.getValue(val, param, j);
                valueList = valueList + FormatValue(val, param, macroName);
            }

            // close the list, if needed
            if (n > 1)
                valueList = valueList + Cached(" ]");

            // add a new value to the description
            // remember the last comma
            Cached newLine;
            Cached comma = ",";
            if (i == (count - 1)) {
                if (expandedMap.size() == 0 && (lastComma == NO_LAST_COMMA || lastComma == PUT_END))
                    comma = "";
            }

            FormatLine(param, valueList, comma, size);
        }

        // At last print out the fields that has been expanded
        int j = 0;
        auto ii = expandedMap.begin();
        for (; ii != expandedMap.end(); ii++, j++) {
            Cached comma = ",";
            if (j == (signed)(expandedMap.size() - 1)) {
                if (lastComma == NO_LAST_COMMA || lastComma == PUT_END)
                    comma = "";
            }

            FormatLine((*ii).first, (*ii).second, comma, size);
        }

        if (lastComma == PUT_END)
            this->PutNewLine("   )");
    }
}

// -- METHOD :
//
// -- PURPOSE:
//
// -- INPUT  :
//
// -- OUTPUT :

Cached
ObjectInfo::ConvertDataUnitToMacro(MvIcon& dataUnit)
{
    // Retrieve the request associated to the dataUnit
    MvRequest duRequest = dataUnit.Request();
    Cached objectName;

    if (isPrint_) {
        Cached fileName;

        MvRequest svcRequest = dataUnit.SvcRequest();
        if (svcRequest.getVerb() == NETCDF)
            fileName = svcRequest("PATH");
        else if (duRequest.getVerb() == GRIB)
            fileName = duRequest("PATH");

        if (FileCanBeOpened(fileName, "r")) {
            Cached objectName = Cached("DataUnit") + dataUnit.Id();
            this->PutNewLine(Cached("# Importing   ") + objectName);
            this->PutNewLine(objectName + Cached(" = read(\"") + fileName + Cached("\")"));
            return objectName;
        }
    }

    int use_mode = duRequest("_USE_MODE");
    if (!use_mode) {
        MvRequest reqAux = duRequest.getSubrequest("_ORIGINAL_REQUEST");
        if (reqAux && (const char*)reqAux("_MACRO_DECODE_TAG"))
            objectName = Convert(reqAux);
        else
            objectName = Convert(duRequest);
    }
    else {
        MvRequest modeRequest = duRequest.getSubrequest("_MODE");
        if ((const char*)duRequest("_NAME"))
            modeRequest("_NAME") = duRequest("_NAME");
        if ((const char*)duRequest("_CLASS"))
            modeRequest("_CLASS") = duRequest("_CLASS");
        if ((const char*)duRequest("_APPL"))
            modeRequest("_APPL") = duRequest("_APPL");

        // Call itself with the mode request.
        MvIcon tmpIcon(modeRequest);
        objectName = ConvertDataUnitToMacro(tmpIcon);
    }

    return objectName;
}

Cached
ObjectInfo::ConvertIconToMacro(MvRequest& iconRequest, Cached defVal, int id)
{
    int first = 0;
    Cached returnName;

    // Cater for request containing more than one request.
    while (iconRequest) {
        Cached uiName = ObjectInfo::ObjectName(iconRequest, defVal, id);

        // Find out the iconClass and macro class name
        Cached iconClass = ObjectInfo::IconClass(iconRequest);

        if (!strcmp("", uiName))
            uiName = defVal + id;

        Cached macroName = ObjectList::MacroName(iconClass);

        // Convert the user interface name to a macro readable name
        Cached iconName = ObjectInfo::SpaceToUnderscore(uiName);

        if (first > 0)
            iconName = iconName + Cached("_") + first;

        // Write the contents of the icon
        this->ConvertRequestToMacro(iconRequest, PUT_END, iconName, macroName);

        if (first > 0)
            returnName = returnName + Cached(", ");

        returnName = returnName + iconName;
        first++;
        iconRequest.advance();
    }

    return returnName;
}

// -- METHOD :  Write Description
//
// -- PURPOSE:  Write a description to a file
//
// -- INPUT  :  A pointer to a file
//
// -- OUTPUT :  Description written in the file

void ObjectInfo::WriteDescription(FILE* filePtr)
{
    require(filePtr != nullptr);

    auto line = description_.begin();

    while (line != description_.end()) {
        Cached newLine = (*line);
        fprintf(filePtr, "%s%s\n", linePrefix_.c_str(), (const char*)newLine);

        ++line;
    }
}

// =======================================================
//
//  Static methods - Used in General by PlotMod
//


// -- METHOD : ObjectPath
//
// -- PURPOSE: Provide the complete path to the object
//
// -- INPUT  : A request which describes the object
//
// -- OUTPUT : Fully-qualified path

// FAMI20170324 : this (Cached) version should be replaced by the
// (string ObjectFullPath) version defined below. After this is done,
// Metview needs to be tested because this function is a basic
// function used everywhere.
Cached
ObjectInfo::ObjectPath(const MvRequest& request)
{
    Cached path;

    const char* mPath = request("_PATH");
    if (mPath)
    //              Path contains file name, it shouldn't but .....
    //              If it's been corrected, use following commented line
    //		return Cached( mPath );
    {
        path = Cached(mdirname(mPath));
        return path;
    }

    // Have we been called from a macro ??
    if (ObjectList::CalledFromMacro(request) == true) {
        // Macro should give us the fully qualified path
        const char* mvPath = request("_MACRO");
        path = Cached(mdirname(mvPath));
    }
    else {
        // Obtain the fully qualified path to the object
        const char* mvPath = request("_NAME");
        path = MakeUserPath(mdirname(mvPath)).c_str();
    }

    // Try to open a directory - if failed, set the path to METVIEW_USER_DIR

    DIR* metDir = nullptr;
    if ((metDir = opendir((const char*)path)) == nullptr)
        path = getenv("METVIEW_USER_DIRECTORY");
    else
        closedir(metDir);

    return path;
}

std::string
ObjectInfo::ObjectFullPath(const MvRequest& request)
{
    std::string path;
    DIR* metDir = nullptr;

    // In certain cases, _PATH contains file name, it shouldn't but .....
    const char* mPath = (const char*)request("_PATH");
    if (mPath) {
        // Try to open a directory
        if ((metDir = opendir(mPath)) != nullptr) {
            closedir(metDir);
            return {mPath};
        }

        // _PATH is not a directory, maybe it contains file name.
        // Remove it and checks directory again at the end.
        path = (const char*)mdirname(mPath);
    }

    // Called from a macro?
    else if (ObjectList::CalledFromMacro(request) == true) {
        // Macro should give us the fully qualified path
        mPath = (const char*)request("_MACRO");
        path = (const char*)mdirname(mPath);
    }
    else if ((const char*)request("_CWD"))
        path = (const char*)request("_CWD");
    else {
        // Obtain the fully qualified path to the object
        mPath = (const char*)request("_NAME");
        path = MakeUserPath(mdirname(mPath));
    }

    // Try to open a directory - if failed, set the path to METVIEW_USER_DIR
    if ((metDir = opendir(path.c_str())) == nullptr)
        path = (const char*)getenv("METVIEW_USER_DIRECTORY");
    else
        closedir(metDir);

    return path;
}

// -- METHOD :  ObjectName
//
// -- PURPOSE:  Determine the object's name
//
// -- INPUT  :  A request which describes the object, the object's type,
//              and the object's id;
//
// -- OUTPUT :  The "best guess" of the object's name
//
// -- NOTES  :  Usually, the object's name is contained in the request,
//              in the "_NAME" field. However, when called from a macro
//              this field contains the line of the macro.
//              Therefore, in the case of a macro, we have to create a
//              name for the object
Cached
ObjectInfo::ObjectName(const MvRequest& request,
                       const char* objType,
                       int id,
                       bool updateId,
                       bool basename)
{
    char tmpstr[100];
    Cached objName;

    // Initialise name of the object
    if (objType == nullptr || !strcmp(objType, ""))
        objName = (const char*)request.getVerb();
    else
        objName = objType;

    // Have we been called from a macro?
    if (ObjectInfo::CalledFromMacro(request) == true) {
        if (id == 0 && updateId)
            id = GenerateId(objName);

        // Macro does not give us a proper name
        sprintf(tmpstr, "%s%d", (const char*)objName, id);
        return tmpstr;
    }
    else {
        // Obtain the name of the object
        const char* mvName = request("_NAME");
        if (mvName == nullptr) {
            if (id == 0 && updateId)
                id = GenerateId(objName);

            sprintf(tmpstr, "%s%d", (const char*)objName, id);
            return tmpstr;
        }
        else
            return (basename ? mbasename(mvName) : mvName);
    }
}

Cached ObjectInfo::GenerateName(MvRequest req)
{
    return SpaceToUnderscore(ObjectName(req, "", 0, true));
}

int ObjectInfo::GenerateId(const char* name)
{
    return ++namesMap_[name];
}

// -- METHOD :  IsAName
//
// -- PURPOSE:  Indicate is a value is a name
//
// -- INPUT  :  Value
//
// -- OUTPUT :  true - the value is a name
//              false - otherwise

bool ObjectInfo::IsAName(const char* value)
{
    for (int i = 0; i < (signed int)strlen(value); i++) {
        if (isalpha(value[i]))
            return true;
    }
    return false;
}

// -- METHOD :  CalledFromMacro
//
// -- PURPOSE:  Indicate if the request is called within a macro
//
// -- INPUT  :  A request
//
// -- OUTPUT :  true/false

bool ObjectInfo::CalledFromMacro(const MvRequest& request)
{
    Cached applicationName, appClass;

    request.getValue(applicationName, "_APPL", 0);
    request.getValue(appClass, "_CLASS");

    if (applicationName == Cached("macro") ||
        appClass == Cached("MACRO"))
        return true;
    else
        return false;
}

// -- METHOD :  IconClass
//
// -- PURPOSE:  Indicate the icon Class
//
// -- INPUT  :  Request
//
// -- OUTPUT :  The class of the icon
//
Cached
ObjectInfo::IconClass(const MvRequest& req)
{
    // Get info from parameter _CLASS (Desktop)
    if ((const char*)req("_CLASS"))
        return (const char*)req("_CLASS");

    // Return a default value
    return req.getVerb();
}

Cached
ObjectInfo::StripExtension(const char* value)
{
    int len = strlen(value);
    Cached name;

    for (int i = 0; i < len; i++) {
        if (*value == '.')
            return name;
        else
            name = name + (*value);
        value++;
    }

    return name;
}

// -- METHOD :  SpaceToUnderscore
//
// -- PURPOSE:  Convert a name with spaces to a name with underscores
//              Converts parenthesis to underscores
//              Converts first character to underscore if is a number
//
// -- INPUT  :  an object name with might contain spaces
//
// -- OUTPUT :  an object name where the spaces have been converted to underscores
//
Cached
ObjectInfo::SpaceToUnderscore(const char* nameWithSpaces)
{
    int len = strlen(nameWithSpaces);
    char* nameWithUnderscores = (char*)malloc(len + 1);

    for (int i = 0; i < len; i++) {
        if ((i == 0) && (isdigit(nameWithSpaces[i])))
            nameWithUnderscores[i] = '_';
        else if ((nameWithSpaces[i] == ' ') ||
                 (nameWithSpaces[i] == ')') ||
                 (nameWithSpaces[i] == '(') ||
                 (nameWithSpaces[i] == ',') ||
                 (nameWithSpaces[i] == ':') ||
                 (nameWithSpaces[i] == '.') ||
                 (nameWithSpaces[i] == '<') ||
                 (nameWithSpaces[i] == '-') ||
                 (nameWithSpaces[i] == '>'))

            nameWithUnderscores[i] = '_';
        else
            nameWithUnderscores[i] = nameWithSpaces[i];
    }
    nameWithUnderscores[len] = '\0';

    return nameWithUnderscores;
}

Cached
ObjectInfo::DefaultVisDefName(int treeNode)
{
    Cached visdefName = Cached("(Contour") + treeNode + Cached(")");
    return visdefName;
}

bool ObjectInfo::CheckVisDefClass(const MvRequest& req1, const MvRequest& req2)
{
    const char* class1 = (const char*)req1("_CLASS");
    const char* class2 = (const char*)req2("_CLASS");
    if (!class1 || !class2)
        return false;

    // Visdefs in Metview 3 and Metview 4 have the same name apart from
    // the first character (M for Metview 4 and P for Metview 3)
    if (strcmp(&class1[1], &class2[1]) == 0)
        return true;  // different classes

    // Check if they are companions
    if (ObjectList::AreCompanions(class1, class2))
        return true;

#if 0  // FAMI1110 Metview 4 does not do ISOTACHS yet
   //If it is PCONT, then check for Data_transformation
   if ( ObjectList::IsVisDefContour(class1) )
   {
      const char* data1 = (const char*)req1("CONTOUR_DATA_TRANSFORMATION");
      const char* data2 = (const char*)req2("CONTOUR_DATA_TRANSFORMATION");
      if ( !data1 && !data2 ) //no information about data_transformation
         return true;

      if ( !data1 || !data2 )    //different data transformation
         return false;

      if ( strcmp(data1,data2) )
         return false; //different data transformation
   }
#endif
    return false;
}

Cached
ObjectInfo::Convert(MvRequest& req)
{
    int first = 0;
    Cached returnName(""), oneReqName;

    while (req) {
        // Select converter
        std::string iconClass = (const char*)ObjectInfo::IconClass(req);
        MacroConverter& converter = MacroConverter::Find(iconClass.c_str());

        // Convert an ascii file that is not a LLMATRIX
        oneReqName = "";
        if (iconClass != std::string("LLMATRIX")) {
            std::string fileName = this->FileName(req);
            if (!IsBinaryOrMissingFile(fileName.c_str())) {
                MvRequest fileReq;
                fileReq.read(fileName.c_str(), true);

                // Just in case further expansion is needed
                if (!(const char*)fileReq("_PATH") && (const char*)req("_PATH"))
                    fileReq("_PATH") = req("_PATH");
                if (!(const char*)fileReq("_NAME") && (const char*)req("_NAME"))
                    fileReq("_NAME") = req("_NAME");

                oneReqName = converter.Convert(fileReq, this);
            }
        }

        if (oneReqName == Cached("")) {
            oneReqName = converter.Convert(req, this);
            if (oneReqName == Cached("")) {
                req.advance();
                continue;
            }
        }

        if (first == 1)
            returnName = Cached("[") + returnName;

        if (first > 0)
            returnName = returnName + Cached(", ");

        returnName = returnName + oneReqName;
        first++;

        req.advance();
    }

    if (first > 1)
        returnName = returnName + Cached("] ");

    return returnName;
}

// Build a filename, including the path, from a request
// First guess: _PATH & _NAME
// Second guess: METVIEW_USER_DIRECTORY & _NAME
// If filename does not exist, returns an empty string
std::string
ObjectInfo::FileName(const MvRequest& req)
{
    // Build first filename guess: _PATH & _NAME
    std::string name = (const char*)ObjectInfo::ObjectName(req, "", 0, false, false);
    std::string path = ObjectInfo::ObjectFullPath(req);
    std::string fileName = path + ((path[path.size() - 1] == '/') ? name : "/" + name);
    if (FileCanBeOpened(fileName.c_str(), "r"))
        return fileName;

    // Build second filename guess: METVIEW_USER_DIRECTORY & _NAME
    path = (const char*)getenv("METVIEW_USER_DIRECTORY");
    fileName = path + ((path[path.size() - 1] == '/') ? name : "/" + name);
    if (FileCanBeOpened(fileName.c_str(), "r"))
        return fileName;

    fileName.clear();

    return fileName;
}

void ObjectInfo::FormatLine(const Cached& param, const Cached& val, const Cached& comma, int width)
{
    std::ostringstream str;
    str.setf(std::ios::left);
    str << "   ";
    str.width(width);
    str << param;
    str << ": " << val << comma << std::ends;

    PutNewLine(str.str().c_str());
}

Cached ObjectInfo::FormatValue(const char* val, const char* param, const Cached& macroName)
{
    std::string sval;
    bool addQuotes = false;

    // Multiple lines
    if (strstr(val, "\n") != nullptr) {
        char* pch = nullptr;
        int len = strlen(val);
        if (len > 0) {
            char* buff = (char*)calloc(len + 1, sizeof(char));
            sprintf(buff, "%s", val);
            pch = strtok(buff, "\n");
            sval = sval + "\"" + pch + " \"";

            while (pch != nullptr) {
                pch = strtok(nullptr, "\n");
                if (pch != nullptr) {
                    sval += " &\n";
                    sval = sval + "            \"" + pch + " \"";
                }
            }

            free(buff);
        }

        return {sval.c_str()};
    }

    // Single line
    // Put all names under '' symbols
    if ((ObjectInfo::IsAName(val)) || (strcmp(val, "") == 0) ||
        ((strcmp(param, "TIME") == 0) && (macroName == Cached("retrieve"))))
        addQuotes = true;

    if (addQuotes)
        sval = "\"";

    while (*val) {
        if (*val == '\\')
            sval += '\\';

        // If dot is first character and it's a number, add a 0 at the beginning.
        if (*val == '.' && sval.length() == 0 && is_number(val))
            sval += "0";

        sval += *val;
        val++;
    }

    if (addQuotes)
        sval += "\"";

    return {sval.c_str()};
}

int ObjectInfo::MaximumParameterNameLength(MvRequest& inRequest)
{
    int size = 0;
    for (int j = 0; j < inRequest.countParameters(); j++) {
        const char* param = inRequest.getParameter(j);
        if (size < (int)strlen(param))
            size = strlen(param);
    }

    // Give one extra space
    return size + 1;
}
