/** @file
    Option parsing functions to complement getopt.

    Copyright (C) 2017 Christian Zuckschwerdt

    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 2 of the License, or
    (at your option) any later version.
*/

#include "optparse.h"
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <string.h>

int tls_param(tls_opts_t *tls_opts, char const *key, char const *val)
{
    if (!tls_opts || !key || !*key)
        return 1;
    else if (!strcasecmp(key, "tls_cert"))
        tls_opts->tls_cert = val;
    else if (!strcasecmp(key, "tls_key"))
        tls_opts->tls_key = val;
    else if (!strcasecmp(key, "tls_ca_cert"))
        tls_opts->tls_ca_cert = val;
    else if (!strcasecmp(key, "tls_cipher_suites"))
        tls_opts->tls_cipher_suites = val;
    else if (!strcasecmp(key, "tls_server_name"))
        tls_opts->tls_server_name = val;
    else if (!strcasecmp(key, "tls_psk_identity"))
        tls_opts->tls_psk_identity = val;
    else if (!strcasecmp(key, "tls_psk_key"))
        tls_opts->tls_psk_key = val;
    else
        return 1;
    return 0;
}

int atobv(char const *arg, int def)
{
    if (!arg)
        return def;
    if (!strcasecmp(arg, "true") || !strcasecmp(arg, "yes") || !strcasecmp(arg, "on") || !strcasecmp(arg, "enable"))
        return 1;
    return atoi(arg);
}

int atoiv(char const *arg, int def)
{
    if (!arg)
        return def;
    char *endptr;
    int val = strtol(arg, &endptr, 10);
    if (arg == endptr)
        return def;
    return val;
}

char *arg_param(char const *arg)
{
    if (!arg)
        return NULL;
    char *p = strchr(arg, ':');
    char *c = strchr(arg, ',');
    if (p && (!c || p < c))
        return ++p;
    else if (c)
        return c;
    else
        return p;
}

double arg_float(char const *str, char const *error_hint)
{
    if (!str) {
        fprintf(stderr, "%smissing number argument\n", error_hint);
        exit(1);
    }

    if (!*str) {
        fprintf(stderr, "%sempty number argument\n", error_hint);
        exit(1);
    }

    // allow whitespace and equals char
    while (*str == ' ' || *str == '=')
        ++str;

    char *endptr;
    double val = strtod(str, &endptr);

    if (str == endptr) {
        fprintf(stderr, "%sinvalid number argument (%s)\n", error_hint, str);
        exit(1);
    }

    return val;
}

char *hostport_param(char *param, char const **host, char const **port)
{
    if (param && *param) {
        if (param[0] == '/' && param[1] == '/') {
            param += 2;
        }
        if (*param != ':' && *param != ',') {
            *host = param;
            if (*param == '[') {
                (*host)++;
                param = strchr(param, ']');
                if (param) {
                    *param++ = '\0';
                }
                else {
                    fprintf(stderr, "Malformed Ipv6 address!\n");
                    exit(1);
                }
            }
        }
        char *colon = strchr(param, ':');
        char *comma = strchr(param, ',');
        if (colon && (!comma || colon < comma)) {
            *colon++ = '\0';
            *port    = colon;
        }
        if (comma) {
            *comma++ = '\0';
            return comma;
        }
    }
    return NULL;
}

uint32_t atouint32_metric(char const *str, char const *error_hint)
{
    if (!str) {
        fprintf(stderr, "%smissing number argument\n", error_hint);
        exit(1);
    }

    if (!*str) {
        fprintf(stderr, "%sempty number argument\n", error_hint);
        exit(1);
    }

    char *endptr;
    double val = strtod(str, &endptr);

    if (str == endptr) {
        fprintf(stderr, "%sinvalid number argument (%s)\n", error_hint, str);
        exit(1);
    }

    if (val < 0.0) {
        fprintf(stderr, "%snon-negative number argument expected (%f)\n", error_hint, val);
        exit(1);
    }

    // allow whitespace before suffix
    while (*endptr == ' ' || *endptr == '\t')
        ++endptr;

    switch (*endptr) {
        case '\0':
            break;
        case 'k':
        case 'K':
            val *= 1e3;
            break;
        case 'M':
        case 'm':
            val *= 1e6;
            break;
        case 'G':
        case 'g':
            val *= 1e9;
            break;
        default:
            fprintf(stderr, "%sunknown number suffix (%s)\n", error_hint, endptr);
            exit(1);
    }

    if (val > UINT32_MAX) {
        fprintf(stderr, "%snumber argument too big (%f)\n", error_hint, val);
        exit(1);
    }

    val += 1e-5; // rounding (e.g. 4123456789.99999)
    if (val - (uint32_t)val > 2e-5) {
        fprintf(stderr, "%sdecimal fraction (%f) did you forget k, M, or G suffix?\n", error_hint, val - (uint32_t)val);
    }

    return (uint32_t)val;
}

int atoi_time(char const *str, char const *error_hint)
{
    if (!str) {
        fprintf(stderr, "%smissing time argument\n", error_hint);
        exit(1);
    }

    if (!*str) {
        fprintf(stderr, "%sempty time argument\n", error_hint);
        exit(1);
    }

    char *endptr    = NULL;
    double val      = 0.0;
    unsigned colons = 0;

    do {
        double num = strtod(str, &endptr);

        if (!endptr || str == endptr) {
            fprintf(stderr, "%sinvalid time argument (%s)\n", error_hint, str);
            exit(1);
        }

        // allow whitespace before suffix
        while (*endptr == ' ' || *endptr == '\t')
            ++endptr;

        switch (*endptr) {
        case '\0':
            if (colons == 0) {
                // assume seconds
                val += num;
                break;
            }
            // intentional fallthrough
#if defined __has_attribute
#if __has_attribute(fallthrough)
            __attribute__((fallthrough));
#endif
#endif
        case ':':
            ++colons;
            if (colons == 1)
                val += num * 60 * 60;
            else if (colons == 2)
                val += num * 60;
            else if (colons == 3)
                val += num;
            else {
                fprintf(stderr, "%stoo many colons (use HH:MM[:SS]))\n", error_hint);
                exit(1);
            }
            if (*endptr)
                ++endptr;
            break;
        case 's':
        case 'S':
            val += num;
            ++endptr;
            break;
        case 'm':
        case 'M':
            val += num * 60;
            ++endptr;
            break;
        case 'h':
        case 'H':
            val += num * 60 * 60;
            ++endptr;
            break;
        case 'd':
        case 'D':
            val += num * 60 * 60 * 24;
            ++endptr;
            break;
        default:
            fprintf(stderr, "%sunknown time suffix (%s)\n", error_hint, endptr);
            exit(1);
        }

        // chew up any remaining whitespace
        while (*endptr == ' ' || *endptr == '\t')
            ++endptr;
        str = endptr;

    } while (*endptr);

    if (val > INT_MAX || val < INT_MIN) {
        fprintf(stderr, "%stime argument too big (%f)\n", error_hint, val);
        exit(1);
    }

    if (val < 0) {
        val -= 1e-5; // rounding (e.g. -4123456789.99999)
    }
    else {
        val += 1e-5; // rounding (e.g. 4123456789.99999)
    }
    if (val - (int)(val) > 2e-5) {
        fprintf(stderr, "%sdecimal fraction (%f) did you forget m, or h suffix?\n", error_hint, val - (uint32_t)val);
    }

    return (int)val;
}

char *asepc(char **stringp, char delim)
{
    if (!stringp || !*stringp) return NULL;
    char *s = strchr(*stringp, delim);
    if (s) *s++ = '\0';
    char *p = *stringp;
    *stringp = s;
    return p;
}

static char *achrb(char *s, int c, int b)
{
    for (; s && *s && *s != b; ++s)
        if (*s == c) return (char *)s;
    return NULL;
}

char *asepcb(char **stringp, char delim, char stop)
{
    if (!stringp || !*stringp) return NULL;
    char *s = achrb(*stringp, delim, stop);
    if (s) *s++ = '\0';
    char *p = *stringp;
    *stringp = s;
    return p;
}

int kwargs_match(char const *s, char const *key, char const **val)
{
    if (!key || !*key) {
        return 0; // no match
    }
    size_t len = strlen(key);
    // check prefix match
    if (strncmp(s, key, len)) {
        return 0; // no match
    }
    s += len;
    // skip whitespace after match
    while (*s == ' ' || *s == '\t')
        ++s;
    if (*s == '\0' || * s == ',') {
        if (val)
            *val = NULL;
        return 1; // match with no arg
    }
    if (*s == '=') {
        if (val)
            *val = s + 1;
        return 1; // match with arg
    }
    return 0; // no exact match
}

char const *kwargs_skip(char const *s)
{
    // skip to the next comma if possible
    while (s && *s && *s != ',')
        ++s;
    // skip comma and whitespace if possible
    while (s && *s && (*s == ',' || *s == ' ' || *s == '\t'))
        ++s;
    return s;
}

char *getkwargs(char **s, char **key, char **val)
{
    char *v = asepc(s, ',');
    char *k = asepc(&v, '=');
    if (key) *key = k;
    if (val) *val = v;
    return k;
}

char *trim_ws(char *str)
{
    if (!str || !*str)
        return str;
    while (*str == ' ' || *str == '\t' || *str == '\r' || *str == '\n')
        ++str;
    char *e = str; // end pointer (last non ws)
    char *p = str; // scanning pointer
    while (*p) {
        while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
            ++p;
        if (*p)
            e = p++;
    }
    *++e = '\0';
    return str;
}

char *remove_ws(char *str)
{
    if (!str)
        return str;
    char *d = str; // dst pointer
    char *s = str; // src pointer
    while (*s) {
        while (*s == ' ' || *s == '\t' || *s == '\r' || *s == '\n')
            ++s;
        if (*s)
            *d++ = *s++;
    }
    *d++ = '\0';
    return str;
}

// Unit testing
#ifdef _TEST
#define ASSERT_EQUALS(a,b)                                  \
    do {                                                    \
        if ((a) == (b))                                     \
            ++passed;                                       \
        else {                                              \
            ++failed;                                       \
            fprintf(stderr, "FAIL: %d <> %d\n", (a), (b));  \
        }                                                   \
    } while (0)

int main(void)
{
    unsigned passed = 0;
    unsigned failed = 0;

    fprintf(stderr, "optparse:: atouint32_metric\n");
    ASSERT_EQUALS(atouint32_metric("0", ""), 0);
    ASSERT_EQUALS(atouint32_metric("1", ""), 1);
    ASSERT_EQUALS(atouint32_metric("0.0", ""), 0);
    ASSERT_EQUALS(atouint32_metric("1.0", ""), 1);
    ASSERT_EQUALS(atouint32_metric("1.024k", ""), 1024);
    ASSERT_EQUALS(atouint32_metric("433.92M", ""), 433920000);
    ASSERT_EQUALS(atouint32_metric("433.94M", ""), 433940000);
    ASSERT_EQUALS(atouint32_metric(" +1 G ", ""), 1000000000);

    fprintf(stderr, "optparse:: atoi_time\n");
    ASSERT_EQUALS(atoi_time("0", ""), 0);
    ASSERT_EQUALS(atoi_time("1", ""), 1);
    ASSERT_EQUALS(atoi_time("0.0", ""), 0);
    ASSERT_EQUALS(atoi_time("1.0", ""), 1);
    ASSERT_EQUALS(atoi_time("1s", ""), 1);
    ASSERT_EQUALS(atoi_time("2d", ""), 2 * 60 * 60 * 24);
    ASSERT_EQUALS(atoi_time("2h", ""), 2 * 60 * 60);
    ASSERT_EQUALS(atoi_time("2m", ""), 2 * 60);
    ASSERT_EQUALS(atoi_time("2s", ""), 2);
    ASSERT_EQUALS(atoi_time("2D", ""), 2 * 60 * 60 * 24);
    ASSERT_EQUALS(atoi_time("2H", ""), 2 * 60 * 60);
    ASSERT_EQUALS(atoi_time("2M", ""), 2 * 60);
    ASSERT_EQUALS(atoi_time("2S", ""), 2);
    ASSERT_EQUALS(atoi_time("2h3m4s", ""), 2 * 60 * 60 + 3 * 60 + 4);
    ASSERT_EQUALS(atoi_time("2h 3m 4s", ""), 2 * 60 * 60 + 3 * 60 + 4);
    ASSERT_EQUALS(atoi_time("2h3h 3m 4s 5", ""), 5 * 60 * 60 + 3 * 60 + 9);
    ASSERT_EQUALS(atoi_time(" 2m ", ""), 2 * 60);
    ASSERT_EQUALS(atoi_time("2 m", ""), 2 * 60);
    ASSERT_EQUALS(atoi_time("  2  m  ", ""), 2 * 60);
    ASSERT_EQUALS(atoi_time("-1m", ""), -60);
    ASSERT_EQUALS(atoi_time("1h-15m", ""), 45 * 60);

    ASSERT_EQUALS(atoi_time("2:3", ""), 2 * 60 * 60 + 3 * 60);
    ASSERT_EQUALS(atoi_time("2:3:4", ""), 2 * 60 * 60 + 3 * 60 + 4);
    ASSERT_EQUALS(atoi_time("02:03", ""), 2 * 60 * 60 + 3 * 60);
    ASSERT_EQUALS(atoi_time("02:03:04", ""), 2 * 60 * 60 + 3 * 60 + 4);
    ASSERT_EQUALS(atoi_time(" 2 : 3 ", ""), 2 * 60 * 60 + 3 * 60);
    ASSERT_EQUALS(atoi_time(" 2 : 3 : 4 ", ""), 2 * 60 * 60 + 3 * 60 + 4);

    fprintf(stderr, "optparse:: test (%u/%u) passed, (%u) failed.\n", passed, passed + failed, failed);

    return failed;
}
#endif /* _TEST */
