/*
    zserial_tty - portable serial port API
    Module for UNIX serial port
    Copyright (C) 2012-2023 Ladislav Vaiz <ok1zia@nagano.cz>

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    version 2 as published by the Free Software Foundation.

*/

#include <zserial.h>

#include <zdebug.h>
#include <zerror.h>
#include <zfhs.h>

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>

#ifdef Z_HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif

#include <sys/stat.h>
#include <sys/types.h>

#ifdef Z_HAVE_TERMIOS_H
#include <termios.h>
#endif

#ifdef Z_HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef Z_HAVE_TERMIOS_H
static int baud(int br){
	switch (br){
		case 50: return B50;
		case 75: return B75;
		case 150: return B150;
		case 200: return B200;
		case 300: return B300;
		case 600: return B600;
		case 1200: return B1200;
		case 2400: return B2400;
		case 4800: return B4800;
		case 9600: return B9600;
		case 19200: return B19200;
		case 38400: return B38400;
		case 57600: return B57600;
		case 115200: return B115200;
		case 230400: return B230400;
		default: return -1;
	}
}

static int cs(int size){
	switch(size){
		case 5: return CS5;
		case 6: return CS6;
		case 7: return CS7;
		case 8:
		default: return CS8;
	}
}

static int zserial_tty_open(struct zserial *zser){
    int ret;
	struct termios tio;

    if (zser->fd >= 0) return 0;

    ret = zfhs_lock(zser->filename, 1);
    if (ret){
        char *c = zfhs_strdup_error(ret, zser->filename);
        g_string_printf(zser->errorstr, "%s", c);
        g_free(c);
        return ret;
    }else{
        zser->locked = 1;
    }

    zser->fd = open(zser->filename, O_RDWR | O_SYNC | O_NONBLOCK | O_NOCTTY);
    if (zser->fd < 0){
        g_string_printf(zser->errorstr, "Can't open device %s", zser->id);
        zfhs_unlock(zser->filename);
        dbg("zserial_tty_open: can't open %s\n", zser->filename);
        return -3;
    }
    dbg("zserial_tty_open: %s opened fd=%d\n", zser->filename, zser->fd);
    
    if (fcntl(zser->fd, F_SETFL, O_NONBLOCK)) {
        g_string_printf(zser->errorstr, "Can't set O_NONBLOCK on %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zfhs_unlock(zser->filename);
        zserial_close(zser);
        return -4;
    }                                       
    
    if (tcgetattr(zser->fd, &tio) < 0){
        g_string_printf(zser->errorstr, "Can't get device attributes for %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
        return -1;
    }
    cfmakeraw(&tio);

    int b = baud(zser->baudrate);
    cfsetispeed(&tio, b);
    cfsetospeed(&tio, b);
    tio.c_cflag |= (CLOCAL | CREAD);
    tio.c_cflag &= ~CSIZE;
    tio.c_cflag |= cs(zser->bits);
    switch (zser->stopbits){
        case 1:
            tio.c_cflag &= ~CSTOPB;
            break;
        case 2:
            tio.c_cflag |= CSTOPB;
            break;
    }
    switch (zser->parity){
        case 'e':
        case 'E':
            tio.c_cflag |= PARENB;
            tio.c_cflag &= ~PARODD;
            break;
        case 'o':
        case 'O':
            tio.c_cflag |= PARENB;
            tio.c_cflag |= PARODD;
            break;
        case 'n':
        case 'N':
        default:
            tio.c_cflag &= ~PARENB;
            break;

    }

    tio.c_cflag &= ~CRTSCTS;
    tio.c_iflag &= ~IXON;
    tio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    tio.c_oflag &= ~OPOST;
    if (tcflush(zser->fd, TCIFLUSH) < 0){
        g_string_printf(zser->errorstr, "Can't flush device attributes for %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
        return -1;
    }
    if (tcsetattr(zser->fd, TCSANOW, &tio) < 0){
        g_string_printf(zser->errorstr, "Can't set device attributes for %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
        return -1;
    }
    zser->opened = 1;
    return 0;
}

static int zserial_tty_read(struct zserial *zser, void *data, int len, int timeout_ms){
    int ret;
    fd_set fdr;
    struct timeval tv;
            
    FD_ZERO(&fdr);
    FD_SET(zser->fd, &fdr);
    tv.tv_usec = timeout_ms * 1000;
    tv.tv_sec = 0;
    
    ret = select(zser->fd + 1, &fdr, NULL, NULL, &tv);
//        dbg("select=%d\n", ret);
    if (ret < 0) {
        g_string_printf(zser->errorstr, "Can't select %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
        return ret;
    }
    if (ret == 0) {
        g_string_printf(zser->errorstr, "Can't read %s: Timeout", zser->id);
        return 0;
    }
    if (!FD_ISSET(zser->fd, &fdr)){
        g_string_printf(zser->errorstr, "Can't read %s: Data not available", zser->id);
        zserial_close(zser);
    }

    ret = read(zser->fd, data, len);
    if (ret < 0){
        g_string_printf(zser->errorstr, "Can't read from %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
    }
    return ret;
}

static int zserial_tty_write(struct zserial *zser, void *data, int len){
    int ret;

    ret = write(zser->fd, data, len);
    if (ret < 0){
        g_string_printf(zser->errorstr, "Can't write to %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        zserial_close(zser);
    }
    return ret;
}

static int zserial_tty_close(struct zserial *zser){
    if (zser->fd >= 0) {
        close(zser->fd);
        dbg("zserial_tty_close: %s closed fd %d\n", zser->filename, zser->fd);
    }
    zser->fd = -1;

    if (zser->locked) zfhs_unlock(zser->filename);
    zser->locked = 0;
    return 0;
}

static int zserial_tty_dtr(struct zserial *zser, int on){
	int pin;

    pin = TIOCM_DTR;
    if (ioctl(zser->fd, on ? TIOCMBIS : TIOCMBIC, &pin) < 0){
        g_string_printf(zser->errorstr, "Can't set DTR on %s: ", zser->id); 
        z_strerror(zser->errorstr, errno);
        return -1;
    }
    return 0;
}

static int zserial_tty_rts(struct zserial *zser, int on){
    int pin;

    pin = TIOCM_RTS;
    if (ioctl(zser->fd, on ? TIOCMBIS : TIOCMBIC, &pin) < 0){
        g_string_printf(zser->errorstr, "Can't set RTS on %s: ", zser->id);
        z_strerror(zser->errorstr, errno);
        return -1;
    }
    return 0;
}

#endif

int zserial_tty_detect(struct zserial *zser){
    struct dirent *d;
    
    DIR *dp = opendir("/sys/class/tty");
    if (!dp) return 0;
    
    while ((d = readdir(dp)) != NULL){
        if (strcmp(d->d_name, ".") == 0) continue; 
        if (strcmp(d->d_name, "..") == 0) continue; 

        char *device = g_strconcat("/sys/class/tty/", d->d_name, "/device", NULL);
        struct stat st;

        if (stat(device, &st) < 0){
            g_free(device);
            continue;
        }
        g_free(device);

        if (strncmp(d->d_name, "ttyS", 4) == 0 && strncmp(d->d_name, "ttySAC", 6) != 0){
            device = g_strconcat("/sys/class/tty/", d->d_name, "/device/resources", NULL);
            if (stat(device, &st) < 0){
                g_free(device);
                continue;
            }
            g_free(device);
        }

        struct zserial_port *port = g_new0(struct zserial_port, 1);
        port->filename = g_strconcat("/dev/", d->d_name, NULL);
        if (strncmp(d->d_name, "ttySAC", 6) == 0){
            port->desc = g_strdup("Console serial port");

        }else if (strncmp(d->d_name, "ttyS", 4) == 0){
            port->desc = g_strdup("Serial port");

        }else if (strncmp(d->d_name, "ttyUSB", 6) == 0){
            port->desc = g_strdup("USB serial port");

        }else if (strncmp(d->d_name, "ttyGS", 5) == 0){
            port->desc = g_strdup("Gadget serial port");

        }else{
            port->desc = g_strdup("");
        }
        g_ptr_array_add(zser->ports, port);

    }
    closedir(dp);
    return zser->ports->len;
}






struct zserial *zserial_init_tty(const char *filename){
#ifdef Z_HAVE_TERMIOS_H
    struct zserial *zser = zserial_init();
	zser->type = ZSERTYPE_TTY;
	zser->id = g_strdup(filename);
    zser->filename = g_strdup(filename);

    zser->zs_open = zserial_tty_open;
    zser->zs_read = zserial_tty_read;
    zser->zs_write = zserial_tty_write;
    zser->zs_close = zserial_tty_close;
    zser->zs_dtr = zserial_tty_dtr;
    zser->zs_rts = zserial_tty_rts;
    zser->zs_detect = zserial_tty_detect;
    return zser;
#else
	return NULL;
#endif
}


