/* dracal-sensgate-get: Connect to a SensGate via TCP/IP and log data from sensor devices.
 *
 * 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 <getopt.h>
#include <ctype.h>

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

#include "tenkinet.h"
#include "discovery.h"
#include "datalog.h"
#include "csv.h"
#include "chip.h"
#include "source.h"
#include "timestamp.h"
#include "str_helper.h"
#include "heap.h"
#include "usbtenki_version.h"


#define VERSION_STRING "dracal-sensgate-get " USBTENKI_VERSION

#define INTERVAL_DEFAULT    TIMESTAMP_ONE_SECOND
#define INTERVAL_MIN        (TIMESTAMP_ONE_SECOND / 10)

#define DEFAULT_CHANNEL_ID		0
#define DEFAULT_LOG_INTERVAL	1000
#define DEFAULT_DECIMAL_DIGITS	2


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[] =
	"Usage: ./dracal-sensgate-get [arguments]\n"
	"\n"
    "Valid arguments:\n"
	"    -V           Display version information\n"
	"    -v           Verbose mode\n"
	"    -h           Displays help\n"
	"    -l           List and display info about available sensors\n"
    "    -H host      Host address[:port], name or serial number of SensGate (see Examples)\n"
	"    -s serial    Use sensor with matching serial number. Default: Use first found\n"
	"    -i id<,id,id...>  Use specific channel(s) id(s) or 'a' for all. Default: " STR(DEFAULT_CHANNEL_ID) "\n"
	"    -x digits    Set number of fractional digits [0-6]. Default: " STR(DEFAULT_DECIMAL_DIGITS) "\n"
	"    -R retry     Number of extra sensgate discovery messages to send. Default: 0\n"
	"    -T unit      Select the temperature unit to use. Default: Celsius\n"
	"    -P unit      Select the pressure unit to use. Default: kPa\n"
	"    -F unit      Select the frequency unit to use. Default: Hz\n"
	"    -M unit      Select the length unit to use. Default: m\n"
	"    -C unit      Select the concentration unit to use. Default: Sensor default\n"
	"    -p           Enable pretty output\n"
	"    -7           Use 7-bit ASCII output (no Unicode degree symbols)\n"
	"    -u           Print uncalibrated values, i.e. do not apply user calibration configured by usbtenkical\n"
	"    -L logfile   Log to specified file (use - for console)\n"
	"    -I interval  Log interval. In milliseconds. Default: " STR(DEFAULT_LOG_INTERVAL) "\n"
    "    -r rows      Number of log rows. Default: 0, i.e. run continuously\n"
	"    -S value     Set standard sea level pressure (Pascals) used to compute altitude. Default: 101325\n"
	"    -o option    Enable specified option (see below). You may use -o multiple times.\n"
    "\n"
	"Options:\n"
	"    no_humidex_range     Calculate humidex even if input values are out of range.\n"
	"    no_heat_index_range  Calculate heat index even if the input values are out of range.\n"
    "\n"
	"Valid temperature units:\n"
	"    Celsius, C, Fahrenheit, F, Kelvin, K\n"
	"\nValid pressure units:\n"
	"    kPa, hPa, Pa, bar, at (98.0665 kPa), atm (101.325 kPa), Torr, psi, inHg\n"
	"\nValid frequency units:\n"
	"    mHz, Hz, kHz, MHz, rpm\n"
	"\nValid length units:\n"
	"    mm, cm, dm, m, mil, in, ft, yd\n"
	"\nValid concentration units:\n"
	"    ppb, ppm, percent\n"
    "\n"
	"Errors:\n"
    "\n"
	"When an error occurs reading a channel, the value is replaced by an error string:\n"
	"    Undefined            Unknown/undefined error.\n"
	"    Saturated            Sensor (or resulting value) is saturated and unusable.\n"
	"    SensorError          The physical sensor or interface circuitry is not working properly\n"
	"    ProbeDisconnected    Indicates that the probe is disconnected or cable is cut/open\n"
	"    OutOfRange           The reading falls outside the sensor possible or supported range\n"
	"    InvalidData          The data received from the sensor did not make sense or was incomplete\n"
	"\n"
    "Examples:\n"
    "\n"
    "1. Access all data from sensor with serial number E22826 on SensGate at address 192.168.20.167:10395\n"
    "    dracal-sensgate-get -H 192.168.20.167:10395 -s E22826\n"
    "\n"
    "2. Access all data from sensor with serial number E22826 on SensGate with serial number G00002\n"
    "    dracal-sensgate-get -H G00002 -s E22826\n"
    "\n"
    "3. Access all data from sensor with serial number E22826, finding SensGate automatically\n"
    "    dracal-sensgate-get -s E22826\n"
    "\n"
	"Return value:\n"
	" - On success, dracal-sensgate-get returns 0.\n"
	" - If the requested serial number (see -s) was not found, or if no devices were found (-f and -l) a non-zero value is returned.\n"
;


static const struct option OPTIONS[] = {

    { "version", no_argument, NULL, 'V' },
    { "verbose", no_argument, NULL, 'v'},
    { "help", no_argument, NULL, 'h' },
    { "list", no_argument, NULL, 'l' },
    { "first", no_argument, NULL, 'f'},
    { "pretty", no_argument, NULL, 'p'},
    { "uncalibrated", no_argument, NULL, 'u'},
    { "ascii", no_argument, NULL, '7' },
    { "host", required_argument, NULL, 'H' },
    { "serial", required_argument, NULL, 's' },
    { "index", required_argument, NULL, 'i'},
    { "retry", 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' },
    { "log", required_argument, NULL, 'L' },
    { "rows", required_argument, NULL, 'r'},
    { "interval", required_argument, NULL, 'I' },
    { "digits", required_argument, NULL, 'x'},
    { "slp", required_argument, NULL, 'S' },
    { "option", required_argument, NULL, 'o' },
    { NULL, 0, NULL, 0 }
};

static char OPTIONS_SHORT[] = "Vvhlfpu7H:s:i:R:T:P:F:M:C:L:I:r:x:S:o:";

typedef enum Mode {

    MODE_SAMPLE = 0,
    MODE_LOG,
    MODE_LIST

} Mode;


typedef enum Error {  // TODO use error codes from tenkinet module instead

    SUCCESS = 0,
    ERROR_INVALID_OPTION,
    ERROR_HOST_NOT_FOUND,
    ERROR_INVALID_PORT,
    ERROR_EMPTY_SERIAL_NUMBER,
    ERROR_INVALID_CHANNEL_INDEX,
    ERROR_INIT,
    ERROR_NO_SERVER,
    ERROR_PROTOCOL_VERSION,
    ERROR_CONNECTION,
    ERROR_SELECT,
    ERROR_DISCONNECTED,
    ERROR_DEVICE_NOT_FOUND,
    ERROR_CANNOT_OPEN_LOG_FILE,
    ERROR_INVALID_NUM_ROWS,
    ERROR_INVALID_UNIT,
    ERROR_INVALID_DECIMAL_DIGITS,
    ERROR_INVALID_RETRY_VALUE,
    DONE,  // not an error; indicates that we should disconnect from server and exit cleanly

} Error;


static const char* const ERRORS[] = {
    [SUCCESS] = "",
    [ERROR_INVALID_OPTION] = "Invalid option",
    [ERROR_HOST_NOT_FOUND] = "HOST not found",
    [ERROR_INVALID_PORT] = "Invalid PORT argument",
    [ERROR_EMPTY_SERIAL_NUMBER] = "Empty DEVICE argument",
    [ERROR_INVALID_CHANNEL_INDEX] = "Invalid CHANNEL argument: must be a non-negative integer",
    [ERROR_INIT] = "Cannot initialize tenkinet module",
    [ERROR_NO_SERVER] = "No suitable server found",
    [ERROR_PROTOCOL_VERSION] = "Incompatible server version",
    [ERROR_CONNECTION] = "Could not connect to server",
    [ERROR_SELECT] = "Error in select()",
    [ERROR_DISCONNECTED] = "Disconnected from server",
    [ERROR_DEVICE_NOT_FOUND] = "Device not found",
    [ERROR_CANNOT_OPEN_LOG_FILE] = "Cannot open log FILE specified with -L",
    [ERROR_INVALID_NUM_ROWS] = "Invalid number of rows",
    [ERROR_INVALID_UNIT] = "Invalid measurement unit",
    [ERROR_INVALID_DECIMAL_DIGITS] = "Invalid number of decimal digits",
    [ERROR_INVALID_RETRY_VALUE] = "Invalid retry value",
};


typedef struct Subscription {

    Source *source;
    char serial_number[DEVICE_SERIAL_NUMBER_LEN + 1];
    List channel_indices;
    List channels;

} Subscription;


static Error error = SUCCESS;
static LIST(servers);
static LIST(clients);
static LIST(subscriptions);
static LIST(devices);

static select_helper_data seldata;

// Configuration parsed from command-line arguments
static char *opt_host_id = NULL;
static char opt_host_addr[INET_ADDRSTRLEN] = "";
static char opt_host_port[6] = STR(TENKINET_SERVER_PORT);
static Mode opt_mode = MODE_SAMPLE;
static char opt_verbose = 0;
static char opt_pretty = 0;
static uint32_t opt_interval = INTERVAL_DEFAULT;
static char *opt_serial = NULL;
static char *opt_index = "0";
static int opt_rows = 0;
static char opt_digits = DEFAULT_DECIMAL_DIGITS;
static char opt_ascii = 0;
static char opt_retry = 0;
static char opt_uncal = 0;
static char *opt_logfile = NULL;

static FILE *log_fp = NULL;
static VirtualOptions cfg_virtual_options = VIRTUAL_OPTIONS_DEFAULT;
static int line_counter = 0;

unit_t 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,
};

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

static int validate_number(char *str);
static int validate_ip(char *arg);

static int loop();

static Device *find_existing_device(const char *serial_number);

static void connection_callback(TenkinetClient client, int status, void *user_data);
static void list_callback(TenkinetClient client, void *user_data);
static void data_callback(TenkinetClient client, int64_t timestamp, void *user_data);
static void device_status_callback(TenkinetClient client, int64_t timestamp, Device *device, void *user_data);

static int scan_servers();
static void subscribe(TenkinetClient client, void *user_data);

static int device_list_compare(void *p, void *q);

static void print_list(TenkinetClient client);
static void print_device_info(Device *device);
static void printTimeStamp(FILE *stream);

static TenkinetCallbacks callbacks = {

    .connection_cb = connection_callback,
    .list_cb = list_callback,
    .data_cb = data_callback,
    .device_status_cb = device_status_callback,
    .user_data = NULL

};


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

    // Parse command-line options
    int c;
    while ((c = getopt_long(argc, argv, OPTIONS_SHORT, OPTIONS, NULL)) != -1) {
        switch (c) {
            case 'V':
                fputs(VERSION_TEXT, stdout);
                return 0;
            case 'v':
                opt_verbose = 1;
                break;
            case 'h':
                fputs(HELP_TEXT, stdout);
                return 0;
            case 'l':
                opt_mode = MODE_LIST;
                break;
            case 'f':
                // Nothing to do. Defaults to 'first'
                break;
            case 'p':
                opt_pretty = 1;
                break;
            case 'H':
                opt_host_id = optarg;
                break;
            case 's':
                opt_serial = optarg;
                break;
            case 'i':
                if (*optarg == 'a') {
                    opt_index = "";
                } else {
                    opt_index = optarg;
                }
                break;
            case 'R':
                if (!validate_number(optarg)) {
                    quit(ERROR_INVALID_RETRY_VALUE);
                }
                opt_retry = atoi(optarg);
                if (opt_retry < 0 || opt_retry > 9) {
                    quit(ERROR_INVALID_RETRY_VALUE);
                }
                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':
                opt_ascii = 1;
                break;
            case 'u':
                opt_uncal = 1;
                break;
            case 'L':
                opt_logfile = optarg;
                opt_mode = MODE_LOG;
                break;
            case 'I':
                opt_interval = ((uint32_t)atoi(optarg)) * 1000;  // ms -> us
                if (opt_interval < INTERVAL_MIN) {
                    opt_interval = INTERVAL_MIN; 
                }
                break;
            case 'r':
                if (!validate_number(optarg)) {
                    quit(ERROR_INVALID_NUM_ROWS);
                }
                opt_rows = atoi(optarg);
                break;
            case 'x':
                if (!validate_number(optarg)) {
                    quit(ERROR_INVALID_DECIMAL_DIGITS);
                }
                opt_digits = atoi(optarg);
                if (opt_digits < 0 || opt_digits > 6) {
                    fprintf(stderr, "Argument -x must be between 0 and 6\n");
                    quit(ERROR_INVALID_DECIMAL_DIGITS);
                }
                break;
            case 'S':
				{
					char *e;
					double slp;

					slp = strtod(optarg, &e);
					if (e==optarg) {
						fprintf(stderr, "Invalid pressure\n");
						return -1;
					}
                    cfg_virtual_options.standard_sea_level_pressure = slp;
				}
                break;
            case 'o':
                if (!strcasecmp(optarg, "no_humidex_range")) {
                    cfg_virtual_options.flags |= VIRTUAL_FLAG_NO_HUMIDEX_RANGE;
                }
                else if (!strcasecmp(optarg, "no_heat_index_range")) {
                    cfg_virtual_options.flags |= VIRTUAL_FLAG_NO_HEAT_INDEX_RANGE;
                }
                else {
                    quit(ERROR_INVALID_OPTION);
                }
                break;
            default:
                quit(ERROR_INVALID_OPTION);
        }
    }

    // Open log file if required
    if (opt_mode == MODE_LOG) {
        printf("Log mode on.\n");
        if (strcmp(opt_logfile, "-")) {
            log_fp = fopen(opt_logfile, "a");
            if (!log_fp) {
                quit(ERROR_CANNOT_OPEN_LOG_FILE);
            }
            printf("Opened file '%s' for logging. Append mode.\n", opt_logfile);
#ifdef _WIN32
				setvbuf(stdout, NULL, _IONBF, 0);
#else
				setlinebuf(log_fp);
#endif
        }
        else {
            printf("Logging to stdout\n");
        }
    }

    if (tenkinet_init(&seldata)) {
        quit(ERROR_INIT);
    }

    if (opt_host_id && validate_ip(opt_host_id)) {
        parse_host_and_port(opt_host_id);
    }

    // Create/connect client(s)
    TenkinetClient client;
    if (*opt_host_addr) { // select specific host by address
        if (opt_verbose) {
            printf("# Selecting host by address\n");
        }
        client = tenkinet_client_new(opt_host_addr, opt_host_port, &callbacks);
        if (!client) {
            quit(ERROR_CONNECTION);
        }
        tenkinet_connect(client);
        list_add(&clients, client);
    }
    else { // list available hosts
        int n_servers = scan_servers();
        if (!error) {
            if (opt_verbose) {
                printf("# Found %d server", n_servers);
                n_servers > 1 ? printf("s.\n") : printf(".\n");
            }
            callbacks.user_data = malloc(sizeof(int));
            *(int *)callbacks.user_data = n_servers;
            LIST_FOR(&servers) {
                TenkinetServerInfo *server = LIST_CUR(TenkinetServerInfo);
                client = tenkinet_client_new(server->address_str, server->port_str, &callbacks);
                if (client) {
                    tenkinet_connect(client);
                    list_add(&clients, client);
                }
            }
            client = NULL;            
        }
        else {
            quit(error);
        }
    }

    if (clients.size == 0) {
        quit(ERROR_NO_SERVER);
    }

    // Gather info
    if (loop() == DONE) {
        error = SUCCESS;
    }
    else {
        quit(error);
    }

    // Select client by ID (name or serial)
    if (opt_host_id && !client) {
        if (opt_verbose) {
            printf("# Selecting host by ID\n");
        }
        LIST_FOR(&clients) {
            client = LIST_CUR(TenkinetClient);
            if (strcmp(opt_host_id, tenkinet_get_server_name(client)) == 0) {
                break;
            }
            if (strcmp(opt_host_id, tenkinet_get_server_serial_number(client)) == 0) {
                break;
            }
            client = NULL;
        }
    }

    // Print info and quit if MODE_LIST
    if (opt_mode == MODE_LIST) {
        if (client) {
            print_list(client);
        }
        else {
            LIST_FOR(&clients) {
                client = LIST_CUR(TenkinetClient);
                print_list(client);
            }
        }
        quit(error);
    }

    // Select client according to device serial
    if (!client) {
        if (opt_verbose) {
            printf("# Selecting host by device\n");
        }
        list_remove_all(&devices);
        LIST_FOR(&clients) {
            client = LIST_CUR(TenkinetClient);
            if (opt_serial) {
                if (tenkinet_find_device(client, opt_serial)) {
                    break;
                }
            }
            else {
                tenkinet_get_devices(client, &devices);
                if (devices.size > 0) {
                    Device *device = list_get(&devices, 0);
                    opt_serial = device->serial_number;
                    break;
                }
            }
            client = NULL;
        }
    }

    if (!client) {
        if (opt_serial) {
            quit(ERROR_DEVICE_NOT_FOUND);
        }
        else if (opt_host_id) {
            quit(ERROR_HOST_NOT_FOUND);
        }
        else {
            quit(ERROR_NO_SERVER);
        }
    }

    // FIXME
    if (tenkinet_get_server_protocol(client) != 1) {
        fprintf(stderr, "Server protocol version not supported. Please update this software.\n");
        quit(ERROR_PROTOCOL_VERSION);
    }

    snprintf(opt_host_addr, sizeof(opt_host_addr), "%s", tenkinet_get_server_address(client));
    snprintf(opt_host_port, sizeof(opt_host_port), "%d", tenkinet_get_server_port(client));

    if (opt_verbose) {
        printf("# Connected to %s:%s\n", opt_host_addr, opt_host_port);
    }

    list_remove_all(&devices);
    tenkinet_get_devices(client, &devices);

    if (!opt_serial) {
        Device *device = list_get(&devices, 0);
        opt_serial = device->serial_number;
    }

    // Subscribe to device:channels
    parse_device_argument();
    subscribe(client, NULL);

    loop();

    if (log_fp) {
        printf("Closing log file.\n");
        fflush(log_fp);
        fclose(log_fp);
    }


    quit(error);
}


static int loop() {

    while (!error) {

        if (select_helper(&seldata, NULL) < 0) {
            error = ERROR_SELECT;
            break;
        }

        tenkinet_process();

    }

    return error;
}


static void quit(Error error) {

    if (error == DONE) {
        error = SUCCESS;
    }

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

    tenkinet_exit();
    exit(error);

}


static int scan_servers() {

    uint16_t port = TENKINET_SERVER_PORT;
    unsigned int interval = 100;
    unsigned int count = 1 + opt_retry;

    if (tenkinet_discovery_init(port, NULL)) {
        error = ERROR_CONNECTION;
        return 0;
    }
    tenkinet_discovery_loop(interval, count);

    TenkinetServerInfo *server;
    TenkinetServerInfo *info = tenkinet_discovery_results();

    while (info) {
        server = malloc(sizeof(TenkinetServerInfo));
        memcpy(server, info, sizeof(TenkinetServerInfo));
        list_add(&servers, server);
        info = info->next;
    }

    tenkinet_discovery_exit();

    return servers.size;
}


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);
    }

    units[cat] = unit;

}


static void parse_host_and_port(char *arg) {

    static const char COLON[] = ":";

    char *p = strtok(arg, COLON);
    if (!p) {
        return;
    }

    size_t host_len = strlen(p);
    memcpy(opt_host_addr, p, host_len);
    opt_host_addr[host_len] = 0;

    p = strtok(NULL, COLON);
    if (p) {
        memset(opt_host_port, 0, sizeof(opt_host_port));
        strncpy(opt_host_port, p, sizeof(opt_host_port) - 1);
    }

}


static void parse_device_argument() {

    // Format
    //     DEVICE            all channels
    //     DEVICE:0,1,2,3    comma-separated list of channel indices

    // Find the end of the serial number in the arg string

    char arg[64];
    snprintf(arg, sizeof(arg), "%s:%s", opt_serial, opt_index);

    char *p;
    for (p = arg; *p != ':' && *p != 0 && (p - arg < DEVICE_SERIAL_NUMBER_LEN); p++);

    size_t serial_number_len = p - arg;

    if (serial_number_len == 0) {
        quit(ERROR_EMPTY_SERIAL_NUMBER);
    }

    // Find or create subscription

    Subscription *sub = NULL;

    LIST_FOR(&subscriptions) {
        Subscription *s = LIST_CUR(Subscription);
        if (!strncmp(s->serial_number, arg, serial_number_len)) {
            sub = s;
            break;
        }
    }

    if (sub == NULL) {
        sub = calloc(1, sizeof(Subscription));
        memcpy(sub->serial_number, arg, serial_number_len);  // null-terminated, thanks to calloc()
        list_add(&subscriptions, sub);
    }

    // Parse channel indices

    if (*p == 0) {  // no channel indices specified
        return;
    }

    static const char COMMA[] = ",";

    for (char *token = strtok(++p, COMMA); token != NULL; token = strtok(NULL, COMMA)) {

        // Validate token; simpler than error-checking with strtol()...
        for (p = token; *p != ',' && *p != 0; p++) {
            if (*p < '0' || '9' < *p) {
                quit(ERROR_INVALID_CHANNEL_INDEX);
            }
        }

        size_t channel_idx = atoi(token);
        list_add(&sub->channel_indices, (void *)channel_idx);

    }

    // Sort indices
    heap_sort(sub->channel_indices.elements, sub->channel_indices.size, compare_unsigned_integers);

}


static Device *find_existing_device(const char *serial_number) {

    Device *device = device_find(&devices, serial_number, 0);

    return device;

}


static void connection_callback(TenkinetClient client, int status, void *user_data) {

    if (status == TENKINET_SUCCESS) {
        tenkinet_req_list(client);
        tenkinet_send(client);
    }
    else {
        if (!error) {
            if (tenkinet_get_server_error(client) == TENKINET_ERROR_UNSUPPORTED) {
                error = ERROR_PROTOCOL_VERSION;
            }
            else {
                error = ERROR_CONNECTION;
            }
        }
    }

}

static void list_callback(TenkinetClient client, void *user_data) {

    list_remove_all(&devices);
    tenkinet_get_devices(client, &devices);

    if (opt_verbose) {
        printf("# Found %zu device", devices.size);
        if (devices.size > 1) {
            printf("s");
        }
        printf(" @ %s\n", tenkinet_get_server_address(client));
    }

    if (user_data) {
        int *pending = (int *)user_data;
        *pending -= 1;
        if (*pending <= 0) {
            error = DONE;
        }
    } else {
        error = DONE;
    }

}


static void subscribe(TenkinetClient client, void *user_data) {

    if (subscriptions.size == 0) {
        error = ERROR_DEVICE_NOT_FOUND;
    }

    // Resolve subscribed devices and channels

    LIST_FOR(&subscriptions) {

        Subscription *sub = LIST_CUR(Subscription);
        Source *source = sub->source;
        Device *device = NULL;

        if (source) {
            device = source->device;
        }
        else {
            device = find_existing_device(sub->serial_number);
            if (!device) {
                // fprintf(stderr, "# ERROR: Device not found: %s\n", sub->serial_number);
                error = ERROR_DEVICE_NOT_FOUND;
                return;
            }
            source = source_new(device, &cfg_virtual_options);
            sub->source = source;
        }

        // Set source calibration option

        char cal_flags = opt_uncal ? 0 : 0xff;
        if (opt_verbose) {
            if (!cal_flags) {
                printf("# Using uncalibrated values.\n");
            }
        }
        memset(&sub->source->user_calibration_enabled, cal_flags, sizeof(sub->source->user_calibration_enabled));

        if (sub->channel_indices.size > 0) {

            // Resolve channels from indices

            list_grow(&sub->channels, sub->channel_indices.size);

            LIST_FOR(&sub->channel_indices) {

                void *p = LIST_CUR(void);
                size_t c = (size_t)p;

                if (c < USBTENKI_VIRTUAL_START) {
                    // actual channel
                    list_add(&sub->channels, list_get(&source->calibrated_channels, c));
                }
                else {
                    // virtual channel
                    LIST_FOR(&source->virtual_channels) {
                        VirtualChannel *vc = LIST_CUR(VirtualChannel);
                        Channel *channel = &vc->channel;
                        if (channel->chip_id == c) {
                            list_add(&sub->channels, channel);
                            break;
                        }
                    }
                }

            }

        }
        else {

            // Subscribe to all channels (including virtual)

            size_t size = device->channels.size + source->virtual_channels.size;
            list_grow(&sub->channel_indices, size);
            list_grow(&sub->channels, size);

            for (size_t c = 0; c < device->channels.size; c++) {
                list_add(&sub->channel_indices, (void *)c);
                list_add(&sub->channels, list_get(&source->calibrated_channels, c));
            }

            LIST_FOR(&source->virtual_channels) {
                VirtualChannel *vc = LIST_CUR(VirtualChannel);
                size_t c = vc->channel.chip_id;
                list_add(&sub->channel_indices, (void *)c);
                list_add(&sub->channels, &vc->channel);
            }

        }

        tenkinet_req_subscribe(client, device);
        tenkinet_send(client);

    }

    tenkinet_req_poll(client, opt_interval);
    tenkinet_send(client);

}


static void data_callback(TenkinetClient client, int64_t timestamp, void *user_data) {

	char fmt[16];
	sprintf(fmt, "%%.%df", opt_digits);

    // Refresh sources
    LIST_FOR(&subscriptions) {
        Subscription *sub = LIST_CUR(Subscription);
        Source *source = sub->source;
        source_refresh(source);
    }

    if (log_fp) {
        printTimeStamp(log_fp);
        fprintf(log_fp, ", ");
        // if (log_fp != stdout) {
        //     printTimeStamp(stdout);
        //     fflush(stdout);
        //     opt_pretty ? printf("\n") : printf(", ");
        // }
    }

    LIST_FOR(&subscriptions) {
        Subscription *sub = LIST_CUR(Subscription);
        int i = 0;
        LIST_FOR(&sub->channels) {
            Channel *channel = LIST_CUR(Channel);

            if (opt_pretty) {
                printf("%s: ", chip_description(channel->chip_id));
            }

            Quantity *qty = &channel->quantity;

            if (qty->type == QUANTITY_TYPE_ERROR) {
                if (opt_pretty) {
                    printf("%s\n", chip_error_to_string(qty->value_error));
                }
                else {
                    printf("%s", chip_error_to_string_no_spaces(qty->value_error));
                    if (i < sub->channels.size -1) {
                        printf(", ");
                    }
                }
            } 
            else {

                unit_t channel_unit = qty->unit;
                unit_category_t unitcat = unit_category(channel_unit);
                unit_t pref_unit = units[unitcat];
                quantity_convert_to_unit(qty, pref_unit);
                quantity_convert_to_float(qty);

                // if (log_fp != stdout) {
                    printf(fmt, qty->value_float);
                    if (opt_pretty) {
                        printf(" %s\n", unit_to_string(qty->unit, opt_ascii));
                    }
                    else {
                        i < sub->channels.size -1 ? printf(", ") : printf("\n");
                    }
                // }
            }

            if (log_fp) {
                float value = qty->type == QUANTITY_TYPE_FLOAT ? qty->value_float : NAN;
                fprintf(log_fp, fmt, value);
                if (i < sub->channels.size -1) {
                    fprintf(log_fp, ", ");
                }
            }
            i++;
        }
    }

    if (log_fp) {
        fprintf(log_fp, "\n");
        fflush(log_fp);
    }

    if (opt_mode == MODE_SAMPLE) {
        error = DONE;
    }

    line_counter++;
    if (opt_rows > 0 && line_counter >= opt_rows) {
        error = DONE;
    }
}


static void device_status_callback(TenkinetClient client, int64_t timestamp, Device *device, void *user_data) {

    if (opt_verbose) {
        if (DEVICE_SERIAL_NUMBERS_EQUAL(device->serial_number, opt_serial)) {
            if (device_flag_check(device, DEVICE_FLAG_ALIVE)) {
                printf("# Device '%s' connected.\n", opt_serial);
                if (opt_mode == MODE_LOG && log_fp) {
                    printf("# Logging at interval: %d ms\n", opt_interval / 1000);
                }
            }
            else if (opt_mode == MODE_LOG) {
                printf("# Device '%s' disconnected. Logging paused...\n", opt_serial);
            }
        }
    }
}


static int device_list_compare(void *p, void *q) {

    Device *a = (Device*)p;
    Device *b = (Device*)q;

    int c = ((int)(a->port)) - ((int)(b->port));

    if (c != 0) {
        return c;
    }

    return strncmp(a->serial_number, b->serial_number, DEVICE_SERIAL_NUMBER_LEN);

}

static void print_list(TenkinetClient client) {

    List connected;
    list_init(&connected);

    tenkinet_get_devices(client, &connected);

    if (connected.size) {
        printf("# %s [%s @ %s:%d]\n", 
            tenkinet_get_server_name(client),
            tenkinet_get_server_serial_number(client), 
            tenkinet_get_server_address(client),
            tenkinet_get_server_port(client)
        );
        heap_sort(connected.elements, connected.size, device_list_compare);

        LIST_FOR(&connected) {
            Device *device = LIST_CUR(Device);
            print_device_info(device);
        }
    }

    list_clear(&connected);
}


static void print_device_info(Device *device) {

    Source source;

    source_init(&source, device, &cfg_virtual_options);

    printf("Found: '%s', ", device->product_name);
    printf("Serial: '%s', ", device->serial_number);
    printf("Version %d.%d, ", device->version.major,
                            device->version.minor);
	printf("Channels: %zu\n", source.device->channels.size + source.virtual_channels.size);

    for (int c = 0; c < device->channels.size; c++) {
        Channel *channel = LIST_GET(&device->channels, c, Channel);
        printf("    Channel %d: %s [%s]\n",
                c,
                chip_description(channel->chip_id),
                chip_description_short(channel->chip_id));
    }

    LIST_FOR(&source.virtual_channels) {
        VirtualChannel *vc = LIST_CUR(VirtualChannel);
        Channel *channel = &(vc->channel);
        printf("    Virtual Channel %d: %s [%s]\n",
                channel->chip_id,  // TODO
                chip_description(channel->chip_id),
                chip_description_short(channel->chip_id));
    }

    source_clear(&source);
}


static void printTimeStamp(FILE *stream)
{
	time_t t;
	struct timeval tv_now;
	struct tm *tm;

	gettimeofday(&tv_now, NULL);
	t = tv_now.tv_sec;

	tm = localtime(&t);

	fprintf(stream, "%d-%02d-%02d %02d:%02d:%02d.%03ld", 
		tm->tm_year + 1900, tm->tm_mon+1, tm->tm_mday,
			tm->tm_hour, tm->tm_min, tm->tm_sec,
				(long)tv_now.tv_usec / 1000);

}


static int validate_number(char *str) {
    while (*str) {
        if(!isdigit(*str)){
            return 0;
        }
        str++;
    }
    return 1;
}


static int validate_ip(char *arg) {
    if (arg == NULL) {
        return 0;
    }
    int num, dots = 0;
    char *ptr;
    char ip[INET_ADDRSTRLEN];
    strncpy(ip, arg, sizeof(ip));
    strtok(ip, ":");
    ptr = strtok(ip, ".");
    if (ptr == NULL) {
        return 0;
    }
    while (ptr) {
        if (!validate_number(ptr)) {
            return 0;
        }
        num = atoi(ptr);
        if (num >= 0 && num <= 255) {
            ptr = strtok(NULL, ".");
            if (ptr != NULL)
                dots++;
            }
        else {
            return 0;
        }
    }
    if (dots != 3) { //if the number of dots are not 3, return false
        return 0;
    }
    return 1;
}