#include <stdlib.h>
#include <string.h>

#ifndef _WIN32
#include <unistd.h>
#endif

#include "datalog.h"
#include "buffer.h"
#include "chip.h"
#include "portable_endian.h"

typedef struct Context {

    FILE *file;
    int fd;
    Buffer buffer;

    uint32_t row_size;
    uint32_t row_count;
    uint32_t row_index;

    long first_row_offset;

    char datalog_ownership;

} Context;


static const char MAGIC[] = { 0x28, 0x9b };

#define VERSION 1


static Context *context_open(const char *path, const char *mode) {

    FILE *f = fopen(path, mode);

    if (f == NULL) {
        return NULL;
    }

    Context *context = calloc(1, sizeof(Context));

    context->file = f;
    context->fd = fileno(f);
    buffer_grow(&context->buffer, 1024);

    return context;

}

static int datalog_write(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    Buffer *buf = &context->buffer;
    char *p = buf->array + buf->start;
    char *end = p + buf->end;

    while (p < end) {

        size_t n = fwrite(p, 1, end - p, context->file);

        if (n == 0) {  // error
            datalog_close(datalog);
            return 1;
        }

        p += n;

    }

    // Sync to disk immediately (robustness against sudden power loss)
    #ifdef _WIN32
        if (fflush(context->file)) {
            datalog_close(datalog);
            return 1;
        }
    #else
        if (fflush(context->file) || fsync(context->fd)) {
            datalog_close(datalog);
            return 1;
        }
    #endif

    buffer_reset(buf);

    return 0;  // success

}

#define WRITE(datalog) { \
    int _error_ = datalog_write(datalog); \
    if (_error_) { \
        return _error_; \
    } \
}

static int datalog_read(DataLog *datalog, size_t len) {

    Context *context = (Context*) datalog->_handle;
    Buffer *buf = &context->buffer;

    char *p = buf->array;
    char *end = p + len;

    while (p < end) {

        size_t n = fread(p, 1, end - p, context->file);

        if (n == 0) {
            datalog_close(datalog);
            return 1;  // error or EOF
        }

        p += n;

    }

    buf->start = 0;
    buf->end = len;

    return 0;

}

#define READ(datalog, len) { \
    int _error_ = datalog_read(datalog, len); \
    if (_error_) { \
        return _error_; \
    } \
}

static int datalog_write_header(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    Buffer *buf = &context->buffer;
    buffer_reset(buf);

    // Magic number
    buffer_put(buf, MAGIC, sizeof(MAGIC));

    // Version
    buffer_put_char(buf, VERSION);

    // Creation timestamp
    int64_t creation_timestamp = htobe64(datalog->creation_timestamp);
    buffer_put(buf, &creation_timestamp, sizeof(creation_timestamp));

    // Interval
    uint32_t interval = htobe32(datalog->interval);
    buffer_put(buf, &interval, sizeof(interval));

    // Comment
    if (datalog->comment) {
        size_t comment_len = strlen(datalog->comment);
        uint16_t n = htobe16((uint16_t)comment_len);
        buffer_put(buf, &n, sizeof(n));
        buffer_reserve(buf, comment_len);
        buffer_put(buf, datalog->comment, comment_len);
    }
    else {
        buffer_put_char(buf, 0);
        buffer_put_char(buf, 0);
    }

    WRITE(datalog);

    // Device list

    uint8_t device_count = datalog->devices.size;
    buffer_put_char(buf, device_count);

    void **scp = datalog->channels.elements;
    void **scp_end = scp + datalog->channels.size;

    LIST_FOR(&datalog->devices) {

        Device *device = LIST_CUR(Device);

        buffer_put(buf, device->serial_number, DEVICE_SERIAL_NUMBER_LEN);
        size_t name_len = strlen(device->product_name);
        buffer_put_char(buf, (uint8_t)name_len);
        buffer_put(buf, device->product_name, name_len);
        buffer_put_char(buf, device->version.major);
        buffer_put_char(buf, device->version.minor);
        buffer_put_char(buf, (uint8_t)device->channels.size);

        LIST_FOR(&device->channels) {

            Channel *channel = LIST_CUR(Channel);
            uint16_t chip_id = htobe16(channel->chip_id);
            buffer_put(buf, &chip_id, sizeof(chip_id));

            uint8_t selected = 0;

            if (scp < scp_end) {
                Channel *sc = (Channel*)(*scp);
                if (sc == channel) {
                    selected = 1;
                    scp++;
                }
            }

            buffer_put_char(buf, selected);

        }

        WRITE(datalog);

    }

    uint32_t row_size = 8 + (6 * datalog->channels.size);
    buffer_grow(buf, row_size);
    context->row_size = row_size;
    context->first_row_offset = ftell(context->file);

    return 0;

}

static int datalog_read_header(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    Buffer *buf = &context->buffer;

    READ(datalog, 17);

    // Magic number
    if (memcmp(MAGIC, buf->array, 2)) {
        return 2;  // mismatch
    }

    // Version
    if (buf->array[2] != VERSION) {
        return 2;  // incompatible version
    }

    buffer_skip(buf, 3);

    // Creation timestamp
    int64_t creation_timestamp;
    buffer_get(buf, &creation_timestamp, sizeof(creation_timestamp));
    creation_timestamp = (int64_t) be64toh((uint64_t)creation_timestamp);
    datalog->creation_timestamp = creation_timestamp;

    // Interval
    uint32_t interval;
    buffer_get(buf, &interval, sizeof(interval));
    interval = be32toh(interval);
    datalog->interval = interval;

    // Comment length
    uint16_t comment_len;
    buffer_get(buf, &comment_len, sizeof(comment_len));
    comment_len = be16toh(comment_len);

    // Comment
    datalog->comment = malloc(comment_len + 1);
    if (comment_len > 0) {
        buffer_grow(buf, comment_len);
        READ(datalog, comment_len);
        comment_len = buffer_get(buf, datalog->comment, comment_len);
    }
    datalog->comment[comment_len] = '\0';

    // Device list
    READ(datalog, 1);
    int device_count = buffer_get_char(buf);
    list_grow(&datalog->devices, device_count);

    for (int i = 0; i < device_count; i++) {

        Device *device = device_new();
        list_add(&datalog->devices, device);

        READ(datalog, DEVICE_SERIAL_NUMBER_LEN + 1);
        buffer_get(buf, device->serial_number, DEVICE_SERIAL_NUMBER_LEN);
        size_t name_len = buffer_get_char(buf);

        READ(datalog, name_len + 3);
        buffer_get(buf, device->product_name, name_len);
        device->version.major = buffer_get_char(buf);
        device->version.minor = buffer_get_char(buf);
        size_t channel_count = buffer_get_char(buf);

        READ(datalog, channel_count * 3);
        device_add_channels(device, channel_count);

        LIST_FOR(&device->channels) {

            Channel *channel = LIST_CUR(Channel);

            uint16_t chip_id;
            buffer_get(buf, &chip_id, sizeof(chip_id));
            chip_id = be16toh(chip_id);
            channel->chip_id = chip_id;

            channel->quantity = QUANTITY_NO_DATA;

            uint8_t selected = buffer_get_char(buf);
            if (selected) {
                list_add(&datalog->channels, channel);
            }

        }

    }

    uint32_t row_size = 8 + (6 * datalog->channels.size);
    buffer_grow(buf, row_size);
    context->row_size = row_size;
    context->first_row_offset = ftell(context->file);

    return 0;

}

int datalog_create(const char *path, DataLog *datalog) {

    Context *context = context_open(path, "wb");

    if (!context) {
        return 1;
    }

    datalog->_handle = context;
    context->datalog_ownership = 0;

    if (datalog_write_header(datalog)) {
        return 1;
    }

    return 0;

}

static DataLog *datalog_open(const char *path, const char *mode) {

    Context *context = context_open(path, mode);

    if (!context) {
        return NULL;
    }

    DataLog *datalog = calloc(1, sizeof(DataLog));
    datalog->_handle = context;
    context->datalog_ownership = 1;

    if (datalog_read_header(datalog)) {
        return NULL;
    }

    // Determine the row count

    fseek(context->file, 0, SEEK_END);
    long end_offset = ftell(context->file);
    long size = end_offset - context->first_row_offset;
    context->row_count = size / context->row_size;
    long leftover = size % context->row_size;

    if (leftover) {
        // There was truncated data; discard it
        fseek(context->file, -leftover, SEEK_END);
    }

    context->row_index = context->row_count;

    return datalog;

}

DataLog *datalog_open_read(const char *path) {

    DataLog *datalog = datalog_open(path, "rb");

    if (!datalog) {
        return NULL;
    }

    Context *context = (Context*) datalog->_handle;

    int error = fseek(context->file, context->first_row_offset, SEEK_SET);
    if (error) {
        datalog_close(datalog);
        return NULL;
    }

    context->row_index = 0;

    return datalog;

}

DataLog *datalog_open_append(const char *path) {

    return datalog_open(path, "rb+");

}

int datalog_read_row(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;

    if (context->row_index >= context->row_count) {
        return 1;
    }

    READ(datalog, context->row_size);

    context->row_index++;

    Buffer *buf = &context->buffer;
    char *p = buf->array;

    uint64_t timestamp;
    memcpy(&timestamp, p, sizeof(timestamp));
    datalog->timestamp = (int64_t) be64toh(timestamp);
    p += sizeof(timestamp);

    LIST_FOR(&datalog->channels) {

        Channel *channel = LIST_CUR(Channel);
        Quantity *q = &channel->quantity;

        q->unit = *p++;
        q->type = *p++;

        uint32_t value;
        memcpy(&value, p, sizeof(value));
        value = be32toh(value);
        q->value_uint32 = value;
        p += sizeof(value);

    }

    return 0;

}

int datalog_append_row(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    Buffer *buf = &context->buffer;
    buffer_reset(buf);

    uint64_t timestamp = htobe64((uint64_t)datalog->timestamp);
    buffer_put(buf, &timestamp, sizeof(timestamp));

    LIST_FOR(&datalog->channels) {
        Channel *channel = LIST_CUR(Channel);
        Quantity *q = &channel->quantity;
        buffer_put_char(buf, q->unit);
        buffer_put_char(buf, q->type);
        uint32_t value = htobe32(q->value_uint32);
        buffer_put(buf, &value, sizeof(value));
    }

    WRITE(datalog);

    context->row_count++;
    context->row_index++;

    return 0;

}

uint32_t datalog_row_size(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    return context->row_size;

}

uint32_t datalog_row_count(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    return context->row_count;

}

uint32_t datalog_row_index(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;
    return context->row_index;

}

int datalog_seek_row(DataLog *datalog, uint32_t index) {

    Context *context = (Context*) datalog->_handle;

    if (index > context->row_count) {
        index = context->row_count;
    }
    else if (index > 0) {
        index--;
    }

    long offset = context->first_row_offset + (context->row_size * index);

    int error = fseek(context->file, offset, SEEK_SET);
    if (error) {
        return error;
    }

    context->row_index = index;

    return 0;

}

void datalog_close(DataLog *datalog) {

    Context *context = (Context*) datalog->_handle;

    fclose(context->file);
    buffer_clear(&context->buffer);

    if (context->datalog_ownership) {

        free(datalog->comment);

        LIST_FOR(&datalog->devices) {
            Device *device = LIST_CUR(Device);
            device_delete(device);
        }

        list_clear(&datalog->devices);
        list_clear(&datalog->channels);

        free(datalog);

    }

    free(context);

}
