#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include "util.h"

#include <skipstone/link.h>
#include <skipstone/message.h>

enum skipstone_link_type {
    SKIPSTONE_WATCH_LINK_SERIAL = 1
};

struct _skipstone_link {
    enum skipstone_link_type type;

    union {
        struct {
            int fd;

            struct termios attr_old,
                           attr_new;
        } serial;
    };
};

skipstone_link *skipstone_link_open_serial(const char *device) {
    skipstone_link *link;

    if ((link = malloc(sizeof(*link))) == NULL) {
        goto error_malloc_link;
    }

    link->type = SKIPSTONE_WATCH_LINK_SERIAL;

    if ((link->serial.fd = open(device, O_RDWR | O_NOCTTY)) < 0) {
        goto error_open;
    }

    if (tcgetattr(link->serial.fd, &link->serial.attr_old) < 0) {
        goto error_io;
    }

    memset(&link->serial.attr_new, 0, sizeof(struct termios));

    link->serial.attr_new.c_cflag     = CS8 | HUPCL;
    link->serial.attr_new.c_ispeed    = B115200;
    link->serial.attr_new.c_ospeed    = B115200;
    link->serial.attr_new.c_iflag     = IGNPAR;
    link->serial.attr_new.c_oflag     = 0;
    link->serial.attr_new.c_lflag     = 0;
    link->serial.attr_new.c_cc[VTIME] = 0;
    link->serial.attr_new.c_cc[VMIN]  = 1;

    if (tcsetattr(link->serial.fd, TCSANOW, &link->serial.attr_new) < 0) {
        goto error_io;
    }

    if (fcntl(link->serial.fd, F_SETFL, 0) < 0) {
        goto error_io;
    }

    return link;

error_io:
    close(link->serial.fd);

error_open:
    free(link);

error_malloc_link:
    return NULL;
}

static int _watch_link_close_serial(skipstone_link *link) {
    if (tcsetattr(link->serial.fd, TCSANOW, &link->serial.attr_old) < 0) {
        goto error_io;
    }

    return close(link->serial.fd);

error_io:
    return -1;
}

int skipstone_link_close(skipstone_link *link) {
    switch (link->type) {
        case SKIPSTONE_WATCH_LINK_SERIAL: {
            return _watch_link_close_serial(link);
        }

        default: break;
    }

    return -1;
}

int skipstone_link_send(skipstone_link *link, void *buf, uint16_t size, uint16_t id) {
    skipstone_message_header header;

    ssize_t wrlen;

     size_t remaining = (size_t)size,
               offset = 0;

    if (size > SKIPSTONE_MESSAGE_MAX_PAYLOAD) {
        goto error_toobig;
    }

    header.size = htobe16(size);
    header.id   = htobe16(id);

    if ((wrlen = write(link->serial.fd, &header, sizeof(header))) < 0) {
        goto error_io;
    }

    while (remaining) {
        if ((wrlen = write(link->serial.fd, (uint8_t *)buf + offset, remaining)) < 0) {
            goto error_io;
        }

        remaining -= (size_t)wrlen;
        offset    += (size_t)wrlen;
    }

    return 0;

error_io:
error_toobig:
    return -1;
}

int skipstone_link_send_message(skipstone_link *link,
        skipstone_message *message, uint16_t id) {
    void *buf;
    uint16_t size = skipstone_message_size(message);

    if ((buf = malloc(size)) == NULL) {
        goto error_malloc_size;
    }

    if (skipstone_message_pack(message, buf) < 0) {
        goto error_message_pack;
    }

    if (skipstone_link_send(link, buf, size, id) < 0) {
        goto error_link_send;
    }

    free(buf);

    return 0;

error_link_send:
error_message_pack:
    free(buf);

error_malloc_size:
    return -1;
}

int skipstone_link_recv(skipstone_link *link, void *buf, uint16_t *size, uint16_t *id) {
    skipstone_message_header header;

    ssize_t len_read;
     size_t len_wanted, offset = 0;

    if (read(link->serial.fd, &header, sizeof(header)) < 0) {
        goto error_io;
    }

    len_wanted = (size_t)be16toh(header.size);

    if (len_wanted > SKIPSTONE_MESSAGE_MAX_PAYLOAD) {
        goto error_io;
    }

    while (len_wanted) {
        if ((len_read = read(link->serial.fd, (uint8_t *)buf + offset, len_wanted)) < 0) {
            goto error_io;
        }

        len_wanted -= len_read;
        offset     += len_read;
    }

    *size = be16toh(header.size);
    *id   = be16toh(header.id);

    return 0;

error_io:
    return -1;
}