/*
 * jim-signal.c
 *
 */

#include <signal.h>
#include <string.h>
#include <ctype.h>

#include "jimautoconf.h"
#ifdef HAVE_UNISTD_H
    #include <unistd.h>
#endif
#ifdef HAVE_STRINGS_H
    #include <strings.h>
#endif

#include <jim-subcmd.h>
#include <jim-signal.h>

#define MAX_SIGNALS_WIDE (sizeof(jim_wide) * 8)
#if defined(NSIG)
    #define MAX_SIGNALS (int)((NSIG < MAX_SIGNALS_WIDE) ? NSIG : MAX_SIGNALS_WIDE)
#else
    #define MAX_SIGNALS (int)MAX_SIGNALS_WIDE
#endif

static jim_wide *sigloc;
static jim_wide sigsignored;
static struct sigaction *sa_old;
static struct {
    int status;
    const char *name;
} siginfo[MAX_SIGNALS];

/* Make sure to do this as a wide, not int */
#define sig_to_bit(SIG) ((jim_wide)1 << (SIG))

static void signal_handler(int sig)
{
    /* Remember which signals occurred and store in *sigloc.
     * Jim_Eval() will notice this as soon as it can and throw an error
     */
    *sigloc |= sig_to_bit(sig);
}

static void signal_ignorer(int sig)
{
    /* Remember which signals occurred for access by 'signal check' */
    sigsignored |= sig_to_bit(sig);
}

void Jim_SignalSetIgnored(jim_wide mask)
{
    sigsignored |= mask;
}

static void signal_init_names(void)
{
#define SET_SIG_NAME(SIG) siginfo[SIG].name = #SIG

    SET_SIG_NAME(SIGABRT);
    SET_SIG_NAME(SIGALRM);
    SET_SIG_NAME(SIGBUS);
    SET_SIG_NAME(SIGCHLD);
    SET_SIG_NAME(SIGCONT);
    SET_SIG_NAME(SIGFPE);
    SET_SIG_NAME(SIGHUP);
    SET_SIG_NAME(SIGILL);
    SET_SIG_NAME(SIGINT);
#ifdef SIGIO
    SET_SIG_NAME(SIGIO);
#endif
    SET_SIG_NAME(SIGKILL);
    SET_SIG_NAME(SIGPIPE);
    SET_SIG_NAME(SIGPROF);
    SET_SIG_NAME(SIGQUIT);
    SET_SIG_NAME(SIGSEGV);
    SET_SIG_NAME(SIGSTOP);
    SET_SIG_NAME(SIGSYS);
    SET_SIG_NAME(SIGTERM);
    SET_SIG_NAME(SIGTRAP);
    SET_SIG_NAME(SIGTSTP);
    SET_SIG_NAME(SIGTTIN);
    SET_SIG_NAME(SIGTTOU);
    SET_SIG_NAME(SIGURG);
    SET_SIG_NAME(SIGUSR1);
    SET_SIG_NAME(SIGUSR2);
    SET_SIG_NAME(SIGVTALRM);
    SET_SIG_NAME(SIGWINCH);
    SET_SIG_NAME(SIGXCPU);
    SET_SIG_NAME(SIGXFSZ);
#ifdef SIGPWR
    SET_SIG_NAME(SIGPWR);
#endif
#ifdef SIGCLD
    SET_SIG_NAME(SIGCLD);
#endif
#ifdef SIGEMT
    SET_SIG_NAME(SIGEMT);
#endif
#ifdef SIGLOST
    SET_SIG_NAME(SIGLOST);
#endif
#ifdef SIGPOLL
    SET_SIG_NAME(SIGPOLL);
#endif
#ifdef SIGINFO
    SET_SIG_NAME(SIGINFO);
#endif
}

/*
 *----------------------------------------------------------------------
 *
 * Tcl_SignalId --
 *
 *      Return a textual identifier for a signal number.
 *
 * Results:
 *      This procedure returns a machine-readable textual identifier
 *      that corresponds to sig.  The identifier is the same as the
 *      #define name in signal.h.
 *
 * Side effects:
 *      None.
 *
 *----------------------------------------------------------------------
 */
const char *Jim_SignalId(int sig)
{
    if (sig >=0 && sig < MAX_SIGNALS) {
        if (siginfo[sig].name) {
            return siginfo[sig].name;
        }
    }
    return "unknown signal";
}

/**
 * Given the name of a signal, returns the signal value if found,
 * or returns -1 (and sets an error) if not found.
 * We accept -SIGINT, SIGINT, INT or any lowercase version or a number,
 * either positive or negative.
 */
static int find_signal_by_name(Jim_Interp *interp, const char *name)
{
    int i;
    const char *pt = name;

    /* Remove optional - and SIG from the front of the name */
    if (*pt == '-') {
        pt++;
    }
    if (strncasecmp(name, "sig", 3) == 0) {
        pt += 3;
    }
    if (isdigit(UCHAR(pt[0]))) {
        i = atoi(pt);
        if (i > 0 && i < MAX_SIGNALS) {
            return i;
        }
    }
    else {
        for (i = 1; i < MAX_SIGNALS; i++) {
            /* Jim_SignalId() returns names such as SIGINT, and
             * returns "unknown signal" if unknown, so this will work
             */
            if (strcasecmp(Jim_SignalId(i) + 3, pt) == 0) {
                return i;
            }
        }
    }
    Jim_SetResultFormatted(interp, "unknown signal %s", name);

    return -1;
}

#define SIGNAL_ACTION_HANDLE 1
#define SIGNAL_ACTION_IGNORE -1
#define SIGNAL_ACTION_BLOCK -2
#define SIGNAL_ACTION_DEFAULT 0

static int do_signal_cmd(Jim_Interp *interp, int action, int argc, Jim_Obj *const *argv)
{
    struct sigaction sa;
    int i;

    if (argc == 0) {
        Jim_SetResult(interp, Jim_NewListObj(interp, NULL, 0));
        for (i = 1; i < MAX_SIGNALS; i++) {
            if (siginfo[i].status == action) {
                /* Add signal name to the list  */
                Jim_ListAppendElement(interp, Jim_GetResult(interp),
                    Jim_NewStringObj(interp, Jim_SignalId(i), -1));
            }
        }
        return JIM_OK;
    }

    /* Catch all the signals we care about */
    if (action != SIGNAL_ACTION_DEFAULT) {
        memset(&sa, 0, sizeof(sa));
        if (action == SIGNAL_ACTION_HANDLE) {
            sa.sa_handler = signal_handler;
        }
        else if (action == SIGNAL_ACTION_IGNORE) {
            sa.sa_handler = signal_ignorer;
        }
        else {
            /* SIGNAL_ACTION_BLOCK */
            sa.sa_handler = SIG_IGN;
        }
    }

    /* Iterate through the provided signals */
    for (i = 0; i < argc; i++) {
        int sig = find_signal_by_name(interp, Jim_String(argv[i]));

        if (sig < 0) {
            return JIM_ERR;
        }
        if (action != siginfo[sig].status) {
            /* Need to change the action for this signal */
            switch (action) {
                case SIGNAL_ACTION_BLOCK:
                case SIGNAL_ACTION_HANDLE:
                case SIGNAL_ACTION_IGNORE:
                    if (siginfo[sig].status == SIGNAL_ACTION_DEFAULT) {
                        if (!sa_old) {
                            /* Allocate the structure the first time through */
                            sa_old = Jim_Alloc(sizeof(*sa_old) * MAX_SIGNALS);
                        }
                        sigaction(sig, &sa, &sa_old[sig]);
                    }
                    else {
                        sigaction(sig, &sa, 0);
                    }
                    break;

                case SIGNAL_ACTION_DEFAULT:
                    /* Restore old handler */
                    if (sa_old) {
                        sigaction(sig, &sa_old[sig], 0);
                    }
            }
            siginfo[sig].status = action;
        }
    }

    return JIM_OK;
}

static int signal_cmd_handle(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return do_signal_cmd(interp, SIGNAL_ACTION_HANDLE, argc, argv);
}

static int signal_cmd_ignore(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return do_signal_cmd(interp, SIGNAL_ACTION_IGNORE, argc, argv);
}

static int signal_cmd_block(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return do_signal_cmd(interp, SIGNAL_ACTION_BLOCK, argc, argv);
}

static int signal_cmd_default(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    return do_signal_cmd(interp, SIGNAL_ACTION_DEFAULT, argc, argv);
}

static int signal_set_sigmask_result(Jim_Interp *interp, jim_wide sigmask)
{
    int i;
    Jim_Obj *listObj = Jim_NewListObj(interp, NULL, 0);

    for (i = 0; i < MAX_SIGNALS; i++) {
        if (sigmask & sig_to_bit(i)) {
            Jim_ListAppendElement(interp, listObj, Jim_NewStringObj(interp, Jim_SignalId(i), -1));
        }
    }
    Jim_SetResult(interp, listObj);
    return JIM_OK;
}

static int signal_cmd_check(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int clear = 0;
    jim_wide mask = 0;
    jim_wide ignored;

    if (argc > 0 && Jim_CompareStringImmediate(interp, argv[0], "-clear")) {
        clear++;
    }
    if (argc > clear) {
        int i;

        /* Signals specified */
        for (i = clear; i < argc; i++) {
            int sig = find_signal_by_name(interp, Jim_String(argv[i]));

            if (sig < 0 || sig >= MAX_SIGNALS) {
                return JIM_ERR;
            }
            mask |= sig_to_bit(sig);
        }
    }
    else {
        /* No signals specified, so check/clear all */
        mask = ~mask;
    }

    /* Be careful we don't have a race condition where signals are cleared but not returned */
    ignored = sigsignored & mask;
    if (clear) {
        sigsignored &= ~ignored;
    }
    /* Set the result */
    signal_set_sigmask_result(interp, ignored);
    return JIM_OK;
}

static int signal_cmd_throw(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int sig = SIGINT;

    if (argc == 1) {
        if ((sig = find_signal_by_name(interp, Jim_String(argv[0]))) < 0) {
            return JIM_ERR;
        }
    }

    /* If the signal is ignored ... */
    if (siginfo[sig].status == SIGNAL_ACTION_IGNORE) {
        sigsignored |= sig_to_bit(sig);
        return JIM_OK;
    }

    /* Just set the signal */
    interp->sigmask |= sig_to_bit(sig);

    /* Set the canonical name of the signal as the result */
    Jim_SetResultString(interp, Jim_SignalId(sig), -1);

    /* And simply say we caught the signal */
    return JIM_SIGNAL;
}

/*
 *-----------------------------------------------------------------------------
 *
 * Jim_SignalCmd --
 *     Implements the TCL signal command:
 *         signal handle|ignore|default|throw ?signals ...?
 *         signal throw signal
 *
 *     Specifies which signals are handled by Tcl code.
 *     If the one of the given signals is caught, it causes a JIM_SIGNAL
 *     exception to be thrown which can be caught by catch.
 *
 *     Use 'signal ignore' to ignore the signal(s)
 *     Use 'signal default' to go back to the default behaviour
 *     Use 'signal throw signal' to raise the given signal
 *
 *     If no arguments are given, returns the list of signals which are being handled
 *
 * Results:
 *      Standard TCL results.
 *
 *-----------------------------------------------------------------------------
 */
static const jim_subcmd_type signal_command_table[] = {
    {   "handle",
        "?signals ...?",
        signal_cmd_handle,
        0,
        -1,
        /* Description: Lists handled signals, or adds to handled signals */
    },
    {   "ignore",
        "?signals ...?",
        signal_cmd_ignore,
        0,
        -1,
        /* Description: Lists ignored signals, or adds to ignored signals */
    },
    {   "block",
        "?signals ...?",
        signal_cmd_block,
        0,
        -1,
        /* Description: Lists blocked signals, or adds to blocked signals */
    },
    {   "default",
        "?signals ...?",
        signal_cmd_default,
        0,
        -1,
        /* Description: Lists defaulted signals, or adds to defaulted signals */
    },
    {   "check",
        "?-clear? ?signals ...?",
        signal_cmd_check,
        0,
        -1,
        /* Description: Returns ignored signals which have occurred, and optionally clearing them */
    },
    {   "throw",
        "?signal?",
        signal_cmd_throw,
        0,
        1,
        /* Description: Raises the given signal (default SIGINT) */
    },
    { NULL }
};

/**
 * Restore default signal handling.
 */
static void JimSignalCmdDelete(Jim_Interp *interp, void *privData)
{
    int i;
    if (sa_old) {
        for (i = 1; i < MAX_SIGNALS; i++) {
            if (siginfo[i].status != SIGNAL_ACTION_DEFAULT) {
                sigaction(i, &sa_old[i], 0);
                siginfo[i].status = SIGNAL_ACTION_DEFAULT;
            }
        }
    }
    Jim_Free(sa_old);
    sa_old = NULL;
    sigloc = NULL;
}

static int Jim_AlarmCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int ret;

    if (argc != 2) {
        Jim_WrongNumArgs(interp, 1, argv, "seconds");
        return JIM_ERR;
    }
    else {
#ifdef HAVE_UALARM
        double t;

        ret = Jim_GetDouble(interp, argv[1], &t);
        if (ret == JIM_OK) {
            if (t < 1) {
                ualarm(t * 1e6, 0);
            }
            else {
                alarm(t);
            }
        }
#else
        long t;

        ret = Jim_GetLong(interp, argv[1], &t);
        if (ret == JIM_OK) {
            alarm(t);
        }
#endif
    }

    return ret;
}

static int Jim_SleepCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int ret;

    if (argc != 2) {
        Jim_WrongNumArgs(interp, 1, argv, "seconds");
        return JIM_ERR;
    }
    else {
        double t;

        ret = Jim_GetDouble(interp, argv[1], &t);
        if (ret == JIM_OK) {
#ifdef HAVE_USLEEP
            usleep((int)((t - (int)t) * 1e6));
#endif
            sleep(t);
        }
    }

    return ret;
}

static int Jim_KillCmd(Jim_Interp *interp, int argc, Jim_Obj *const *argv)
{
    int sig;
    long pid;
    Jim_Obj *pidObj;
    const char *signame;

    if (argc != 2 && argc != 3) {
        Jim_WrongNumArgs(interp, 1, argv, "?SIG|-0? pid");
        return JIM_ERR;
    }

    if (argc == 2) {
        sig = SIGTERM;
        pidObj = argv[1];
    }
    else {
        signame = Jim_String(argv[1]);
        pidObj = argv[2];

        /* Special 'kill -0 pid' to determine if a pid exists */
        if (strcmp(signame, "-0") == 0 || strcmp(signame, "0") == 0) {
            sig = 0;
        }
        else {
            sig = find_signal_by_name(interp, signame);
            if (sig < 0) {
                return JIM_ERR;
            }
        }
    }

    if (Jim_GetLong(interp, pidObj, &pid) != JIM_OK) {
        return JIM_ERR;
    }

    if (kill(pid, sig) == 0) {
        return JIM_OK;
    }

    Jim_SetResultString(interp, "kill: Failed to deliver signal", -1);
    return JIM_ERR;
}

int Jim_signalInit(Jim_Interp *interp)
{
    Jim_PackageProvideCheck(interp, "signal");
    Jim_CreateCommand(interp, "alarm", Jim_AlarmCmd, 0, 0);
    Jim_CreateCommand(interp, "kill", Jim_KillCmd, 0, 0);
    /* Sleep is slightly dubious here */
    Jim_CreateCommand(interp, "sleep", Jim_SleepCmd, 0, 0);

    /* Teach the jim core how to set a result from a sigmask */
    interp->signal_set_result = signal_set_sigmask_result;

    /* Currently only the top level interp supports signals */
    if (!sigloc) {
        signal_init_names();

        /* Make sure we know where to store the signals which occur */
        sigloc = &interp->sigmask;

        Jim_CreateCommand(interp, "signal", Jim_SubCmdProc, (void *)signal_command_table, JimSignalCmdDelete);
    }

    return JIM_OK;
}
