/*
 * Support for tty settings using termios
 *
 * (c) 2016 Steve Bennett <steveb@workware.net.au>
 *
 */

/* termios support is required */
#include <jim-tty.h>
#include <sys/ioctl.h>
#include <termios.h>

static const struct {
    unsigned baud;
    speed_t speed;
} baudtable[] = {
    { 0, B0 },
    { 50, B50 },
    { 75, B75 },
    { 110, B110 },
    { 134, B134 },
    { 150, B150 },
    { 200, B200 },
    { 300, B300 },
    { 600, B600 },
    { 1200, B1200 },
    { 1800, B1800 },
    { 2400, B2400 },
    { 4800, B4800 },
    { 9600, B9600 },
    { 19200, B19200 },
    { 38400, B38400 },
    { 57600, B57600 },
    { 115200, B115200 },
#ifdef B230400
    { 230400, B230400 },
#endif
#ifdef B460800
    { 460800, B460800 }
#endif
};

struct flag_name_map {
    const char *name;
    unsigned value;
};

static const struct flag_name_map parity_map[] = {
    { "none", 0 },
    { "even", PARENB },
    { "odd", PARENB | PARODD },
};
static const struct flag_name_map data_size_map[] = {
    { "5", CS5 },
    { "6", CS6 },
    { "7", CS7 },
    { "8", CS8 },
};
static const struct flag_name_map stop_size_map[] = {
    { "1", 0 },
    { "2", CSTOPB },
};
static const struct flag_name_map input_map[] = {
    { "raw", 0 },
    { "cooked", ICANON },
};
static const struct flag_name_map output_map[] = {
    { "raw", 0 },
    { "cooked", OPOST },
};

static const char * const tty_settings_names[] = {
    "baud",
    "data",
    "handshake",
    "input",
    "output",
    "parity",
    "stop",
    "vmin",
    "vtime",
    "echo",
    "vstart",
    "vstop",
    NULL
};

enum {
    OPT_BAUD,
    OPT_DATA,
    OPT_HANDSHAKE,
    OPT_INPUT,
    OPT_OUTPUT,
    OPT_PARITY,
    OPT_STOP,
    OPT_VMIN,
    OPT_VTIME,
    OPT_ECHO,
    OPT_VSTART,
    OPT_VSTOP,
};


#define ARRAYSIZE(A) (sizeof(A)/sizeof(*(A)))

/**
 * Search the flag/name map for an entry with the given name.
 * (Actually, just matching the first char)
 * Returns a pointer to the entry if found, or NULL if not.
 */
static const struct flag_name_map *flag_name_to_value(const struct flag_name_map *map, int len, const char *name)
{
    int i;

    for (i = 0; i < len; i++) {
        /* Only need to compare the first character since all names are unique in the first char */
        if (*name == *map[i].name) {
            return &map[i];
        }
    }
    return NULL;
}

/**
 * Search the flag/name map for an entry with the matching value.
 * Returns the corresponding name if found, or NULL if no match.
 */
static const char *flag_value_to_name(const struct flag_name_map *map, int len, unsigned value)
{
    int i;

    for (i = 0; i < len; i++) {
        if (value == map[i].value) {
            return map[i].name;
        }
    }
    return NULL;
}

/**
 * If 'str2' is not NULL, appends 'str1' and 'str2' to the list.
 * Otherwise does nothing.
 */
static void JimListAddPair(Jim_Interp *interp, Jim_Obj *listObjPtr, const char *str1, const char *str2)
{
    if (str2) {
        Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, str1, -1));
        Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, str2, -1));
    }
}

Jim_Obj *Jim_GetTtySettings(Jim_Interp *interp, int fd)
{
    struct termios tio;
    size_t i;
    const char *p;
    Jim_Obj *listObjPtr;
    speed_t speed;
    int baud;

    if (tcgetattr(fd, &tio) < 0) {
        return NULL;
    }

    listObjPtr = Jim_NewListObj(interp, NULL, 0);

    p = flag_value_to_name(parity_map, ARRAYSIZE(parity_map), tio.c_cflag & (PARENB | PARODD));
    JimListAddPair(interp, listObjPtr, "parity", p);
    p = flag_value_to_name(data_size_map, ARRAYSIZE(data_size_map), tio.c_cflag & CSIZE);
    JimListAddPair(interp, listObjPtr, "data", p);
    p = flag_value_to_name(stop_size_map, ARRAYSIZE(stop_size_map), tio.c_cflag & CSTOPB);
    JimListAddPair(interp, listObjPtr, "stop", p);
    if (tio.c_iflag & (IXON | IXOFF)) {
        p = "xonxoff";
    }
    else if (tio.c_cflag & CRTSCTS) {
        p = "rtscts";
    }
    else {
        p = "none";
    }
    JimListAddPair(interp, listObjPtr, "handshake", p);
    p = flag_value_to_name(input_map, ARRAYSIZE(input_map), tio.c_lflag & ICANON);
    JimListAddPair(interp, listObjPtr, "input", p);
    p = flag_value_to_name(output_map, ARRAYSIZE(output_map), tio.c_oflag & OPOST);
    JimListAddPair(interp, listObjPtr, "output", p);

    Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, "vmin", -1));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, tio.c_cc[VMIN]));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, "vtime", -1));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, tio.c_cc[VTIME]));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, "vstart", -1));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, tio.c_cc[VSTART]));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, "vstop", -1));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, tio.c_cc[VSTOP]));

    speed = cfgetispeed(&tio);
    baud = 0;
    for (i = 0; i < sizeof(baudtable) / sizeof(*baudtable); i++) {
        if (baudtable[i].speed == speed) {
            baud = baudtable[i].baud;
            break;
        }
    }
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, "baud", -1));
    Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, baud));

    return listObjPtr;
}

int Jim_SetTtySettings(Jim_Interp *interp, int fd, Jim_Obj *dictObjPtr)
{
    int len = Jim_ListLength(interp, dictObjPtr);
    int i;

    struct termios tio;

    if (tcgetattr(fd, &tio) < 0) {
        return -1;
    }

    for (i = 0; i < len; i += 2) {
        Jim_Obj *nameObj = Jim_ListGetIndex(interp, dictObjPtr, i);
        Jim_Obj *valueObj = Jim_ListGetIndex(interp, dictObjPtr, i + 1);
        int opt;
        const struct flag_name_map *p;
        long l;
        size_t j;

        if (Jim_GetEnum(interp, nameObj, tty_settings_names, &opt, "setting", JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK) {
            return JIM_ERR;
        }

        switch (opt) {
            case OPT_BAUD:
                if (Jim_GetLong(interp, valueObj, &l) != JIM_OK) {
                    goto badvalue;
                }
                for (j = 0; j < ARRAYSIZE(baudtable); j++) {
                    if (baudtable[j].baud == l) {
                        break;
                    }
                }
                if (j == ARRAYSIZE(baudtable)) {
                    goto badvalue;
                }
                cfsetospeed(&tio, baudtable[j].speed);
                cfsetispeed(&tio, baudtable[j].speed);
                break;

            case OPT_PARITY:
                p = flag_name_to_value(parity_map, ARRAYSIZE(parity_map), Jim_String(valueObj));
                if (p == NULL) {
badvalue:
                    Jim_SetResultFormatted(interp, "bad value for %#s: %#s", nameObj, valueObj);
                    return JIM_ERR;
                }
                tio.c_cflag &= ~(PARENB | PARODD);
                tio.c_cflag |= p->value;
                break;

            case OPT_STOP:
                p = flag_name_to_value(stop_size_map, ARRAYSIZE(stop_size_map), Jim_String(valueObj));
                if (p == NULL) {
                    goto badvalue;
                }
                tio.c_cflag &= ~CSTOPB;
                tio.c_cflag |= p->value;
                break;

            case OPT_DATA:
                p = flag_name_to_value(data_size_map, ARRAYSIZE(data_size_map), Jim_String(valueObj));
                if (p == NULL) {
                    goto badvalue;
                }
                tio.c_cflag &= ~CSIZE;
                tio.c_cflag |= p->value;
                break;

            case OPT_HANDSHAKE:
                tio.c_iflag &= ~(IXON | IXOFF);
                tio.c_cflag &= ~(CRTSCTS);
                if (Jim_CompareStringImmediate(interp, valueObj, "xonxoff")) {
                    tio.c_iflag |= (IXON | IXOFF);
                }
                else if (Jim_CompareStringImmediate(interp, valueObj, "rtscts")) {
                    tio.c_cflag |= CRTSCTS;
                }
                else if (!Jim_CompareStringImmediate(interp, valueObj, "none")) {
                    goto badvalue;
                }
                break;

            case OPT_VMIN:
            case OPT_VTIME:
            case OPT_VSTART:
            case OPT_VSTOP:
                if (Jim_GetLong(interp, valueObj, &l) != JIM_OK) {
                    goto badvalue;
                }
                switch (opt) {
                    case OPT_VMIN: tio.c_cc[VMIN] = l; break;
                    case OPT_VTIME: tio.c_cc[VTIME] = l; break;
                    case OPT_VSTART: tio.c_cc[VSTART] = l; break;
                    case OPT_VSTOP: tio.c_cc[VSTOP] = l; break;
                }
                break;

            case OPT_OUTPUT:
                p = flag_name_to_value(output_map, ARRAYSIZE(output_map), Jim_String(valueObj));
                if (p == NULL) {
                    goto badvalue;
                }
                tio.c_oflag &= ~OPOST;
                tio.c_oflag |= p->value;
                break;

            case OPT_INPUT:
                p = flag_name_to_value(input_map, ARRAYSIZE(input_map), Jim_String(valueObj));
                if (p == NULL) {
                    goto badvalue;
                }
                if (p->value) {
                    tio.c_lflag |= (ECHO | ECHOE | ECHOK | ECHONL | ICANON | IEXTEN | ISIG | NOFLSH | TOSTOP);
                    tio.c_iflag |= ICRNL;
                }
                else {
                    tio.c_lflag &= ~(ECHO | ECHOE | ECHOK | ECHONL | ICANON | IEXTEN | ISIG | NOFLSH | TOSTOP);
                    tio.c_iflag &= ~ICRNL;
                }
                break;

            case OPT_ECHO:
                if (Jim_GetLong(interp, valueObj, &l) != JIM_OK) {
                    goto badvalue;
                }
                if (l) {
                    tio.c_lflag |= ECHO;
                }
                else {
                    tio.c_lflag &= ~ECHO;
                }
                break;

        }
    }

    if (tcsetattr(fd, TCSAFLUSH, &tio) < 0) {
        return -1;
    }
    return 0;
}

Jim_Obj *Jim_GetTtyControlSettings(Jim_Interp *interp, int fd)
{
    size_t i;
    Jim_Obj *listObjPtr;
    int status;
    static const struct {
        const char * const name;
        int value;
    } modem_signals[] = {
        { "rts", TIOCM_RTS },
        { "dtr", TIOCM_DTR },

        { "dcd", TIOCM_CAR },
        { "dsr", TIOCM_DSR },
        { "ring", TIOCM_RNG },
        { "cts", TIOCM_CTS },
    };

    if (ioctl(fd, TIOCMGET, &status) < 0) {
        return NULL;
    }

    listObjPtr = Jim_NewListObj(interp, NULL, 0);
    for (i = 0; i < ARRAYSIZE(modem_signals); i++) {
        Jim_ListAppendElement(interp, listObjPtr, Jim_NewStringObj(interp, modem_signals[i].name, -1));
        Jim_ListAppendElement(interp, listObjPtr, Jim_NewIntObj(interp, (status & modem_signals[i].value & status) != 0));
    }
    return listObjPtr;
}

int Jim_SetTtyControlSettings(Jim_Interp *interp, int fd, Jim_Obj *dictObjPtr)
{
    static const char * const modem_signal_names[] = {
        "rts", "dtr", "break", NULL
    };
    enum { OPT_RTS, OPT_DTR, OPT_BREAK, };
    int len = Jim_ListLength(interp, dictObjPtr);
    int i;

    int status;

    /* Get the current status of DTR and RTS in case we aren't setting everything */
    if (ioctl(fd, TIOCMGET, &status) < 0) {
        return -1;
    }

    for (i = 0; i < len; i += 2) {
        Jim_Obj *nameObj = Jim_ListGetIndex(interp, dictObjPtr, i);
        Jim_Obj *valueObj = Jim_ListGetIndex(interp, dictObjPtr, i + 1);
        int opt;
        long l;

        if (Jim_GetEnum(interp, nameObj, modem_signal_names, &opt, "signal", JIM_ERRMSG | JIM_ENUM_ABBREV) != JIM_OK) {
            return JIM_ERR;
        }
        if (Jim_GetLong(interp, valueObj, &l) != JIM_OK) {
            Jim_SetResultFormatted(interp, "bad value for %#s: %#s", nameObj, valueObj);
            return JIM_ERR;
        }

        switch (opt) {
            case OPT_RTS:
                if (l) {
                    status |= TIOCM_RTS;
                }
                else {
                    status &= ~TIOCM_RTS;
                }
                break;

            case OPT_DTR:
                if (l) {
                    status |= TIOCM_DTR;
                }
                else {
                    status &= ~TIOCM_DTR;
                }
                break;

            case OPT_BREAK:
                tcsendbreak(fd, l);
                break;
        }
    }

    if (ioctl(fd, TIOCMSET, &status) < 0) {
        return -1;
    }
    return JIM_OK;
}
