#include <net.h>
#include <net/serial.h>
#include <net_private.h>
#include <transport.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <errno.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Based on https://en.wikibooks.org/wiki/Serial_Programming/termios */

struct net_host_domain
{
    int fd;
    enum net_role role;
    struct transport_handle h;
    struct net_event ev;
};

int net_write_serial(struct net_host_domain *const h, const net_peer p,
    const void *const buf, const size_t n)
{
    return transport_send(&h->h, buf, n);
}

int net_close_serial(struct net_host_domain *const h)
{
    if (h && h->fd >= 0)
        close(h->fd);

    free(h);
    return 0;
}

int net_update_serial(struct net_host_domain *const h)
{
    return transport_update(&h->h);
}

static void on_received(const struct transport_event *const ev,
    void *const arg)
{
    struct net_host_domain *const h = arg;

    net_serial_on_received(ev, &h->ev);
}

static int on_read(void *const buf, const size_t n, void *const arg)
{
    struct net_host_domain *const h = arg;
    const ssize_t res = read(h->fd, buf, n);

    if (res < 0)
        switch (errno)
        {
            case EAGAIN:
                /* Fall through. */
            case EINTR:
                return 0;

            default:
                return -1;
        }

    return res;
}

static int on_write(const void *const buf, const size_t n, void *const arg)
{
    struct net_host_domain *const h = arg;
    const ssize_t res = write(h->fd, buf, n);

    if (res < 0)
        switch (errno)
        {
            case EAGAIN:
                /* Fall through. */
            case EINTR:
                return 0;

            default:
                return -1;
        }

    return res;
}

static struct transport_cfg get_transport_cfg(void *const arg)
{
    return (const struct transport_cfg)
    {
        .arg = arg,
        .received = on_received,
        .read = on_read,
        .write = on_write
    };
}

static int open_dev(const char *const dev)
{
    return open(dev, O_RDWR | O_NOCTTY | O_NONBLOCK);
}

static int get_speed(const unsigned long baud, speed_t *const out_s)
{
#define BAUD(x) {.baud = x, .s = B##x}

    static const struct speed
    {
        unsigned long baud;
        speed_t s;
    } speeds[] =
    {
        BAUD(19200),
        BAUD(38400),
        BAUD(57600),
        BAUD(115200)
    };

#undef BAUD

    for (size_t i = 0; i < sizeof speeds / sizeof *speeds; i++)
    {
        const struct speed *const s = &speeds[i];

        if (baud == s->baud)
        {
            *out_s = s->s;
            return 0;
        }
    }

    return -1;
}

static int config_fd(const int fd, const unsigned long baud)
{
    struct termios t;
    int ret = -1;

    if (tcgetattr(fd, &t))
    {
        fprintf(stderr, "%s: tcgetattr(3) failed: %s\n", __func__,
            strerror(errno));
        goto end;
    }

    /* cfmakeraw(3) is non-portable, but its man page states the
     * following attributes are set. */
    t.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
        | INLCR | IGNCR | ICRNL | IXON);
    t.c_oflag &= ~OPOST;
    t.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
    t.c_cflag &= ~(CSIZE | PARENB);
    t.c_cflag |= CS8;

    /* Make read(2) immediately regardless how many data is available. */
    t.c_cc[VMIN] = 0;
    t.c_cc[VTIME] = 0;

    speed_t s;

    if (get_speed(baud, &s))
    {
        fprintf(stderr, "%s: get_speed failed\n", __func__);
        goto end;
    }
    else if (cfsetispeed(&t, s))
    {
        fprintf(stderr, "%s: cfsetispeed(3) failed: %s\n", __func__,
            strerror(errno));
        goto end;
    }
    else if (cfsetospeed(&t, s))
    {
        fprintf(stderr, "%s: cfsetospeed(3) failed: %s\n", __func__,
            strerror(errno));
        goto end;
    }
    else if (tcsetattr(fd, TCSANOW, &t))
    {
        fprintf(stderr, "%s: tcsetattr(3) failed: %s\n", __func__,
            strerror(errno));
        goto end;
    }

    ret = 0;

end:
    return ret;
}

struct net_host_domain *net_connect_serial(const union net_connect *const c)
{
    struct net_host_domain *const h = malloc(sizeof *h);
    const struct net_serial_cfg *const cfg = &c->serial.cfg;

    if (!h)
        goto failure;

    *h = (const struct net_host_domain)
    {
        .role = NET_ROLE_CLIENT,
        .fd = open_dev(cfg->dev),
        .h =
        {
            .cfg = get_transport_cfg(h)
        }
    };

    if (h->fd < 0)
    {
        fprintf(stderr, "%s: open(2) failed: %s\n", __func__,
            strerror(errno));
        goto failure;
    }
    else if (config_fd(h->fd, cfg->baud))
    {
        fprintf(stderr, "%s: config_gf failed\n", __func__);
        goto failure;
    }
    else if (transport_connect(&h->h))
    {
        fprintf(stderr, "%s: transport_connect failed\n", __func__);
        goto failure;
    }

    return h;

failure:
    net_close_serial(h);
    return NULL;
}

int net_set_event_serial(struct net_host_domain *const h,
    const struct net_event *const ev)
{
    h->ev = *ev;
    return 0;
}

struct net_host_domain *net_server_serial(const union net_server *const s)
{
    struct net_host_domain *const h = malloc(sizeof *h);
    const struct net_serial_cfg *const cfg = &s->serial.cfg;

    if (!h)
        goto failure;

    *h = (const struct net_host_domain)
    {
        .role = NET_ROLE_SERVER,
        .fd = open_dev(cfg->dev),
        .h =
        {
            .cfg = get_transport_cfg(h)
        }
    };

    if (h->fd < 0)
    {
        fprintf(stderr, "%s: open(2) failed: %s\n", __func__,
            strerror(errno));
        goto failure;
    }
    else if (config_fd(h->fd, cfg->baud))
    {
        fprintf(stderr, "%s: config_gf failed\n", __func__);
        goto failure;
    }

    return h;

failure:
    net_close_serial(h);
    return NULL;
}

enum net_role net_role_serial(const struct net_host_domain *const h)
{
    return h->role;
}

int net_init_serial(void)
{
    return 0;
}

void net_deinit_serial(void)
{
}

bool net_serial_unique(void)
{
    return false;
}
