/***************************** 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 <setjmp.h>
#include <string.h>
#include <strings.h>
#include <signal.h>

#ifdef NO_CHEADERS
#include <sys/time.h>
#else
#include <ctime>
#endif

#include "Event.h"


Service* Service::gHead = nullptr;
Service* Service::gMonitor = nullptr;

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

// clang on Linux showed a problem with the signature of mxdrproc (METV-2881)
#if defined(__linux__) && defined(__clang__)
using mxdrparam = caddr_t;
#else
// in most cases we want to use void*
using mxdrparam = void*;
#endif

#if !defined(__linux__) || defined(__INTEL_COMPILER) || defined(__clang__)
typedef int (*mxdrproc)(void*, void*, int);
#else
typedef int (*mxdrproc)(char*, char*, int);
#endif

void _xdrrec_create(XDR* xdrs, unsigned int sendsize, unsigned int recvsize,
                    mxdrparam handle, mxdrproc readfunc, mxdrproc writefunc)
{
    xdrrec_create(xdrs, sendsize, recvsize, handle, readfunc, writefunc);
};

Service::Service(int soc)
{
    next_ = gHead;
    gHead = this;

    soc_ = soc;

    Event::AddInput(soc);

    //#if defined(sun) || defined(__alpha) ...and Linux/GNU...
    _xdrrec_create(&x_, 0, 0, (mxdrparam)&soc_, (mxdrproc)readtcp, (mxdrproc)writetcp);
    //#else
    // 	xdrrec_create(&x_, 0,0,(void*)&soc_,(mxdrproc)readtcp,(mxdrproc)writetcp);
    //#endif

    name_ = nullptr;
    host_ = nullptr;
    user_ = nullptr;
    ref_ = -1;
    timeout_ = 0;
    alive_ = 0;
    outstanding_ = 0;
    want_progress_ = 0;
    want_message_ = 0;
    valid_ = 0;
    maximum_ = 1;

#ifdef USE_THREADS

    pthread_mutex_init(&mutex_, 0);
    pthread_cond_init(&cond_, 0);

    stop_ = 0;
    running_ = 0;
    threaded_ = 0;


#endif
}

#ifdef USE_THREADS
void* Service::StartThread(void* self)
{
    ((Service*)self)->ThreadProc();
    return 0;
}

void Service::ThreadProc()
{
    // Notify start

    pthread_mutex_lock(&mutex_);

    marslog(LOG_INFO, "Thread started for %s", name_);


    running_ = 1;
    pthread_cond_signal(&cond_);
    pthread_mutex_unlock(&mutex_);

    int more = 1;
    while (more) {
        QElem* q = 0;
        pthread_mutex_lock(&mutex_);
        while (!stop_ && queue_.Empty()) {
            marslog(LOG_INFO, "Thread pthread_cond_wait for %s", name_);
            pthread_cond_wait(&cond_, &mutex_);
        }


        if (stop_) {
            marslog(LOG_INFO, "Thread stop received %s", name_);
            more = 0;
        }
        else {
            q = queue_.Pop();
            if (q->Request())
                marslog(LOG_INFO, "Thread work for %s %p %s", name_, q, q ? q->Request()->name : "");
            else {
                marslog(LOG_INFO, "Thread work for %s %p [empty request => discarded!!!]", name_, q);
                q = 0;
            }
        }

        pthread_mutex_unlock(&mutex_);

        if (q) {
            marslog(LOG_INFO, "Thread --> encode_request for %s %p", name_, this);

            // print_all_requests(q->Request());

            request* r = q->Request();

            encode_request(r, &x_);

            marslog(LOG_INFO, "Thread <-- encode_request for %s", name_);

            pthread_mutex_lock(&mutex_);
            // Because libmars is not reentrant, don;t delete the requests here
            done_.Append(q);
            pthread_mutex_unlock(&mutex_);
        }
    }
}

#endif

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

int Service::Busy()
{
    return maximum_ != 0 && outstanding_ >= maximum_;
}

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

void Service::HandleRegister(request* r)
{
    static char* service = strcache("service");
    const char* name = get_value(r, "NAME", 0);

    if (Find(name)) {
        marslog(LOG_WARN, "Service %s is already registered", name);
        return;
    }

    ref_ = Event::UniqueID();
    name_ = strcache(get_value(r, "NAME", 0));
    user_ = strcache(get_value(r, "USER", 0));
    host_ = strcache(get_value(r, "HOST", 0));
    pid_ = atol(get_value(r, "PID", 0));
    lastcall_ = time(nullptr);
    valid_ = 1;

    marslog(LOG_INFO, "Register: %s %s %s %d as ref %d",
            name_,
            user_,
            host_,
            pid_,
            ref_);

    /* Check for options */

    request* u = mars.setup;
    while (u) {
        if (u->name == service) {
            const char* nme = get_value(u, "name", 0);
            if (nme && (name == nme)) {
                const char* alive = get_value(u, "alive", 0);
                const char* timeout = get_value(u, "timeout", 0);
                const char* maximum = get_value(u, "maximum", 0);

                if (timeout)
                    timeout_ = atol(timeout) * 60;
                if (alive)
                    alive_ = atoi(alive);
                if (maximum)
                    maximum_ = atoi(maximum);

                marslog(LOG_INFO, "%s : Timeout = %d Alive = %d, Max = %d",
                        name_, timeout_, alive_, maximum_);
            }
        }
        u = u->next;
    }

#ifdef USE_THREADS
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    const size_t size = 8 * 1024 * 1024;
    pthread_attr_setstacksize(&attr, size);

    if (pthread_create(&thread_, &attr, StartThread, this)) {
        marslog(LOG_EROR | LOG_PERR, "Cannot start thread");
        threaded_ = 0;
    }
    else {
        // Wait for thread to be running
        pthread_mutex_lock(&mutex_);
        while (!running_)
            pthread_cond_wait(&cond_, &mutex_);
        pthread_mutex_unlock(&mutex_);

        threaded_ = 1;
        marslog(LOG_INFO, "Thread ok for %s", name_);
    }
#endif

    NotifyMonitor("REGISTER", r);
    u = empty_request("BIRTH");
    reqcpy(u, r);
    BroadcastMessage(u);
    free_all_requests(u);
}

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

Service::~Service()
{
#ifdef USE_THREADS

    marslog(LOG_INFO, "Delete service %s", name_);

    if (threaded_) {
        pthread_mutex_lock(&mutex_);
        stop_ = 1;
        pthread_cond_signal(&cond_);
        pthread_mutex_unlock(&mutex_);

        void* data = 0;
        marslog(LOG_INFO, "pthread_join service %s", name_);
        pthread_join(thread_, &data);
    }

    pthread_mutex_destroy(&mutex_);
    pthread_cond_destroy(&cond_);

    queue_.RemoveAll();
    done_.RemoveAll();

#endif


    Service* s = gHead;
    Service* t = nullptr;

    // Remove self for the list
    while (s) {
        if (s == this) {
            if (t)
                t->next_ = next_;
            else
                gHead = next_;
            break;
        }
        t = s;
        s = s->next_;
    }

    // Check if monitor

    if (this == gMonitor)
        gMonitor = nullptr;

    marslog(LOG_INFO, "Closing service %s", name_ ? name_ : "(null)");
    marslog(LOG_INFO, "%d outstanding replies", outstanding_);

    // Send missing replies

    ProcessingQueue::RemoveAll(this);

    // Kill Output requests
    OutputQueue::RemoveAll(this);

    if (name_) {
        NotifyMonitor("EXIT", nullptr);
        request* r = empty_request("DEATH");
        set_value(r, "NAME", "%s", name_);
        set_value(r, "USER", "%s", user_);
        set_value(r, "HOST", "%s", host_);
        set_value(r, "PID", "%ld", pid_);
        BroadcastMessage(r);
        free_all_requests(r);
        Event::EndOfModule(name_);
    }

    xdr_destroy(&x_);
    strfree(name_);
    strfree(host_);
    strfree(user_);
    Event::RemoveInput(soc_);
    close(soc_);
}

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

void Service::DispatchInput(fd_set& fds)
{
    Service* s = gHead;
    while (s) {
        Service* n = s->next_;
        if (FD_ISSET(s->soc_, &fds))
            s->ProcessIncoming();
        s = n;
    }
}

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

void Service::HandleService(request* r)
{
    const char* target = get_value(r, "TARGET", 0);

    // Append request info
    if (get_value(r, "SOURCE", 0) == nullptr) {
        set_value(r, "REQ_ID", "%ld", Event::UniqueID());
        set_value(r, "SOURCE", "%s", name_);
        set_value(r, "SOURCE_REF", "%ld", ref_);
    }


    Service* s = Find(target);
    if (s && !s->Busy())  // Pass to the target service
        s->CallService(r);
    else {
        // Put in waiting queue
        NotifyMonitor("WAIT", r);
        WaitingQueue::Append(r, target);
    }
}

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

static jmp_buf env;

//#if (defined(__cplusplus) || defined(c_plusplus) || !defined(__alpha))
// static void catch_alarm(...)
//#else
// static void catch_alarm(int)
//#endif

// Sun, HP and Alpha have (int) as parameter!
#ifdef sgimips
static void catch_alarm(...)
#else
static void catch_alarm(int)
#endif
{
    marslog(LOG_WARN, "Too many requests : alarm");
    longjmp(env, 1);
}

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

#ifdef USE_THREADS

int Service::EncodeRequest(QElem* q)
{
    pthread_mutex_lock(&mutex_);
    marslog(LOG_INFO, "--> Thread EncodeRequest for %s", name_);

    done_.RemoveAll();
    queue_.Append(q);

    pthread_cond_signal(&cond_);
    pthread_mutex_unlock(&mutex_);

    marslog(LOG_INFO, "<-- Thread EncodeRequest for %s", name_);

    return 0;
}

#else
int Service::EncodeRequest(request* r)
{
    fd_set fds;
    int ret = 0;

    FD_ZERO(&fds);
    FD_SET(soc_, &fds);
    static struct timeval timeout = {
        0,
        10,
    };

    if (setjmp(env) == 0) {
        signal(SIGALRM, catch_alarm);
        alarm(20);

//
// 1) Commented out G.A. Gcc does not want this, on HP.
// 2) feedback from UkMet/SGI:
//     - HP should now be happy with "standard" parameters!
// 3) but at ECMWF we do need this ifdef...??!!
//
#if defined(hpux) && !defined(__GNU__)
        switch (select(FD_SETSIZE, 0, (int*)&fds, nullptr, &timeout))
#else
        switch (select(FD_SETSIZE, nullptr, &fds, nullptr, &timeout))
#endif
        {
            case 0:
                // this can be if the module does not listen, especialy messages
                marslog(LOG_EROR, "Timeout sending to %s", name_);
                ret = 1;
                break;

            case -1:
                marslog(LOG_EROR | LOG_PERR, "Error sending to %s", name_);
                ret = 1;
                break;

            default:
                encode_request(r, &x_);
                ret = 0;
                break;
        }

        alarm(0);
    }
    else
        ret = 1;
    return ret;
}
#endif

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

void Service::SendRequest(request* r)
{
    static char* MESSAGE = strcache("MESSAGE");
    static char* PROGRESS = strcache("PROGRESS");
    /* check if the module is interested ... */

    if (r) {
        if ((r->name == MESSAGE) && (want_message_ == 0))
            return;

        if ((r->name == PROGRESS) && (want_progress_ == 0))
            return;
    }
    OutputQueue::Append(r, this);
}

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

void Service::GotReply(request* r)
{
    SendRequest(r);
}

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

void Service::HandleReply(request* r)
{
    const char* x = get_value(r, "SOURCE_REF", 0);
    long ref = x ? atol(x) : -1;

    NotifyMonitor("REPLY", r);

    Service* s = Find(ref);
    if (s)
        s->GotReply(r);

    outstanding_--;

    lastcall_ = time(nullptr);

    // Remove from queue

    ProcessingQueue::Remove(r);
}

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

void Service::HandleTimeout(request* r)
{
    const char* t = get_value(r, "TIMEOUT", 0);
    timeout_ = (t ? atol(t) : 0) * 60;
    marslog(LOG_INFO, "Timeout for %s : %d seconds", name_, timeout_);
}

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

void Service::HandleAlive(request* r)
{
    const char* t = get_value(r, "ALIVE", 0);
    alive_ = t ? atoi(t) : 0;
    marslog(LOG_INFO, "Alive value for %s : %s ", name_, alive_ ? "on" : "off");
}

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

void Service::HandleStop(request* r)
{
    const char* t = get_value(r, "STOP", 0);
    int stop = t ? atoi(t) : 0;
    marslog(LOG_INFO, "Stop %d requested by service %s", stop, name_);
    marslog(LOG_INFO, "%s", get_value(r, "INFO", 0));
    marsexit(stop);
}

//==============================================================================
void Service::NotifyMonitor(const char* mode, request* r, long ref, const char* name)
{
    static request* u = nullptr;
    if (!u)
        u = empty_request("PROGRESS");

    if (gMonitor) {
        u->next = r;
        set_value(u, "NAME", "%s", name ? name : "?");
        set_value(u, "REF", "%ld", ref);
        set_value(u, "MODE", "%s", mode);
        gMonitor->SendRequest(u);
    }
}

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

void Service::NotifyMonitor(const char* mode, request* r)
{
    NotifyMonitor(mode, r, ref_, name_);
}

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

void Service::HandleMonitor(request* r)
{
    gMonitor = this;
    marslog(LOG_INFO, "%s is a monitor", name_);


    // Loop through services

    r = empty_request("REGISTER");

    Service* s = gHead;
    while (s) {
        set_value(r, "NAME", "%s", s->name_);
        set_value(r, "HOST", "%s", s->host_);
        set_value(r, "USER", "%s", s->user_);
        set_value(r, "PID", "%ld", s->pid_);
        set_value(r, "REF", "%ld", s->ref_);
        s->NotifyMonitor("REGISTER", r);
        s = s->next_;
    }

    free_all_requests(r);

    // Loop through queues.
    WaitingQueue::NotifyMonitor();
    ProcessingQueue::NotifyMonitor();
}

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

void Service::HandleWant(request* r)
{
    static char* MESSAGE = strcache("MESSAGE");
    static char* PROGRESS = strcache("PROGRESS");
    const char* kind = get_value(r, "TYPE", 0);

    if (kind == MESSAGE)
        want_message_++;
    if (kind == PROGRESS)
        want_progress_++;
}

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

void Service::HandleProgress(request* r)
{
    const char* x = get_value(r, "SOURCE_REF", 0);
    long ref = x ? atol(x) : -1;
#if 0
    marslog(LOG_INFO, "progress %ld", ref);
    print_all_requests(r);
#endif
    NotifyMonitor("PROGRESS", r);
    Service* s = Find(ref);
    if (s)
        s->SendRequest(r);
}

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

void Service::HandleLater(request* r)
{
    r = r->next;

    marslog(LOG_INFO, "Module %s refuses request", name_);

    // Remove from queue
    outstanding_--;
    ProcessingQueue::Remove(r);

    // Re queue
    NotifyMonitor("WAIT", r);
    WaitingQueue::Later(r, name_);
}

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

void Service::Broadcast(request* r)
{
    Service* s = gHead;
    while (s) {
        s->SendRequest(r);
        s = s->next_;
    }
}

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

void Service::BroadcastMessage(request* r)
{
    static request* u = nullptr;
    if (!u)
        u = empty_request("MESSAGE");
    u->next = r;
    Broadcast(u);
}

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

void Service::CallService(request* r)
{
    NotifyMonitor("SERVICE", r);
    SendRequest(r);
    ProcessingQueue::Append(r, this);
    outstanding_++;
}

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

void Service::HandleDrop(request* r)
{
    const char* target = get_value(r, "TARGET", 0);

    if (get_value(r, "SOURCE", 0) == nullptr) {
        set_value(r, "SOURCE", "%s", name_);
        set_value(r, "SOURCE_REF", "%ld", ref_);
    }

    Service* s = Find(target);
    if (s)
        s->SendRequest(r);
}

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

void Service::HandleMessage(request* r)
{
    if (get_value(r, "SOURCE", 0) == nullptr) {
        set_value(r, "SOURCE", "%s", name_);
        set_value(r, "SOURCE_REF", "%ld", ref_);
    }
    Broadcast(r);
}

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

void Service::HandleGetInfo(request*)
{
    static request* u = nullptr;
    if (u == nullptr)
        u = empty_request("REPLY");
    u->next = mars.setup;
    SendRequest(u);
}

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

void Service::ReplyFail(const char* fmt, const char* name, request* r)
{
    const char* x = get_value(r, "SOURCE_REF", 0);
    long ref = x ? atol(x) : -1;

    Service* a = Service::Find(ref);

    request* u = empty_request("REPLY");
    reqcpy(u, r);
    set_value(u, "ERR_CODE", "-42");
    set_value(u, "ERROR", fmt, name);

    if (a) {
        a->NotifyMonitor("REPLY", u);
        a->GotReply(u);
    }
    else
        NotifyMonitor("REPLY", u, ref, get_value(r, "SOURCE", 0));

    free_all_requests(u);
}

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

void Service::HandleStart(request* r)
{
    const char* name = get_value(r, "NAME", 0);
    const char* cmd = get_value(r, "CMD", 0);
    if (cmd) {
        char buf[1024];
        marslog(LOG_INFO, "Starting command: %s", cmd);
        sprintf(buf, "%s 2>&1 < /dev/null&", no_quotes(cmd));
        system(buf);
    }
    else
        WaitingQueue::StartModule(name);
    SendRequest(nullptr);
}

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

void Service::HandleFollowup(request* r)
{
    Service* s = ProcessingQueue::Followup(r, this);

    if (s)
        s->outstanding_--;
    outstanding_++;
    NotifyMonitor("FOLLOWUP", r);
}

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

Service* Service::Find(const char* name)
{
    Service* s = gHead;
    while (s) {
        if (s->name_ == name)
            return s;
        s = s->next_;
    }
    return nullptr;
}

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

Service* Service::Find(long ref)
{
    Service* s = gHead;
    while (s) {
        if (s->ref_ == ref)
            return s;
        s = s->next_;
    }
    return nullptr;
}

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

void Service::HandleExit(request*)
{
    valid_ = 0;
}

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

void Service::HandleMaximum(request* r)
{
    const char* t = get_value(r, "MAXIMUM", 0);
    maximum_ = t ? atoi(t) : 0;
    marslog(LOG_INFO, "Maximum value for %s : %d ", name_, maximum_);
}

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

int Service::CheckTimeout()
{
    time_t now = time(nullptr);
    int alive = 0;

    Service* s = gHead;
    while (s) {
        if (s->alive_)
            alive++;

        if (s->outstanding_ == 0 &&
            s->timeout_ != 0 && now - s->lastcall_ > s->timeout_) {
            marslog(LOG_INFO, "Service %s has timed out", s->name_);
            delete s;
            return 1;
        }

        s = s->next_;
    }
    return alive;
}

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

void Service::UserMessage(const char* fmt, ...)
{
    va_list al;
    request* r = empty_request("USER_MESSAGE");
    char buf[1024];

    va_start(al, fmt);
    vsprintf(buf, fmt, al);
    va_end(al);

    set_value(r, "INFO", "%s", buf);

    BroadcastMessage(r);
    free_all_requests(r);
}

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

using reqproc = void (Service::*)(request*);

struct
{
    const char* name;
    reqproc proc;
} handlers[] = {

    {
        "EXIT",
        &Service::HandleExit,
    },
    {
        "REGISTER",
        &Service::HandleRegister,
    },
    {
        "FOLLOWUP",
        &Service::HandleFollowup,
    },
    {
        "SERVICE",
        &Service::HandleService,
    },
    {
        "MESSAGE",
        &Service::HandleMessage,
    },
    {
        "REPLY",
        &Service::HandleReply,
    },
    {
        "DROP",
        &Service::HandleDrop,
    },
    {
        "PROGRESS",
        &Service::HandleProgress,
    },
    {
        "GETINFO",
        &Service::HandleGetInfo,
    },
    {
        "MONITOR",
        &Service::HandleMonitor,
    },
    {
        "TIMEOUT",
        &Service::HandleTimeout,
    },
    {
        "START",
        &Service::HandleStart,
    },
    {
        "WANT",
        &Service::HandleWant,
    },
    {
        "LATER",
        &Service::HandleLater,
    },
    {
        "ALIVE",
        &Service::HandleAlive,
    },
    {
        "STOP",
        &Service::HandleStop,
    },
    {
        "MAXIMUM",
        &Service::HandleMaximum,
    },

};

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

void Service::Dispatch(request* r)
{
    lastcall_ = time(nullptr);
    for (auto& handler : handlers)
        if (handler.name == r->name)
            (this->*handler.proc)(r);
}

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

void Service::ProcessIncoming()
{
    request* r = nullptr;
    static int first = 1;

    //    marslog(LOG_INFO, "Service incoming: %s", name_);

    if (first) {
        for (auto& handler : handlers)
            handler.name = strcache(handler.name);
        first = 0;
    }

    do {
        r = decode_request(&x_);

        if (r == (void*)-1) /* connection is lost */
            valid_ = 0;
        else {
            marslog(LOG_DBUG, "Service::ProcessIncoming(), dispatch request:");
            if (mars.debug)
                print_all_requests(r);

            Dispatch(r);
            free_all_requests(r);
        }
    } while (ref_ >= 0 && !xdrrec_eof(&x_));

    if (!valid_)
        delete this;
}
