/***************************** 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 <fstream>

#include "fstream_mars_fix.h"

#include "macro.h"
#include "cbufr.h"
#include "cgptset.h"
#include "cnetcdf.h"
#include "ctable.h"
#include "cpngjpeg.h"

#ifdef METVIEW_ODB
#include "codb.h"
#endif

#include "MvFlextra.cc"

#include "LLMatrixToGRIB.h"
#include "MvScanFileType.h"

//=============================================================================


class CFile : public Content
{
    using closeproc = int (*)(FILE*);

    char* name;
    int rmode{1};  // read mode
    FILE* f{nullptr};
    closeproc closef{nullptr};


    virtual void ToFile(CFile*& x) { x = this; }
    virtual void Print();

public:
    CFile(const char* n) :
        Content(tfile)
    {
        name = strcache(n);
    }
    ~CFile();

    //

    FILE* File(char* m);
};

CFile::~CFile()
{
    if (f != nullptr && closef(f) != 0)
        Error("-Error while closing file %s", name);
    strfree(name);
}

void CFile::Print()
{
    std::cout << "<file " << name;
    if (f) {
        const char* p = rmode ? "(read)" : "(write)";
        std::cout << ' ' << p;
    }
    std::cout << '>';
}

FILE* CFile::File(char* m)
{
    int r = *m == 'r';

    if (f == nullptr) {
        if (*name == '|') {
            f = popen(name + 1, m);
            closef = pclose;
        }
        else {
            f = fopen(name, m);
            closef = fclose;
        }
        if (f == nullptr)
            Error("Cannot open file %s", name);
        rmode = r;
        return f;
    }
    else if (r != rmode) {
        Error("File %s in open in '%s' mode", name, rmode ? "read" : "write");
        return nullptr;
    }
    return f;
}

//=============================================================================

class FileFunction : public Function
{
public:
    FileFunction(const char* n) :
        Function(n, 1, tstring)
    {
        info = "Returns a file handler for the specified filename. args: (string)";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value FileFunction::Execute(int, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);
    return Value(new CFile(p));
}

//=============================================================================
class Write1Function : public Function
{
public:
    Write1Function(const char* n) :
        Function(n)
    {
        info = "Writes/appends the given data to file. args: (string, any)";
    }
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

int Write1Function::ValidArguments(int arity, Value* arg)
{
    if (arity < 2)
        return false;
    if (arg[0].GetType() != tstring)
        return false;
    return true;
}

Value Write1Function::Execute(int arity, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);

    // Get mode from function name
    char mode[2];
    mode[0] = Name()[0];
    mode[1] = 0;

    FILE* f = *p == '|' ? popen(p + 1, mode) : fopen(p, mode);
    if (!f)
        return Error("-Cannot open file %s", p);

    int e = 0;

    for (int i = 1; i < arity; i++) {
        arg[i].Sync();  // This will force sync

        // Flextra related part
        if (arg[i].GetType() == trequest) {
            request* r;
            arg[i].GetValue(r);

            if (r && strcmp(r->name, "FLEXTRA_FILE") == 0) {
                const char* path = get_value(r, "PATH", 0);
                if (!path) {
                    e = 1;
                    break;
                }

                std::string s(path);
                MvFlextra flx(s);
                flx.write(f);
            }
            else if ((e = arg[i].Write(f)))
                break;
        }

        // Any other type
        else if ((e = arg[i].Write(f)))
            break;
    }

    int c = *p == '|' ? pclose(f) : fclose(f);
    if (c)
        return Error("-Error while closing %s", p);

    if (e)
        return Error("Write failed");

    return Value(0.0);
}

//=============================================================================
class Write2Function : public Function
{
public:
    Write2Function(const char* n) :
        Function(n)
    {
        info = "Writes/appends the given data to file. args: (filehandler, any)";
    }
    virtual Value Execute(int arity, Value* arg);
    virtual int ValidArguments(int arity, Value* arg);
};

int Write2Function::ValidArguments(int arity, Value* arg)
{
    if (arity < 2)
        return false;
    if (arg[0].GetType() != tfile)
        return false;
    return true;
}

Value Write2Function::Execute(int arity, Value* arg)
{
    CFile* file;
    arg[0].GetValue(file);

    // Get mode from function name
    char mode[2];
    mode[0] = Name()[0];
    mode[1] = 0;

    FILE* f = file->File(mode);
    if (!f)
        return Error("Invalid file");

    int e = 0;

    for (int i = 1; i < arity; i++) {
        arg[i].Sync();  // This will force sync

        // Flextra related part
        if (arg[i].GetType() == trequest) {
            request* r;
            arg[i].GetValue(r);

            if (r && strcmp(r->name, "FLEXTRA_FILE") == 0) {
                const char* path = get_value(r, "PATH", 0);
                if (!path) {
                    e = 1;
                    break;
                }

                std::string s(path);
                MvFlextra flx(s);
                flx.write(f);
            }
            else if ((e = arg[i].Write(f)))
                break;
        }

        if ((e = arg[i].Write(f)))
            break;
    }

    if (e)
        return Error("Write failed");

    return Value(0.0);
}

//=============================================================================

class TmpNameFunction : public Function
{
public:
    TmpNameFunction(const char* n) :
        Function(n, 0)
    {
        info = "Returns a new, unique temporary filename. args: none";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value TmpNameFunction::Execute(int, Value*)
{
    return marstmp();
}

//=============================================================================

class ExistFunction : public Function
{
public:
    ExistFunction(const char* n) :
        Function(n, 1, tstring)
    {
        info = "Determines whether the given file exists. Returns 1 or 0. args: (string)";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value ExistFunction::Execute(int, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);
    return access(p, F_OK) == 0;
}

//=============================================================================

class ReadFunction : public Function
{
public:
    ReadFunction(const char* n) :
        Function(n, 1, tstring)
    {
        info = "Reads the given file and returns a variable of the appropriate type. args: (string)";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value ReadFunction::Execute(int, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);

    if (*p == '|')
        return Error("Pipe not yet supported in read()");

    std::string kind = ScanFileType(p);

    Content* c = nullptr;
    // Try to guess

    if (kind == "GRIB")
        // Here, check for images
        c = new CGrib(p);

    else if (kind == "BUFR")
        c = new CBufr(p);

    else if (kind == "GEOPOINTS")
        c = new CGeopts(p);

    else if (kind == "GEOPOINTSET")
        c = new CGeoptSet(p);

    else if (kind == "NETCDF" || kind == "SCM_INPUT_DATA" || kind == "SCM_OUTPUT_DATA" || kind.substr(0, 7) == "NETCDF_")
        c = new CNetCDF(p);

    else if (kind == "TABLE")
        c = new CTable(p);

    else if (kind == "VECTOR")
        c = new CVector(p);

    else if (kind == "LLMATRIX") {
        char* tmp = marstmp();
        if (LLMatrixToGRIB(p, tmp) == 0)
            c = new CGrib(tmp);
        else
            return Error("LLMatrix-to-GRIB failed");
    }

    else if (kind == "PNG" || kind == "JPEG")
        c = new CPngJpeg(p, kind);

    else if (kind == "NOTE" || kind == "MACRO") {
        std::string line;
        std::ifstream f(p);
        if (!f) {
            marslog(LOG_EROR | LOG_PERR, "Cannot open %s", p);
            return Error("Open failed");
        }

        int cnt = 0;
        while (getline(f, line))
            cnt++;

        f.clear();
        f.seekg(0, std::ios::beg);

        auto* l = new CList(cnt);
        for (int i = 0; i < cnt; i++) {
            getline(f, line);

            if (isdate(line.c_str()))
                (*l)[i] = Value(Date(line.c_str()));
            else if (is_number(line.c_str()))
                (*l)[i] = Value(atof(line.c_str()));
            else
                (*l)[i] = Value(line.c_str());
        }

        c = l;

        f.close();
    }

#ifdef METVIEW_ODB
    else if (kind == "ODB_DB")
        c = new COdb(p);
#endif

    else if (kind == "FLEXTRA_FILE") {
        request* r = empty_request("FLEXTRA_FILE");

        if (p) {
            // TODO: figure out if handling relative paths like this still makes any sense
            std::string pStr(p);
            if (pStr.length() > 0 && pStr[0] != '/') {
                const char* pwd = getenv("PWD");
                if (pwd) {
                    pStr = std::string(pwd) + "/" + pStr;
                    set_value(r, "PATH", pStr.c_str());
                }
                else {
                    set_value(r, "PATH", p);
                }
            }
            else {
                set_value(r, "PATH", p);
            }
        }
        c = new CRequest(r);
    }

    return c ? Value(c) : Error("Cannot read %s. Type %s is not supported.", p, kind.c_str());
}

//=============================================================================

class ReadRequestFunction : public Function
{
public:
    ReadRequestFunction(const char* n) :
        Function(n)  //, 2, tstring, tnumber)
    {
        info = "Reads the given request file and returns a list of dictionaries. args: (string)";
    }
    virtual int ValidArguments(int arity, Value* arg);
    virtual Value Execute(int arity, Value* arg);
};

int ReadRequestFunction::ValidArguments(int arity, Value* arg)
{
    if (arity < 1 || arity > 2)
        return false;
    if (arg[0].GetType() != tstring)
        return false;
    if (arity == 2 && arg[1].GetType() != tnumber)
        return false;
    return true;
}

Value ReadRequestFunction::Execute(int arity, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);

    // Read request from disk
    MvRequest req1;
    req1.read(p);

    // Convert request to upper case
    MvRequest req = req1.convertLetterCase(true);

    // Expand request
    if (arity == 2) {
        // get the request object
        long expand_flag;
        arg[1].GetValue(expand_flag);
        MvRequest req_obj = req.findRequestObject();
        if (!req_obj)
            return Value();

        // get the definition&rules filenames
        std::string sdef = (const char*)mbasename(get_value(req_obj, "definition_file", 0));
        std::string srules = (const char*)mbasename(get_value(req_obj, "rules_file", 0));
        if (sdef.empty() || srules.empty())
            return Value();

        // expand request
        req = req.ExpandRequest(sdef.c_str(), srules.c_str(), expand_flag);
        if (!req)
            return Value();
    }

    request* c = req;
    Value val;
    val.SetContentRequest(c);

    return val;
}

//=============================================================================

class FileTypeFunction : public Function
{
public:
    FileTypeFunction(const char* n) :
        Function(n, 1, tstring)
    {
        info = "Returns the type of the given file. args: (string)";
    }
    virtual Value Execute(int arity, Value* arg);
};

Value FileTypeFunction::Execute(int, Value* arg)
{
    const char* p;
    arg[0].GetValue(p);

    if (*p == '|')
        return Error("Pipe is not supported in %()", Name());

    return Value(ScanFileType(p).c_str());
}


//=============================================================================

static void install(Context* c)
{
    c->AddFunction(new Write1Function("write"));
    c->AddFunction(new Write2Function("write"));
    c->AddFunction(new Write1Function("append"));
    c->AddFunction(new Write2Function("append"));
    c->AddFunction(new FileFunction("file"));
    c->AddFunction(new ExistFunction("exist"));
    c->AddFunction(new ReadFunction("read"));
    c->AddFunction(new ReadRequestFunction("read_request"));
    c->AddFunction(new TmpNameFunction("tmpname"));
    c->AddFunction(new FileTypeFunction("filetype"));
}

static Linkage Link(install);
