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

#include "csv.h"
#include "chip.h"
#include "timestamp.h"


typedef struct Context Context;

typedef void (*print_quantity_fn_t)(Context *context, Quantity *qty);

struct Context {

    CSV_Options opt;

    char buf[64];
    char float_format[8];
    print_quantity_fn_t print_quantity_fns[4];
    Quantity *previous_quantities;

};


static void skip_time_header(Context *context);
static void print_time_header(Context *context);
static void print_float(Context *context, Quantity *qty);
static void print_float_comma(Context *context, Quantity *qty);
static void print_uint32(Context *context, Quantity *qty);
static void print_int32(Context *context, Quantity *qty);
static void print_error(Context *context, Quantity *qty);

static void print_time_none(Context *context, int64_t timestamp);
static void print_time_epoch_ms(Context *context, int64_t timestamp);
static void print_time_system_default(Context *context, int64_t timestamp);
static void print_time_iso_8601_short(Context *context, int64_t timestamp);
static void print_time_iso_8601_long(Context *context, int64_t timestamp);
static void print_time_iso_8601_long_dual(Context *context, int64_t timestamp);

typedef void (*print_time_fn_t)(Context *context, int64_t timestamp);

const print_time_fn_t PRINT_TIME_FUNCTIONS[] = {

    [CSV_TIME_NONE] = print_time_none,
    [CSV_TIME_EPOCH_MS] = print_time_epoch_ms,
    [CSV_TIME_SYSTEM_DEFAULT] = print_time_system_default,
    [CSV_TIME_ISO_8601_SHORT] = print_time_iso_8601_short,
    [CSV_TIME_ISO_8601_SHORT_MS] = print_time_iso_8601_short,
    [CSV_TIME_ISO_8601_LONG] = print_time_iso_8601_long,
    [CSV_TIME_ISO_8601_LONG_MS] = print_time_iso_8601_long,
    [CSV_TIME_ISO_8601_LONG_DUAL] = print_time_iso_8601_long_dual,
    [CSV_TIME_ISO_8601_LONG_MS_DUAL] = print_time_iso_8601_long_dual,

};


CSV csv_init(CSV_Options *opt) {

    Context *context = malloc(sizeof(Context));

    context->opt = *opt;

    if (opt->frac_digits >= 0) {
        sprintf(context->float_format, "%%.%df", opt->frac_digits);
    }
    else {
        context->float_format[0] = '%';
        context->float_format[1] = 'g';
        context->float_format[2] = '\0';
    }

    context->print_quantity_fns[QUANTITY_TYPE_FLOAT] = (opt->flags & CSV_FLAG_DECIMAL_COMMA) ? print_float_comma : print_float;
    context->print_quantity_fns[QUANTITY_TYPE_UINT32] = print_uint32;
    context->print_quantity_fns[QUANTITY_TYPE_INT32] = print_int32;
    context->print_quantity_fns[QUANTITY_TYPE_ERROR] = print_error;

    if (opt->error_str) {
        context->previous_quantities = NULL;
    }
    else {
        const size_t n = opt->datalog->channels.size;
        context->previous_quantities = malloc(n * sizeof(Quantity));
        for (Quantity *q = context->previous_quantities; q < context->previous_quantities + n; q++) {
            q->type = QUANTITY_TYPE_ERROR;
        }
    }

    return (CSV)context;

}

void csv_exit(CSV csv) {

    Context *context = (Context*)csv;

    if (context->previous_quantities) {
        free(context->previous_quantities);
    }

    free(context);

}

void csv_write_header(CSV csv) {

    Context *context = (Context*) csv;
    FILE *f = context->opt.file;
    DataLog *datalog = context->opt.datalog;

    if (f != stdout) {
        fputs("\xef\xbb\xbf", f); // UTF-8 BOM
    }
    fputs("# Created: ", f);
    print_time_system_default(context, datalog->creation_timestamp);
    fputs("\n", f);

    fprintf(f, "# Comment: %s\n", datalog->comment);

    double interval = ((double)datalog->interval) / TIMESTAMP_ONE_SECOND;
    fprintf(f, "# Interval: %g s\n", interval);

    fputs("\n", f);

    void **scp;
    void **scp_end;

    // Build a list of selected devices, aligned with selected channels

    LIST(selected_devices);
    list_grow(&selected_devices, datalog->channels.size);

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

    LIST_FOR(&datalog->devices) {

        Device *device = LIST_CUR(Device);

        LIST_FOR(&device->channels) {

            Channel *channel = LIST_CUR(Channel);
            if (*scp != channel) {
                continue;
            }

            list_add(&selected_devices, device);

            scp++;
            if (scp >= scp_end) {
                break;
            }

        }

        if (scp >= scp_end) {
            break;
        }

    }

    // Device name

    print_time_header(context);

    LIST_FOR(&selected_devices) {
        Device *device = LIST_CUR(Device);
        fputs(context->opt.separator_str, f);
        fputs(device->product_name, f);
    }

    fputs("\n", f);

    // Channel ID

    skip_time_header(context);

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

    LIST_FOR(&selected_devices) {

        Device *device = LIST_CUR(Device);

        for (int c = 0; c < device->channels.size && scp < scp_end; c++) {

            Channel *channel = LIST_GET(&device->channels, c, Channel);
            if (*scp != channel) {
                continue;
            }

            char virtual = channel->chip_id >= USBTENKI_VIRTUAL_START;

            fprintf(
                f,
                "%s%s:%02u",
                context->opt.separator_str,
                device->serial_number,
                virtual ? channel->chip_id : c
            );

            scp++;

        }

        if (scp >= scp_end) {
            break;
        }

    }

    fputs("\n", f);

    // Channel description

    skip_time_header(context);

    LIST_FOR(&datalog->channels) {
        Channel *channel = LIST_CUR(Channel);
        fprintf(
            f,
            "%s%s [%s]",
            context->opt.separator_str,
            chip_description(channel->chip_id),
            chip_description_short(channel->chip_id)
        );
    }

    fputs("\n", f);

    // Unit

    skip_time_header(context);

    LIST_FOR(&datalog->channels) {
        Channel *channel = LIST_CUR(Channel);
        unit_t chip_unit = chip_native_unit(channel->chip_id);
        unit_t pref_unit = context->opt.units[unit_category(chip_unit)];
        unit_t unit = (pref_unit == UNIT_SENSOR_DEFAULT) ? chip_unit : pref_unit;
        fputs(context->opt.separator_str, f);
        fputs(unit_to_string(unit, context->opt.flags & CSV_FLAG_ASCII), f);
    }

    fputs("\n", f);

    // Cleanup
    list_clear(&selected_devices);

}

void csv_write_header_custom(CSV csv, List *channel_headers) {

    Context *context = (Context*)csv;
    FILE *f = context->opt.file;
    DataLog *datalog = context->opt.datalog;

    fputs("\xef\xbb\xbf", f); // UTF-8 BOM
    fputs("# Created: ", f);
    print_time_system_default(context, datalog->creation_timestamp);
    fputs("\n", f);

    fprintf(f, "# Interval: %u.%06u s\n", datalog->interval / TIMESTAMP_ONE_SECOND, datalog->interval % TIMESTAMP_ONE_SECOND);
    fprintf(f, "# Comment: %s\n", datalog->comment);
    fputs("\n", f);

    // Device name

    print_time_header(context);

    LIST_FOR(channel_headers) {
        CSV_Channel_Header *ch = LIST_CUR(CSV_Channel_Header);
        fputs(context->opt.separator_str, f);
        fputs(ch->name, f);
    }

    fputs("\n", f);

    // Channel ID

    skip_time_header(context);

        LIST_FOR(channel_headers) {
        CSV_Channel_Header *ch = LIST_CUR(CSV_Channel_Header);
        fputs(context->opt.separator_str, f);
        fputs(ch->id, f);
    }

    fputs("\n", f);

    // Channel description

    skip_time_header(context);

    LIST_FOR(channel_headers) {
        CSV_Channel_Header *ch = LIST_CUR(CSV_Channel_Header);
        fputs(context->opt.separator_str, f);
        fputs(ch->description, f);
    }

    fputs("\n", f);

    // Unit

    skip_time_header(context);

    LIST_FOR(channel_headers) {
        CSV_Channel_Header *ch = LIST_CUR(CSV_Channel_Header);
        fputs(context->opt.separator_str, f);
        fputs(unit_to_string(ch->unit, context->opt.flags & CSV_FLAG_ASCII), f);
    }

    fputs("\n", f);

}

void csv_write_row(CSV csv) {

    Context *context = (Context*)csv;
    FILE *f = context->opt.file;
    DataLog *datalog = context->opt.datalog;
    const char *separator_str = context->opt.separator_str;

    print_time_fn_t print_time_fn = PRINT_TIME_FUNCTIONS[context->opt.time_format];
    print_time_fn(context, datalog->timestamp);

    List *channels = &datalog->channels;

    if (context->opt.error_str) {

        LIST_FOR(channels) {

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

            // Convert units
            unit_category_t cat = unit_category(qty->unit);
            quantity_convert_to_unit(qty, context->opt.units[cat]);

            // Print format is based on type
            fputs(separator_str, f);
            context->print_quantity_fns[qty->type](context, qty);

        }

    }
    else {

        Quantity *p = context->previous_quantities;

        LIST_FOR(channels) {

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

            fputs(separator_str, f);

            if (qty->type != QUANTITY_TYPE_ERROR) {

                // Convert units
                unit_category_t cat = unit_category(qty->unit);
                quantity_convert_to_unit(qty, context->opt.units[cat]);

                // Print format is based on type
                context->print_quantity_fns[qty->type](context, qty);

                // Store quantity for later use
                *p = *qty;

            }
            else if (p->type != QUANTITY_TYPE_ERROR) {
                // Print format is based on type
                context->print_quantity_fns[p->type](context, p);
            }

            p++;

        }

    }

    fputs("\n", f);

}

void csv_convert(CSV csv, uint32_t start, uint32_t n, progress_fn_t progress_fn, void *user_data) {

    Context *context = (Context*)csv;
    DataLog *datalog = context->opt.datalog;

    csv_write_header(csv);

    if (datalog_seek_row(datalog, start)) {
        return;
    }

    // Total number of rows in datalog
    const uint32_t N = datalog_row_count(datalog);

    // We know start is in range [1, N] since indexing starts at 1
    // Compute how many rows we can read until the end
    const uint32_t len = N - start + 1;

    // How many rows are we actually going to read?
    if (len < n) {
        n = len;
    }

    // How often to call the progress function?
    const uint32_t progress_step = (n / 50) + 1;

    // Fetch everything else we need to print
    FILE *f = context->opt.file;
    const char *separator_str = context->opt.separator_str;
    print_time_fn_t print_time_fn = PRINT_TIME_FUNCTIONS[context->opt.time_format];
    List *channels = &datalog->channels;

    // Print rows

    for (uint32_t i = 0; i < n; i++) {

        if (progress_fn && (i % progress_step == 0)) {
            progress_fn(i, n, user_data);
        }

        if (datalog_read_row(datalog)) {
            break;
        }

        print_time_fn(context, datalog->timestamp);

        if (context->opt.error_str) {

            LIST_FOR(channels) {

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

                // Convert units
                unit_category_t cat = unit_category(qty->unit);
                quantity_convert_to_unit(qty, context->opt.units[cat]);

                // Print format is based on type
                fputs(separator_str, f);
                context->print_quantity_fns[qty->type](context, qty);

            }

        }
        else {

            Quantity *p = context->previous_quantities;

            LIST_FOR(channels) {

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

                fputs(separator_str, f);

                if (qty->type != QUANTITY_TYPE_ERROR) {

                    // Convert units
                    unit_category_t cat = unit_category(qty->unit);
                    quantity_convert_to_unit(qty, context->opt.units[cat]);

                    // Print format is based on type
                    context->print_quantity_fns[qty->type](context, qty);

                    // Store quantity for later use
                    *p = *qty;

                }
                else if (p->type != QUANTITY_TYPE_ERROR) {
                    // Print format is based on type
                    context->print_quantity_fns[p->type](context, p);
                }

                p++;

            }

        }

        fputs("\n", f);

    }

    if (progress_fn) {
        progress_fn(n, n, user_data);
    }

}

static void skip_time_header(Context *context) {

    switch(context->opt.time_format) {

        case CSV_TIME_ISO_8601_LONG_DUAL:
        case CSV_TIME_ISO_8601_LONG_MS_DUAL:
            fputs(context->opt.separator_str, context->opt.file);
            break;

    }

}

static void print_time_header(Context *context) {

    FILE *f = context->opt.file;

    switch(context->opt.time_format) {

        case CSV_TIME_EPOCH_MS:
            fputs("Timestamp", f);
            break;

        case CSV_TIME_SYSTEM_DEFAULT:
            fputs("Date", f);
            fputs("Time", f);
            break;

        case CSV_TIME_ISO_8601_SHORT:
        case CSV_TIME_ISO_8601_SHORT_MS:
            fputs("Time", f);
            break;

        case CSV_TIME_ISO_8601_LONG:
        case CSV_TIME_ISO_8601_LONG_MS:
            fputs("Date", f);
            fputs("Time", f);
            break;

        case CSV_TIME_ISO_8601_LONG_DUAL:
        case CSV_TIME_ISO_8601_LONG_MS_DUAL:
            fputs("Date", f);
            fputs(context->opt.separator_str, f);
            fputs("Time", f);
            break;

    }

}

static void print_float(Context *context, Quantity *qty) {

    fprintf(context->opt.file, context->float_format, qty->value_float);

}

static void print_float_comma(Context *context, Quantity *qty) {

    sprintf(context->buf, context->float_format, qty->value_float);

    for (char *p = context->buf; *p != '\0'; p++) {
        if (*p == '.') {
            *p = ',';
            break;
        }
    }

    fputs(context->buf, context->opt.file);

}

static void print_uint32(Context *context, Quantity *qty) {

    fprintf(context->opt.file, "%u", qty->value_uint32);

}

static void print_int32(Context *context, Quantity *qty) {

    fprintf(context->opt.file, "%d", qty->value_int32);

}

static void print_error(Context *context, Quantity *qty) {

    fputs(context->opt.error_str, context->opt.file);

}

static void print_time_none(Context *context, int64_t timestamp) {
    // do nothing
}

static void print_time_epoch_ms(Context *context, int64_t timestamp) {

    fprintf(
        context->opt.file,
        #if __SIZEOF_LONG__ == 8 && !defined __APPLE__
            "%ld",
        #else
            #ifdef _WIN32
                "%I64d",
            #else
                "%lld",
            #endif
        #endif
        timestamp_to_ms(timestamp)
    );

}

static void print_time_system_default(Context *context, int64_t timestamp) {

    time_t s = timestamp / TIMESTAMP_ONE_SECOND;  // us -> s
    struct tm *tm = localtime(&s);

    strftime(context->buf, sizeof(context->buf), "%c", tm);

    fputs(context->buf, context->opt.file);

}

static void print_time_iso_8601_short(Context *context, int64_t timestamp) {

    time_t s = timestamp / TIMESTAMP_ONE_SECOND;  // us -> s
    struct tm *tm = localtime(&s);

    size_t n = strftime(context->buf, sizeof(context->buf), "%H:%M:%S", tm);

    if (context->opt.time_format == CSV_TIME_ISO_8601_SHORT_MS) {
        unsigned int us = timestamp % TIMESTAMP_ONE_SECOND;  // us remainder
        snprintf(context->buf + n, sizeof(context->buf) - n, ".%03u", timestamp_to_ms(us));
    }

    fputs(context->buf, context->opt.file);

}

static void print_time_iso_8601_long(Context *context, int64_t timestamp) {

    time_t s = timestamp / TIMESTAMP_ONE_SECOND;  // us -> s
    struct tm *tm = localtime(&s);

    size_t n = strftime(context->buf, sizeof(context->buf), "%Y-%m-%dT%H:%M:%S", tm);

    if (context->opt.time_format == CSV_TIME_ISO_8601_LONG_MS) {
        unsigned int us = timestamp % TIMESTAMP_ONE_SECOND;  // us remainder
        snprintf(context->buf + n, sizeof(context->buf) - n, ".%03u", timestamp_to_ms(us));
    }

    fputs(context->buf, context->opt.file);

}

static void print_time_iso_8601_long_dual(Context *context, int64_t timestamp) {

    time_t s = timestamp / TIMESTAMP_ONE_SECOND;  // us -> s
    struct tm *tm = localtime(&s);

    size_t n;
    n  = strftime(context->buf, sizeof(context->buf), "%Y-%m-%d", tm);
    n += snprintf(context->buf + n, sizeof(context->buf) - n, "%s", context->opt.separator_str);
    n += strftime(context->buf + n, sizeof(context->buf) - n, "%H:%M:%S", tm);

    if (context->opt.time_format == CSV_TIME_ISO_8601_LONG_MS_DUAL) {
        unsigned int us = timestamp % TIMESTAMP_ONE_SECOND;  // us remainder
        snprintf(context->buf + n, sizeof(context->buf) - n, ".%03u", timestamp_to_ms(us));
    }

    fputs(context->buf, context->opt.file);

}
