/***************************** 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 <new>
#include <cstdio>
#include <cctype>
#include "script.h"
#include "MvDebugPrintControl.h"
#include "MvPath.hpp"

struct data
{
    boolean serve;
    boolean module;
    char* name;
    char* script;
    char* args;
};

static option opts[] = {

    {
        "serve",
        0,
        "-serve",
        "0",
        t_boolean,
        sizeof(boolean),
        OFFSET(data, serve),
    },

    {
        "module",
        0,
        "-module",
        "0",
        t_boolean,
        sizeof(boolean),
        OFFSET(data, module),
    },

    {
        "name",
        0,
        "-name",
        "0",
        t_str,
        sizeof(char*),
        OFFSET(data, name),
    },

    {
        "script",
        0,
        "-script",
        0,
        t_str,
        sizeof(char*),
        OFFSET(data, script),
    },

    {
        "args",
        0,
        "-args",
        0,
        t_str,
        sizeof(char*),
        OFFSET(data, args),
    },
};

static data setup;
svc* service = 0;

static void out_of_memory(void)
{
    marslog(LOG_EXIT, "Out of memory");
}

// This class redirect cout to Script::PutMessage

class sbuf : public std::streambuf
{  // 76830942

    char line[1024];
    int n{0};


    // Oops, SUN says streambuf::seekoff(long,std::ios::seek_dir,int) undefined ????
    //#ifdef sun
    //	virtual long seekoff(long,std::ios::seek_dir,int) { return 0;};
    //#endif

    virtual int sync(void)
    {
        if (n) {
            line[n] = 0;
            marslog(LOG_INFO, "%s", line);
            n = 0;
        }
        return 0;
    }

    virtual int overflow(int c = EOF)
    {
        if (n >= (signed)sizeof(line) - 2)
            sync();
        if ((line[n++] = c) == '\n') {
            n--;
            sync();
        }
        return 0;
    }


public:
    sbuf() = default;
};


static Value* args_from_file(char* file, int& n)
{
    n = 0;
    if (!file)
        return 0;

    FILE* f = fopen(file, "r");
    int max = 20;

    if (f == 0) {
        marslog(LOG_EROR | LOG_PERR, "Cannot open %s", file);
        return 0;
    }

    // Waring unlink file
    unlink(file);

    auto* v = new Value[max];
    char line[1024];

    while (fgets(line, sizeof(line), f)) {
        if (*line)
            line[strlen(line) - 1] = 0;
        if (isdate(line))
            v[n] = Value(Date(line));
        else if (is_number(line))
            v[n] = Value(atof(line));
        else
            v[n] = Value(line);

        n++;

        if (n == max) {
            max += n / 2;
            auto* w = new Value[max];
            for (int i = 0; i < n; i++)
                w[i] = v[i];

            delete[] v;
            v = w;
        }
    }

    fclose(f);
    return v;
}

static void serve_macro(svcid* id, request* r, void* data)
{
    int forked = fork_service(id);
    if (forked > 0)
        return;

    if (forked == -1 && mars.nofork == false) {
        send_later(id);
        return;
    }

    // send pid to caller process
    MvRequest reqPid("REGISTER_MACRO_PID");
    reqPid("PID") = getpid();
    send_progress(id, nullptr, reqPid);

    auto* param = (request*)data;
    const char* path = get_value(r, "PATH", 0);
    const char* name = get_value(r, "_NAME", 0);
    const char* action = get_value(r, "_ACTION", 0);
    const char* trace = get_value(r, "_TRACE", 0);
    const char* wait = get_value(r, "_WAITMODE", 0);
    const char* lines = get_value(r, "_SENDLINES", 0);
    const char* pause = get_value(r, "_PAUSE", 0);
    const char* print = get_value(r, "_RETURN", 0);
    const char* extendMsg = get_value(r, "_EXTENDMESSAGE", 0);

    request* output = get_subrequest(r, "_DEVICE_DRIVER", 0);  //-- uPlot/PlotMod/MetZoom
    request* device = get_subrequest(r, "_DEVICE", 0);         //-- VisMod

    // Arguments to be passed to the Macro (not allowed in param mode!)
    int argNum = count_values(r, "_ARGUMENTS");
    Value* argVal = 0;
    if (!param && argNum > 0 && argNum < 32) {
        argVal = new Value[argNum];
        for (int i = 0; i < argNum; i++) {
            const char* t = get_value(r, "_ARGUMENTS", i);
            argVal[i] = Value(t);
        }
    }
    else {
        argNum = 0;
    }

    request* reply = 0;
    Value w;

    marslog(LOG_INFO, "Starting %s %s %s", path, name, action);

    Script::SvcId = id;
    if (extendMsg)
        mars.outproc = Script::PutExtendedMessage;
    else
        mars.outproc = Script::PutMessage;

    // Important: first, save directory where the Macro application is
    // currently running, usually where the Macro executable is located.
    // Second, change directory to where the Macro program is located.
    // Third, before finishing the execution of this program, restore
    // the current directory.
    char save[1024];
    getcwd(save, sizeof(save) - 1);
    chdir(mdirname(path));             // to ensure we always get the dir that the
                                       // macro is in, even if run from the editor
    setenv("PWD", mdirname(path), 1);  // change env variable too

    start_timer();

    auto* s = new Script(mbasename(name));

    if (output)  //-- uPlot/PlotMod/MetZoom
    {
        Script::Output = Value(output);
        free_all_requests(output);
    }
    if (device)  //-- VisMod
    {
        Script::Device = Value(device);
        free_all_requests(device);
    }


    // store the current macro path - needs to be done *before* Compile(path)
    std::string fullPath = MakeAbsolutePath(path);
    Script::MacroPath(fullPath.c_str());
    Script::MacroMainPath(fullPath.c_str());

    if (s->Compile(path) == 0) {
        Value v(param);

        s->SetRunMode(action);
        s->AddBaseLanguageGlobalVar();
        Context::Trace(trace ? atoi(trace) : 0);
        Context::WaitMode(wait ? atoi(wait) : 0);
        Context::SendLines(lines ? atoi(lines) : 0);
        Context::PauseBetweenLines(pause ? atoi(pause) : 0);

        int handler = s->HasHandler(action, param ? 1 : 0, &v);

        if (strcmp(action, "syntax") == 0)  // check only
            marslog(LOG_INFO, "# Syntax is OK");

        else if ((strcmp(action, "edit") == 0) && !handler)
            marslog(LOG_EROR, "Macro has no 'edit' handler");

        else if ((strcmp(action, "check") == 0) && !handler)
            ;

        else {
            if (param) {
                // Support for new MetviewUI
                print_all_requests(param);
            }

            if (!param && argNum > 0) {
                w = s->Run(action, argNum, argVal);
            }
            else {
                w = s->Run(action, param ? 1 : 0, &v);
            }

            w.Sync();

            if (print) {
                std::cout << "# Macro returns: ";
                w.Print();
                std::cout << '\n';
            }

            w.GetValue(reply);
        }
    }

    // Flush previous plot commands
    if (Script::PlotReq) {
        // Get Output Driver and plotting requests
        request* reqdrive;
        Script::Output.GetValue(reqdrive);
        MvRequest req = Script::PlotReq;

        // Special case when setouput and metzoom commands are defined:
        // ignores setouput and calls uPlotManager instead of uPlotBatch
        if (reqdrive && req &&
            (const char*)req("_MACRO_PLOT_COMMAND") &&
            strcmp((const char*)req("_MACRO_PLOT_COMMAND"), "metzoom") == 0)
            PlotterFunction::SetPlotter("uPlotManager");
        else {
            // Concatenate Output Driver and Plot request
            MvRequest req_aux(reqdrive);
            req.clean();
            req = req_aux;
            req = req + Script::PlotReq;
        }

        // Important to indicate which visualiser will be called.
        // The visualiser default is uPlot
        // Module uPlotManager will be called
        if (action && strcmp(action, "metzoom") == 0)
            req("_ACTION") = "metzoom";

        // Flush
        Value v(PlotterFunction::Plotter(), req);
        v.Sync();  // Force sync

        // empty request
        Script::PlotReq.clean();
    }

    set_svc_err(id, s->GetError());

    delete s;
    if (argVal)
        delete[] argVal;

    mars.outproc = 0;
    Script::SvcId = 0;

    char info[1024];
    stop_timer(info);

    marslog(LOG_INFO, "Compile+run = %s", info);
    marslog(LOG_INFO, "End of %s %s %s", path, name, action);

    // Add parameter _PATH
    if (!(const char*)get_value(reply, "_PATH", 0)) {
        std::string fullPath = MakeAbsolutePath((const char*)get_value(reply, "PATH", 0), mdirname(Script::MacroMainPath()));
        set_value(reply, "_PATH", fullPath.c_str());
    }

    // Be aware that send_reply deletes the id but not the service. See METV-3335
    svc* currentSvc = id->s;
    send_reply(id, reply);
    // Don't free reply, will be done when the Value is destroyed

    // Restore current directory
    chdir(save);

    if (forked != -1) {
        destroy_service(currentSvc);
        marsexit(0);
    }
}

static void serve_param(svcid* id, request* r, void*)
{
    request* macro = get_subrequest(r, "MACRO", 0);
    set_value(macro, "_ACTION", "%s", get_value(r, "_ACTION", 0));

    request* output = get_subrequest(r, "_DEVICE_DRIVER", 0);
    if (output) {
        set_subrequest(macro, "_DEVICE_DRIVER", output);  //-- uPlot/PlotMod/MetZoom
        free_all_requests(output);
    }
    else {
        request* device = get_subrequest(r, "_DEVICE", 0);
        if (device) {
            set_subrequest(macro, "_DEVICE", device);  //-- VisMod
            free_all_requests(device);
        }
    }

    serve_macro(id, macro, (void*)r->next);
    free_all_requests(macro);
}

static void serve_compute(svcid* id, request* r, void*)
{
    int forked = fork_service(id);
    if (forked > 0)
        return;

    if (forked == -1 && mars.nofork == false) {
        send_later(id);
        return;
    }


    request* reply = 0;
    Value w;

    const char* name = get_value(r, "_NAME", 0);

    if ((reply = pool_fetch(id->s, name, r->name))) {
        send_reply(id, reply);
        free_all_requests(reply);
        if (forked != -1) {
            destroy_service(id->s);
            marsexit(0);
        }
    }

    Script::SvcId = id;
    mars.outproc = Script::PutMessage;

    Compute s(name, r);

    if (s.GetError() == 0) {
        w = s.Run();
        w.GetValue(reply);
    }

    set_svc_err(id, s.GetError());


    if (s.GetError() == 0) {
        set_value(reply, "_NAME", "%s", name);
        set_value(reply, "_CLASS", "%s", r->name);
        pool_store(id->s, name, r->name, reply);
        pool_link_objects(id->s, r);
    }

    send_reply(id, reply);

    mars.outproc = 0;
    Script::SvcId = 0;

    if (forked != -1) {
        destroy_service(id->s);
        marsexit(0);
    }
}

static void serve_help(svcid* id, request* r, void*)
{
    static int n = 0;
    print_all_requests(r);
    r = empty_request("HELP");
    set_value(r, "TEXT", "%s%d",
              " Some {text} form macro...\n"
              "%text=moretext@xxx",
              n++);
    send_reply(id, r);
    free_all_requests(r);
}

static void serve_formula(svcid* id, request* r, void*)
{
    int forked = fork_service(id);
    if (forked > 0)
        return;

    if (forked == -1 && mars.nofork == false) {
        send_later(id);
        return;
    }

    request* reply = 0;
    Value w;

    const char* name = get_value(r, "_NAME", 0);

    if ((reply = pool_fetch(id->s, name, r->name))) {
        send_reply(id, reply);
        free_all_requests(reply);
        if (forked != -1) {
            destroy_service(id->s);
            marsexit(0);
        }
    }

    Script::SvcId = id;
    mars.outproc = Script::PutMessage;

    Formula s(name, r);

    if (s.GetError() == 0) {
        Value v(r);
        w = s.Run(0, 1, &v);
        w.GetValue(reply);
    }

    set_svc_err(id, s.GetError());

    if (s.GetError() == 0) {
        set_value(reply, "_NAME", "%s", name);
        set_value(reply, "_CLASS", "%s", r->name);
        pool_store(id->s, name, r->name, reply);
        pool_link_objects(id->s, r);
    }

    send_reply(id, reply);

    if (forked != -1) {
        destroy_service(id->s);
        marsexit(0);
    }

    mars.outproc = 0;
    Script::SvcId = 0;
}

int main(int argc, char** argv)
{
    marsinit(&argc, argv, &setup, NUMBER(opts), opts);

    mvSetMarslogLevel();  //-- if "quiet log"

    std::set_new_handler(out_of_memory);

    CNumber::SetPrecision(CNumber::GetDefaultPrecision());  //-- print precision in digits

    if (setup.serve)  // interactive mode
    {
        // Send prints to caller

        sbuf buf;
        std::ostream out(&buf);
        std::cout.rdbuf(out.rdbuf());

        service = create_service("macro");  // progname());
        add_service_callback(service, "HELP", serve_help, 0);
        add_service_callback(service, "MACRO", serve_macro, 0);
        add_service_callback(service, "WMACRO", serve_macro, 0);
        add_service_callback(service, "MACROPARAM", serve_param, 0);
        add_service_callback(service, "COMPUTE", serve_compute, 0);
        add_service_callback(service, "SAMPLE_FORMULA_DOD", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_DON", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_NOD", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_NON", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FD", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FN", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FDD", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FDN", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FND", serve_formula, 0);
        add_service_callback(service, "SAMPLE_FORMULA_FNN", serve_formula, 0);

        // One macro at a time, for more we use fork()
        set_maximum(service, 1);

        service_run(service);
    }
    else if (!setup.script && isatty(0)) {
        marslog(LOG_INFO, "Terminal mode");
        Script* s = new Terminal("Terminal mode");
        if (s->Compile() == 0)
            s->Run();
        delete s;
    }
    else  // batch mode
    {
        // Script *s = setup.module ? (Script*)new Module(setup.name)
        //			     : (Script*)new Batch(setup.name);

        //-- Here we rely on $MACRO_STARTUP, set up by Metview startup script:
        //--   argv: 0:'macro' 1:'-script' 2:filename 3:'-args' 4:arglist...
        const char* mname = mbasename(argv[2]);

        Script* s = setup.module ? (Script*)new Module(mname)
                                 : (Script*)new Batch(mname);

        s->AddBaseLanguageGlobalVar();
        int n = 0;
        Value* v = args_from_file(setup.args, n);

        if (s->Compile(setup.script) == 0)
            s->Run(s->GetRunMode(), n, v);

        // Flush previous plot commands
        if (Script::PlotReq) {
            // Concatenate Output Driver and Plot request
            request* reqdrive;
            Script::Output.GetValue(reqdrive);
            MvRequest req(reqdrive);
            req = req + Script::PlotReq;

            // Flush
            Value v(PlotterFunction::Plotter(), req);
            v.Sync();  // Force sync

            // empty request
            Script::PlotReq.clean();
        }

        delete s;
        if (v)
            delete[] v;
    }
    return 0;
}
