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

/*

B.Raoult
ECMWF Oct-93

*/

#include <signal.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <setjmp.h>
#include <string.h>
#include <strings.h>

#include "Event.h"
#include "MvDebugPrintControl.h"


static option opts[] = {
    {
        (char*)"startup",
        nullptr,
        (char*)"-startup",
        (char*)"xserv",
        t_str,
        sizeof(char*),
        OFFSET(Options, startup),
    },
    {
        (char*)"exit",
        nullptr,
        (char*)"-exit",
        nullptr,
        t_str,
        sizeof(char*),
        OFFSET(Options, exit),
    },
    {(char*)"port", (char*)"EVENT_PORT", (char*)"-port", (char*)"0",
     t_int, sizeof(int), OFFSET(Options, port)},
    {(char*)"python_pid", (char*)"MV_PY_PID", (char*)"-python_pid", (char*)"0",
     t_int, sizeof(int), OFFSET(Options, python_pid)},
    {(char*)"environment", nullptr, (char*)"-environment", nullptr,
     t_str, sizeof(char*), OFFSET(Options, env)},
};

long Event::gUniqID = 0;
fd_set Event::gFiles;
Options Event::gOpt = {
    0,
};


// When running Metview as a backend to a Python script, termination of the Python process
// should send a signal to Event in order to kill it. We catch that signal and exit.
static void catch_sigusr1(int)
{
    exit(0);
}


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

void Event::NewService(int soc)
{
    struct sockaddr_in from;
    int snew;
    int fromlen;

    fromlen = sizeof(from);
#if defined(Sgi) || defined(Hpux) || defined(__alpha)
    if ((snew = accept(soc, (sockaddr*)&from, &fromlen)) < 0)
#else
    if ((snew = accept(soc, (sockaddr*)&from, (socklen_t*)&fromlen)) < 0)
#endif
    {
        /* Interrupted system call : got on SIGCHLD signals */
        if (errno != EINTR)
            marslog(LOG_WARN | LOG_PERR, "accept");
        return;
    }

    marslog(LOG_INFO, "Got connection");
    if (from.sin_family == AF_INET) {
        char* net = inet_ntoa(from.sin_addr);
        struct hostent* remote;

        remote = gethostbyaddr((char*)&from.sin_addr,
                               sizeof(from.sin_addr),
                               from.sin_family);

        if (remote)
            marslog(LOG_INFO, "incoming host is %s (%s)",
                    remote->h_name, net);
        else
            marslog(LOG_INFO, "incoming address is %s", net);
    }
    else {
        marslog(LOG_INFO, "connection is not from internet");
        close(snew);
        return;
    }

    if (fcntl(snew, F_SETFD, FD_CLOEXEC) < 0)
        marslog(LOG_EROR | LOG_PERR, "fcntl");
    auto* s = new Service(snew);
    s->ProcessIncoming();
}

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

void Event::CheckQueues(void)
{
    WaitingQueue::Process();
    OutputQueue::Process();
}

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

void Event::CheckTimeout(void)
{
    int alive = Service::CheckTimeout();

    // If exit module is given, return
    if (gOpt.exit)
        return;

    // Else, no 'alive' modules

    if (alive == 0 &&
        WaitingQueue::Empty() &&
        ProcessingQueue::Empty() &&
        OutputQueue::Empty()) {
        mars.warning = true;

        marslog(LOG_WARN, "+--------------------------------------------+");
        marslog(LOG_WARN, "|   No more modules - Event will quit...     |");
        marslog(LOG_WARN, "| Quitting kills the communication port and  |");
        marslog(LOG_WARN, "| this will stop all the remaining modules.  |");
        marslog(LOG_WARN, "| Ignore FATAL messages caused by Dead Port! |");
        marslog(LOG_WARN, "+--------------------------------------------+");

        marsexit(0);
    }
}

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

void Event::Startup()
{
    char buf[1024];
    char host[1024];
    char* lochost;

    lochost = getenv("METVIEW_LOCALHOST");
    if (lochost && strcmp(lochost, "0"))  // env var defined and not "0"
    {
        strcpy(buf, "localhost");  //-- for network safe demo on portable pc
        marslog(LOG_INFO, "NOTE: $EVENT_HOST is now \'localhost\'!");
    }
    else {
        gethostname(host, sizeof(host));
        sprintf(buf, "EVENT_HOST=%s", host);
    }
    putenv(strdup(buf));

    sprintf(buf, "EVENT_PORT=%d", gOpt.port);
    putenv(strdup(buf));

    if (gOpt.startup) {
        // There's a possible problem if we start Metview in batch mode
        // (-b <path/to/macro>) if the path to the macro contains spaces.
        // It is passed inside $MACRO_STARTUP, and although it contains
        // quotes around the path, these are lost when system() is
        // called (in this case, the invoked shell expands the environment
        // variable, and it seems to be this that loses the quotes).
        // The next few lines are perhaps a bit of a hack, but we get round
        // this specific case by expanding $MACRO_STARTUP before passing it
        // to the system() call. If invoked with the examiner option (-e)
        // then we get $METVIEW_STARTUP, which we have to treat in the same way.

        char* startup;
        char startupBuffer[1024];

        if (!strcmp(gOpt.startup, "$MACRO_STARTUP"))
            startup = getenv("MACRO_STARTUP");

        else if (!strncmp(gOpt.startup, "$METVIEW_STARTUP", 16))  // does it start with $METVIEW_STARTUP?
        {
            startup = getenv("METVIEW_STARTUP");
            sprintf(startupBuffer, "%s%s", getenv("METVIEW_STARTUP"), &gOpt.startup[16]);  // replace with value
            startup = startupBuffer;
        }

        else
            startup = gOpt.startup;


        sprintf(buf, "%s &", startup);

        if (system(buf)) {
            marslog(LOG_EROR | LOG_PERR, "Cannot start %s", startup);
            marslog(LOG_EXIT, "Bye...");
        }
    }

    // see comments above function catch_sigusr1 for details
    signal(SIGUSR1, catch_sigusr1);
}

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

void Event::CleanUp(int, void*)
{
    if (gOpt.env)
        unlink(gOpt.env);
}

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


void Event::WriteEnvVarIfNotNull(FILE* f, const char* varName)
{
    char* val = getenv(varName);

    if (val != nullptr) {
        fprintf(f, "%s=%s\n", varName, val);
    }
}


void Event::Loop()
{
    fd_set fds;
    static struct timeval smalltime = {
        0,
        100,
    };
    static struct timeval bigtime = {
        60,
        0,
    };
    int soc;

    /* Start real server */

    if ((soc = tcp_server(gOpt.port)) < 0)
        marslog(LOG_EXIT, "Exiting server");


    gOpt.port = port_of(addr_of(soc));
    marslog(LOG_INFO, "Starting server: port is %d", gOpt.port);


    if (fcntl(soc, F_SETFD, FD_CLOEXEC) < 0)
        marslog(LOG_EROR | LOG_PERR, "fcntl");

    FD_ZERO(&gFiles);
    FD_SET(soc, &gFiles);

    Startup();

    if (gOpt.env) {
        // write all information that Python might need to run macro functions in this Metview session
        FILE* f = fopen(gOpt.env, "w");
        if (f == nullptr)
            marslog(LOG_EXIT | LOG_PERR, "Cannot open %s", gOpt.env);
        fprintf(f, "[Info]\n");
        fprintf(f, "EVENT_PID=%d\n", getpid());
        WriteEnvVarIfNotNull(f, "METVIEW_LIB");
        //fprintf(f, "MV_SCRIPT_PID=%s\n",         getenv("MV_SCRIPT_PID"));
        fprintf(f, "[Environment]\n");
        WriteEnvVarIfNotNull(f, "METVIEW_BIN");
        WriteEnvVarIfNotNull(f, "METVIEW_USER_DIRECTORY");
        WriteEnvVarIfNotNull(f, "METVIEW_DIR");
        WriteEnvVarIfNotNull(f, "METVIEW_DIR_SHARE");
        WriteEnvVarIfNotNull(f, "METVIEW_TMPDIR");
        WriteEnvVarIfNotNull(f, "METVIEW_EXIT");
        WriteEnvVarIfNotNull(f, "METVIEW_MODE");
        WriteEnvVarIfNotNull(f, "METVIEW_TIMEOUT");
        WriteEnvVarIfNotNull(f, "METVIEW_MACRO_PATH");
        WriteEnvVarIfNotNull(f, "MV_MODE");
        fprintf(f, "EVENT_PORT=%d\n", gOpt.port);
        WriteEnvVarIfNotNull(f, "EVENT_HOST");
        WriteEnvVarIfNotNull(f, "METVIEW_CMD");
        WriteEnvVarIfNotNull(f, "MV_DESKTOP_NAME");
        WriteEnvVarIfNotNull(f, "MARS_CONFIG");
        WriteEnvVarIfNotNull(f, "MARS_CFG_FILE");
        WriteEnvVarIfNotNull(f, "MARS_STAT_PREFIX");
        WriteEnvVarIfNotNull(f, "MARS_EMS_FILE");
        WriteEnvVarIfNotNull(f, "MARS_STATISTICS_FILE");
        WriteEnvVarIfNotNull(f, "MARS_LANGUAGE_FILE");
        WriteEnvVarIfNotNull(f, "MARS_USER_HOME");
        WriteEnvVarIfNotNull(f, "MARS_STAT_DIR");
        WriteEnvVarIfNotNull(f, "MARS_COMPUTE_FLAG");
        WriteEnvVarIfNotNull(f, "MARS_EMS_ACCOUNTS_FILE");
        WriteEnvVarIfNotNull(f, "MARS_TEST_FILE");
        WriteEnvVarIfNotNull(f, "MARS_HOME");
        WriteEnvVarIfNotNull(f, "TMPDIR");

        // XXX should be changed to ECCCODES_DEFINITION_PATH in the future
        WriteEnvVarIfNotNull(f, "GRIB_DEFINITION_PATH");

        //		fprintf(f,"EVENT_PORT=%d\n",gOpt.port);
        //		fprintf(f,";TMPDIR=%s\n",getenv("TMPDIR"));
        fclose(f);

        // send a signal to the Python process to say that we've written the information
        kill(gOpt.python_pid, SIGUSR1);
        marslog(LOG_DBUG, "Sent signal %d to Python process %d", SIGUSR1, gOpt.python_pid);
    }


    for (;;) {
        struct timeval curr_delay = bigtime;  //-- create a copy

        if (!OutputQueue::Empty() || !WaitingQueue::Empty())
            curr_delay = smalltime;

        //-- use a copy to safeguard original structs!
        struct timeval* delay = &curr_delay;

        fds = gFiles;

#ifdef hpux
        //		Feedback from UkMet/SGI:
        //		- HP should be happy now with the 'standard' parameters!
        //        - but not at ECMWF workstation!!
        switch (select(FD_SETSIZE, (int*)&fds, nullptr, nullptr, delay))
#else
        switch (select(FD_SETSIZE, &fds, nullptr, nullptr, delay))
#endif
        {

            case -1:
                if (errno != EINTR)
                    /* Interrupted system call : got on SIGCHLD signals */
                    marslog(LOG_EXIT | LOG_PERR, "select");
                break;

            case 0: /* time out */
                break;

            default:
                if (FD_ISSET(soc, &fds)) /* new service */
                    NewService(soc);
                else
                    Service::DispatchInput(fds);
                break;
        }

        CheckQueues();
        CheckTimeout();
    }
}

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

void Event::EndOfModule(const char* name)
{
    if (name && gOpt.exit == name) {
        marslog(LOG_INFO, "%s exited, closing all lines", name);
        marsexit(0);
    }
}

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

void Event::AbortIfBatch(const char*)
{
    const char* mode = getenv("MV_BATCH_NOABORT");
    if (mode) {
        marslog(LOG_INFO, "AbortIfBatch: MV_BATCH_NOABORT set, no abort!");
        return;  //-- NO ABORT requested by user
    }

    mode = getenv("METVIEW_MODE");
    if (mode && strcmp(mode, "batch") == 0)  //-- abort only batch macros!
    {
        mars.warning = true;

        marslog(LOG_WARN, "+--------------------------------------------+");
        marslog(LOG_WARN, "| A Metview module has crashed in batch mode |");
        marslog(LOG_WARN, "| this will force your batch macro to abort! |");
        marslog(LOG_WARN, "|       Bailing out - Good Bye World!        |");
        marslog(LOG_WARN, "+--------------------------------------------+");
        marslog(LOG_WARN, "PS: set env.var. MV_BATCH_NOABORT if you do not want to abort!");

        marsexit(1);
    }

    marslog(LOG_INFO, "AbortIfBatch: not batch, no abort!");
}

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

void Event::Init(int argc, char** argv)
{
    marsinit(&argc, argv, &gOpt, NUMBER(opts), opts);
    install_exit_proc(CleanUp, nullptr);
    mvSetMarslogLevel();
}

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

int main(int argc, char** argv)
{
    Event::Init(argc, argv);
    Event::Loop();
    return 0;
}
