/* dracal-sensgate-logtool: Inspect a SensGate binary log file or convert it to text (CSV).
 *
 * Copyright (C) 2020-2024  Dracal Technologies Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

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

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


#define VERSION_STRING "dracal-sensgate-logtool " USBTENKI_VERSION

static const char VERSION_TEXT[] =
    "\n"
    VERSION_STRING "\n"
    "Copyright (C) 2020-2025 Dracal Technologies Inc.\n"
    "\n"
    "This software is free software: you can redistribute it and/or modify it under the\n"
    "terms of the GNU General Public License as published by the Free Software Foundation,\n"
    "either version 3 of the License, or (at your option) any later version.\n"
    "\n"
    "This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;\n"
    "without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
    "See the GNU General Public License for more details.\n"
    "\n"
;

static const char HELP_TEXT[] =
    "\n"
    "Usage: dracal-sensgate-logtool [OPTION...] LOGFILE\n"
    "Inspect a SensGate binary log file or convert it to text (CSV).\n"
    "\n"
    "  -h, --help                Print this help screen\n"
    "  -v, --version             Print the version and copyright notice\n"
    "  -l, --list                List log info and channels to standard output\n"
    "  -o, --output=FILE         Output CSV to a file instead of standard output\n"
    "  -r, --rows=N[-[M]]        Output the last N rows, or range of rows N-M (first row is 1)\n"
    "\n"
    "Formatting options for text data:\n"
    "\n"
    "  -T, --temperature=UNIT    Temperature unit. Default: C\n"
    "  -P, --pressure=UNIT       Pressure unit. Default: kPa\n"
    "  -F, --frequency=UNIT      Frequency unit. Default: Hz\n"
    "  -M, --length=UNIT         Temperature unit. Default: m\n"
    "  -C, --concentration=UNIT  Concentration unit. Default: sensor default\n"
    "  -7, --ascii               Only print 7-bit ASCII characters (no Unicode symbols)\n"
    "  -x, --decimals=N          Number of fractional decimal digits to display. Default: automatic\n"
    "  -c, --decimal-comma       Use a comma as the decimal separator. Default: use a period\n"
    "  -s, --separator=STRING    Field separator string to use. Default: \"; \" (semicolon and space)\n"
    "  -e, --error=STRING        Error field value, or \"previous\" to repeat previous. Default: \"error\"\n"
    "  -t, --time=N              Time format; see explanation below\n"
    "\n"
    "LOGFILE is mandatory. Reading from standard input is not supported.\n"
    "\n"
    "The --list option is very fast: only the log header is read and printed to standard output.\n"
    "Data rows are not processed.\n"
    "\n"
    "Without the --list option, the tool will convert LOGFILE to text in CSV format.\n"
    "An output CSV file can be specified with --output, otherwise standard output is used.\n"
    "\n"
    "Temperature units:\n"
    "    Celsius, C, Fahrenheit, F, Kelvin, K\n"
    "\n"
    "Pressure units:\n"
    "    kPa, hPa, Pa, bar, at (98.0665 kPa), atm (101.325 kPa), Torr, psi, inHg\n"
    "\n"
    "Frequency units:\n"
    "    mHz, Hz, kHz, MHz, rpm\n"
    "\n"
    "Length units:\n"
    "    mm, cm, dm, m, mil, in, ft, yd\n"
    "\n"
    "Concentration units:\n"
    "    ppb, ppm, percent\n"
    "\n"
    "Time formats:\n"
    "    0: None (don't print time)\n"
    "    1: Milliseconds since Epoch\n"
    "  * 2: System default (second-precision) * default\n"
    "    3: ISO 8601: Time only: HH:MM:SS\n"
    "    4: ISO 8601: Time only: HH:MM:SS.SSS\n"
    "    5: ISO 8601: Date and time in one field:  YYYY-mm-ddTHH:MM:SS\n"
    "    6: ISO 8601: Date and time in one field:  YYYY-mm-ddTHH:MM:SS.SSS\n"
    "    7: ISO 8601: Date and time in two fields: YYYY-mm-dd ; HH:MM:SS\n"
    "    8: ISO 8601: Date and time in two fields: YYYY-mm-dd ; HH:MM:SS.SSS\n"
    "\n"
;

static const struct option OPTIONS[] = {

    { "help", no_argument, NULL, 'h' },
    { "version", no_argument, NULL, 'v' },
    { "list", no_argument, NULL, 'l' },
    { "output", required_argument, NULL, 'o' },
    { "rows", required_argument, NULL, 'r' },
    { "temperature", required_argument, NULL, 'T' },
    { "pressure", required_argument, NULL, 'P' },
    { "frequency", required_argument, NULL, 'F' },
    { "length", required_argument, NULL, 'M' },
    { "concentration", required_argument, NULL, 'C' },
    { "ascii", no_argument, NULL, '7' },
    { "decimals", required_argument, NULL, 'x' },
    { "decimal-comma", no_argument, NULL, 'c' },
    { "separator", required_argument, NULL, 's' },
    { "error", required_argument, NULL, 'e' },
    { "time", required_argument, NULL, 't' },
    { NULL, 0, NULL, 0 }

};

typedef enum Mode {
    MODE_CONVERT_RANGE,  // print range of rows N-M
    MODE_CONVERT_END,   // print last N rows
    MODE_LIST,           // print header, don't print rows
} Mode;

typedef enum Error {
    SUCCESS = 0,
    ERROR_INVALID_OPTION,
    ERROR_INVALID_ROW_NUMBER,
    ERROR_INVALID_UNIT,
    ERROR_INVALID_DECIMAL_DIGITS,
    ERROR_INVALID_TIME_FORMAT,
    ERROR_MISSING_LOG_FILE,
    ERROR_CANNOT_OPEN_LOG_FILE,
    ERROR_CANNOT_OPEN_OUTPUT_FILE,
    ERROR_PARSING,
} Error;


static const char* const ERRORS[] = {
    [SUCCESS] = "",
    [ERROR_INVALID_OPTION] = "Invalid option",
    [ERROR_INVALID_ROW_NUMBER] = "Invalid row number",
    [ERROR_INVALID_UNIT] = "Invalid measurement unit",
    [ERROR_INVALID_DECIMAL_DIGITS] = "Invalid number of decimal digits",
    [ERROR_INVALID_TIME_FORMAT] = "Invalid time format",
    [ERROR_MISSING_LOG_FILE] = "Missing LOGFILE argument",
    [ERROR_CANNOT_OPEN_LOG_FILE] = "Cannot open log file",
    [ERROR_CANNOT_OPEN_OUTPUT_FILE] = "Cannot open output file",
    [ERROR_PARSING] = "Log file parsing error",
};


static void quit(Error error);
static void parse_unit_argument(unit_category_t cat, const char *arg);
static void parse_rows_argument(char *arg);

static int list();
static int convert();


static Mode cfg_mode = MODE_CONVERT_RANGE;
static char *cfg_log_path = NULL;
static char *cfg_output_path = NULL;
static uint32_t cfg_row_start = 1;  // row indexing starts at 1
static uint32_t cfg_row_end = -1;
static char buf[64];

static CSV_Options cfg_csv_options = {

    .file = NULL,
    .datalog = NULL,

    .separator_str = "; ",
    .error_str = "error",
    .flags = 0,
    .frac_digits = -1,
    .time_format = CSV_TIME_SYSTEM_DEFAULT,

    .units = {
        [UNIT_CATEGORY_UNKNOWN] =  UNIT_SENSOR_DEFAULT,
        [UNIT_CATEGORY_TEMPERATURE] = UNIT_CELSIUS,
        [UNIT_CATEGORY_PRESSURE] = UNIT_KPA,
        [UNIT_CATEGORY_FREQUENCY] = UNIT_HZ,
        [UNIT_CATEGORY_VOLTAGE] =  UNIT_VOLT,
        [UNIT_CATEGORY_CURRENT] = UNIT_AMP,
        [UNIT_CATEGORY_POWER] = UNIT_WATT,
        [UNIT_CATEGORY_LENGTH] = UNIT_METER,
        [UNIT_CATEGORY_CONCENTRATION] = UNIT_SENSOR_DEFAULT,
        [UNIT_CATEGORY_RELATIVE_HUMIDITY] = UNIT_RH,
    }

};


int main(int argc, char *argv[]) {

    // If no argument is given, print help text and exit

    if (argc == 1) {
        fputs(HELP_TEXT, stdout);
        return 1;
    }

    // Parse command-line options

    int c;

    while ((c = getopt_long(argc, argv, "hvlo:r:T:P:F:M:C:7x:cs:e:t:", OPTIONS, NULL)) != -1) {
        switch (c) {
            case 'h':
                fputs(HELP_TEXT, stdout);
                return 0;
            case 'v':
                fputs(VERSION_TEXT, stdout);
                return 0;
            case 'l':
                cfg_mode = MODE_LIST;
                break;
            case 'o':
                cfg_output_path = optarg;
                break;
            case 'r':
                if (cfg_mode != MODE_LIST) {
                    parse_rows_argument(optarg);
                }
                break;
            case 'T':
                parse_unit_argument(UNIT_CATEGORY_TEMPERATURE, optarg);
                break;
            case 'F':
                parse_unit_argument(UNIT_CATEGORY_FREQUENCY, optarg);
                break;
            case 'P':
                parse_unit_argument(UNIT_CATEGORY_PRESSURE, optarg);
                break;
            case 'M':
                parse_unit_argument(UNIT_CATEGORY_LENGTH, optarg);
                break;
            case 'C':
                parse_unit_argument(UNIT_CATEGORY_CONCENTRATION, optarg);
                break;
            case '7':
                cfg_csv_options.flags |= CSV_FLAG_ASCII;
                break;
            case 'x':
                {
                    int a = atoi(optarg);
                    if (a < 0 || a > 9) {
                        quit(ERROR_INVALID_DECIMAL_DIGITS);
                    }
                    cfg_csv_options.frac_digits = a;
                }
                break;
            case 'c':
                cfg_csv_options.flags |= CSV_FLAG_DECIMAL_COMMA;
                break;
            case 's':
                cfg_csv_options.separator_str = optarg;
                break;
            case 'e':
                cfg_csv_options.error_str = strcasecmp(optarg, "previous") ? optarg : NULL;
                break;
            case 't':
                {
                    int a = atoi(optarg);
                    if (a < 0 || a > 8) {
                        quit(ERROR_INVALID_TIME_FORMAT);
                    }
                    cfg_csv_options.time_format = a;
                }
                break;
            default:
                quit(ERROR_INVALID_OPTION);
        }
    }

    // Next argument: LOGFILE
    if (optind >= argc) {
        quit(ERROR_MISSING_LOG_FILE);
    }

    // Open datalog
    cfg_log_path = argv[optind];
    cfg_csv_options.datalog = datalog_open_read(cfg_log_path);
    if (!cfg_csv_options.datalog) {
        quit(ERROR_CANNOT_OPEN_LOG_FILE);
    }

    // We have everything we need for list mode
    if (cfg_mode == MODE_LIST) {
        return list();
    }

    // Open output file for writing, if specified
    if (cfg_output_path) {
        cfg_csv_options.file = fopen(cfg_output_path, "w");
        if (!cfg_csv_options.file) {
            quit(ERROR_CANNOT_OPEN_OUTPUT_FILE);
        }
    }
    else {
        cfg_csv_options.file = stdout;
    }

    return convert();

}

static void quit(Error error) {

    if (error) {
        fprintf(stderr, "ERROR: %s\n", ERRORS[error]);
    }

    exit(error);

}

static void parse_unit_argument(unit_category_t cat, const char *arg) {

    unit_t unit = unit_parse(cat, arg);

    if (unit == UNIT_UNKNOWN) {
        quit(ERROR_INVALID_UNIT);
    }

    cfg_csv_options.units[cat] = unit;

}

static void parse_rows_argument(char *arg) {

    // Argument format: N[-[M]]
    char *p;

    // Find a hyphen or the end of the string
    for (p = arg; *p != '-' && *p != '\0'; p++) ;

    if (*p == '-') {
        cfg_mode = MODE_CONVERT_RANGE;
        *p = '\0';  // split into two tokens
        cfg_row_start = strtoul(arg, NULL, 10);
        if (cfg_row_start == 0) {
            cfg_row_start = 1;
        }
        cfg_row_end = strtoul(p+1, NULL, 10);
        if (cfg_row_end == 0) {
            cfg_row_end = -1;  // open-ended range
        }
    }
    else {
        cfg_mode = MODE_CONVERT_END;
        cfg_row_start = strtoul(arg, NULL, 10);
        cfg_row_end = cfg_row_start;
    }

    if (cfg_row_start > cfg_row_end) {
        quit(ERROR_INVALID_ROW_NUMBER);
    }

}

static int list() {

    DataLog *datalog = cfg_csv_options.datalog;

    time_t s = datalog->creation_timestamp / TIMESTAMP_ONE_SECOND;  // us -> s
    struct tm *tm = localtime(&s);
    strftime(buf, sizeof(buf), "%c", tm);
    double interval = ((double)datalog->interval) / TIMESTAMP_ONE_SECOND;

    printf("File: %s\n", cfg_log_path);
    printf("Created: %s\n", buf);
    printf("Comment: %s\n", datalog->comment);
    printf("Interval: %g s\n", interval);
    printf("Rows: %u\n", datalog_row_count(datalog));

    uint32_t channel_count = datalog->channels.size;  // for portable print format
    printf("Channels: %u\n", channel_count);

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

    LIST_FOR(&datalog->devices) {

        Device *device = LIST_CUR(Device);

        printf("Device: %s, %s, v%u.%u\n",
            device->product_name,
            device->serial_number,
            device->version.major,
            device->version.minor
        );

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

            Channel *channel = LIST_GET(&device->channels, c, Channel);

            char virtual = channel->chip_id >= USBTENKI_VIRTUAL_START;
            char selected_marker = ' ';

            if (scp < scp_end) {
                if (*scp == channel) {
                    selected_marker = '*';
                    scp++;
                }
            }

            printf("  %c %sChannel %u: %s [%s]\n",
                selected_marker,
                virtual ? "Virtual " : "",
                virtual ? channel->chip_id : c,
                chip_description(channel->chip_id),
                chip_description_short(channel->chip_id)
            );

        }

    }

    return 0;

}

static int convert() {

    uint32_t start;
    uint32_t n;

    if (cfg_mode == MODE_CONVERT_RANGE) {
        start = cfg_row_start;
        n = cfg_row_end - start + 1;
    }
    else {
        // last N rows, where N is cfg_row_end
        uint32_t total = datalog_row_count(cfg_csv_options.datalog);
        n = (cfg_row_end < total) ? cfg_row_end : total;
        start = total - n + 1;
    }

    CSV csv = csv_init(&cfg_csv_options);
    csv_convert(csv, start, n, NULL, NULL);
    csv_exit(csv);

    return 0;

}
