/*
 * © Copyright 1996-2012 ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence Version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation nor
 * does it submit to any jurisdiction.
 */

#include <arpa/inet.h>
#include <errno.h>
#include <netdb.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "mars.h"
#ifdef sgi
#include <bstring.h>
#endif

static int cnt     = 0; /* Number of kids */
static int clients = 0; /* Number of clients processed */

static void no_zombies(int sig
#if defined(__cplusplus) || defined(c_plusplus)
                       ,
                       ...
#endif
) {
    int status;
    pid_t pid;


#ifdef sun_bsd
    while ((pid = wait3(&status, WNOHANG, NULL)) > 0) {
#else
    pid = wait(&status);
#endif
        cnt--;
        marslog(LOG_DBUG, "Remaining tasks %d", cnt);


        if (WIFEXITED(status) && WEXITSTATUS(status)) {
            marslog(LOG_WARN, "task pid %d terminated with exit %d",
                    pid, WEXITSTATUS(status));
            if (WEXITSTATUS(status) == 9)
                marslog(LOG_EXIT, "Exiting...");
        }

        if (WIFSIGNALED(status)) {
            marslog(LOG_WARN, "task pid %d terminated by signal %d",
                    pid, WTERMSIG(status));

            /* due to s sgi bug, exit if child is dead ... */

            if (WTERMSIG(status) == SIGBUS || WTERMSIG(status) == SIGSEGV)
                marslog(LOG_EXIT, "Exiting...");
        }

        if (WIFSTOPPED(status))
            marslog(LOG_WARN, "task pid %d stopped by signal %d",
                    pid, WSTOPSIG(status));

            /* SysV need signal handler to be reinstalled */

#ifdef sun_bsd
    } /* end of while loop */
#endif
    signal(SIGCHLD, no_zombies);
}

struct sockaddr_in* addr_of(int soc) {
    static struct sockaddr_in sin;
    socklen_t len = sizeof(sin);

    if (getsockname(soc, (struct sockaddr*)&sin, &len) != 0)
        marslog(LOG_EROR | LOG_PERR, "getsockname");
    return &sin;
}

int tcp_server(int port) {

    int flg;
    int s;
    struct sockaddr_in sin;

#ifdef SO_LINGER
    struct linger ling;
#endif

    s = socket(AF_INET, SOCK_STREAM, 0);

    if (s < 0) {
        marslog(LOG_EROR | LOG_PERR, "socket");
        return -1;
    }

    if (port != 0) {
        flg = 1;
        if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &flg, sizeof(flg)) < 0)
            marslog(LOG_WARN | LOG_PERR, "setsockopt SO_REUSEADDR");
    }

    flg = 1;
    if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, &flg, sizeof(flg)) < 0)
        marslog(LOG_WARN | LOG_PERR, "setsockopt SO_KEEPALIVE");


#if 0
#ifdef SO_REUSEPORT
	flg = 1 ;
	if(setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &flg, sizeof(flg))<0)
		marslog(LOG_WARN|LOG_PERR,"setsockopt SO_REUSEPORT");
#endif
#endif

#ifdef SO_LINGER
    ling.l_onoff  = 0;
    ling.l_linger = 0;
    if (setsockopt(s, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling)) < 0)
        marslog(LOG_WARN | LOG_PERR, "setsockopt SO_LINGER");
#else
#ifdef SO_DONTLINGER
    if (setsockopt(s, SOL_SOCKET, SO_DONTLINGER, NULL, 0) < 0)
        marslog(LOG_WARN | LOG_PERR, "setsockopt SO_DONTLINGER");
#endif
#endif


    bzero(&sin, sizeof(struct sockaddr_in));
    sin.sin_port        = htons(port);
    sin.sin_family      = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;

    while (bind(s, (struct sockaddr*)&sin, sizeof(struct sockaddr_in)) == -1) {
        marslog(LOG_WARN | LOG_PERR, "bind port = %d", port);
        if (port == 0) {
            close(s);
            return -1;
        }
        sleep(5);
    }

    socket_buffers(s);

    if (listen(s, 5) == -1) {
        close(s);
        marslog(LOG_EROR | LOG_PERR, "listen");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);

    return s;
}

const char* host_of(struct sockaddr_in* from) {
    struct hostent* remote;

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

    if (remote)
        return remote->h_name;
    else
        return inet_ntoa(from->sin_addr);
}

int port_of(struct sockaddr_in* from) {
    return ntohs(from->sin_port);
}

void print_address(const char* name, struct sockaddr_in* from) {
    if (mars.show_hosts) {
        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, "%s %s (%s) %d", name,
                        remote->h_name, net, ntohs(from->sin_port));
            else
                marslog(LOG_INFO, "%s %s %d", name, net, ntohs(from->sin_port));
        }
        else
            marslog(LOG_INFO, "%s (connection is not from internet) %d", name, from->sin_family);
    }
}

void server_run(int port, taskproc task, void* data) {

    struct sockaddr_in from;
    int s;
    int snew;
    socklen_t fromlen;
#ifdef SIG_BLOCK
    sigset_t set;
#endif

    /* Start real server */

    s = tcp_server(port);
    if (s < 0)
        marslog(LOG_EXIT, "Exiting server");

    /* Dont't create zombies */

    signal(SIGCHLD, no_zombies);

    /* Dont't get killed by pipes */
    signal(SIGPIPE, SIG_IGN);

    /* Ignore hang up */
    signal(SIGHUP, SIG_IGN);

    /* loginit(stdout,stdout); */
    marslog(LOG_INFO, "Starting Server - port %d", port);

    for (;;) {

        fromlen = sizeof(from);
        if ((snew = accept(s, (struct sockaddr*)&from, &fromlen)) < 0) {
            if (errno != EINTR)
                /* Interrupted system call : got on SIGCHLD signals */
                marslog(LOG_WARN | LOG_PERR, "accept");
        }
        else {
            pid_t child_pid;
            marslog(LOG_DBUG, "Got connection");

            if (from.sin_family != AF_INET) {
                marslog(LOG_INFO, "connection is not from internet");
                close(snew);
                continue;
            }
            else if (mars.debug)
                print_address("Got tcp connection", &from);

            fflush(0);
#ifdef SIG_BLOCK
            sigemptyset(&set);
            sigaddset(&set, SIGCHLD);
            sigprocmask(SIG_BLOCK, &set, NULL);
#else
            sighold(SIGCHLD);
#endif

            if (mars.nofork) {
                task(snew, 1, data);
                close(snew);
            }
            else
                switch (child_pid = fork()) {
                    case 0:
                        close(s);
                        if (mars.show_pid)
                            mars.show_pid = getpid();
                        task(snew, cnt + 1, data);
                        marsexit(0);
                        break;

                    case -1:
                        marslog(LOG_EROR | LOG_PERR, "Cannot fork");
                        close(snew);
                        break;

                    default:
                        cnt++;
                        clients++;
                        close(snew);
                        break;
                }

            if (mars.clients && (clients == mars.clients)) {
                int status;
                marslog(LOG_INFO, "%d connection(s) satisfied", clients);
                marslog(LOG_INFO, "%d outstanding task(s)", cnt);
                marslog(LOG_INFO, "Waiting for child process id %d to finish", child_pid);
                waitpid(child_pid, &status, 0);
                marslog(LOG_INFO, "Exiting...");
                marsexit(0);
            }

#ifdef SIG_BLOCK
            sigprocmask(SIG_UNBLOCK, &set, NULL);
#else
            sigrelse(SIGCHLD);
#endif
        }
    }
}

int server_mode(int* port, char* address) {
    char me[MAXHOSTNAMELEN];
    int s = tcp_server(0); /* 0 means system chooses port */
    struct hostent* h;
    struct sockaddr_in sin;
    socklen_t len = sizeof(sin);
    int max       = 10;
    int cnt       = 0;

    while (s < 0) {
        sleep(10);
        s = tcp_server(0);
        if (cnt++ > max)
            return -1;
    }

    if (getsockname(s, (struct sockaddr*)&sin, &len) < 0) {
        marslog(LOG_EROR | LOG_PERR, "getsockname");
        close(s);
        return -1;
    }

    *port = ntohs(sin.sin_port);

    if (gethostname(me, sizeof(me)) < 0) {
        marslog(LOG_EROR | LOG_PERR, "gethostname");
        close(s);
        return -1;
    }

    if ((h = gethostbyname(me)) == NULL) {
        marslog(LOG_EROR | LOG_PERR, "gethostbyname");
        close(s);
        return -1;
    }

    bcopy(h->h_addr_list[0], &sin.sin_addr, h->h_length);


    strcpy(address, inet_ntoa(sin.sin_addr));

    return s;
}
