#include "error.h"
#include "options.h"
#include "print.h"

#include <ctype.h>
#include <limits.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#ifdef NOPTIONS

static const opt table[] = {
#define OPTION(N,V,L,H,D)
  { #N, (int)(V), D },
  OPTIONS
#undef OPTION
};

#else

static const opt table[] = {
#define OPTION(N, V, L, H, D) { #N, (int)(V), (int)(L), (int)(H), D },
    OPTIONS
#undef OPTION
};

#endif

#define size_table (sizeof table / sizeof *table)

const opt *kissat_options_begin = table;
const opt *kissat_options_end = table + size_table;

static void check_table_sorted(void)
{
#ifndef NDEBUG
    const opt *p = 0;
    for (all_options(o))
        if (p && strcmp(p->name, o->name) >= 0)
            kissat_fatal("option '%s' before option '%s'", p->name, o->name);
        else
            p = o;
#endif
}

const opt *kissat_options_has(const char *name)
{
    size_t l = 0, m, r = size_table;
    int tmp;
    const opt *o;
    assert(l < r);
    while (l + 1 < r) {
        m = l + (r - l) / 2;
        tmp = strcmp(name, (o = table + m)->name);
        if (tmp < 0)
            r = m;
        else if (tmp > 0)
            l = m;
        else
            return o;
    }
    o = table + l;
    tmp = strcmp(o->name, name);
    return tmp ? 0 : o;
}

bool kissat_parse_option_value(const char *val_str, int *res_ptr)
{
    if (!strcmp(val_str, "true")) {
        *res_ptr = 1;
        return true;
    }
    if (!strcmp(val_str, "false")) {
        *res_ptr = 0;
        return true;
    }
    int sign = 1;
    const char *p = val_str;
    char ch = *p++;
    if (ch == '-') {
        sign = -1;
        ch = *p++;
    }
    if (!isdigit(ch)) // at least one digit
        return false;
    const unsigned max = -(unsigned)INT_MIN;
    unsigned res = ch - '0';
    while (isdigit((ch = *p++))) {
        if (max / 10 < res)
            return false;
        res *= 10;
        const unsigned digit = ch - '0';
        if (max - digit < res)
            return false;
        res += digit;
        if (!res)
            return false; // invalid '00'
    }
    if (ch == 'e') {               // parse '13e5' etc.
        if (!isdigit((ch = *p++))) // at least one digit
            return false;
        if (res) {
            if (*p) // exactly one digit
                return false;
            const unsigned digit = ch - '0';
            for (unsigned i = 0; i < digit; i++) {
                if (max / 10 < res)
                    return false;
                res *= 10;
            }
        } else {                       // parse '0^123123123' etc.
            while (isdigit(ch = *p++)) // arbitrary many digits
                ;
            if (ch)
                return false;
        }
    } else if (ch == '^') { // parse '2^11' etc.
        const unsigned base = res;
        if (!isdigit((ch = *p++))) // at least one digit
            return false;
        unsigned exp = ch - '0';
        if (base < 2) {                // parse '0^123123123' etc.
            while (isdigit(ch = *p++)) // arbitrary many digits
                ;
            if (ch)
                return false;
        } else if (isdigit(ch = *p++)) { // parse '2^30' etc.
            if (*p)                      // at most two digits
                return false;
            exp *= 10;
            const unsigned digit = ch - '0';
            exp += digit;
            if (!exp) // '2^00' invalid
                return false;
        } else if (ch)
            return false;
        if (exp)
            for (unsigned i = 1; i < exp; i++) {
                if (max / base < res)
                    return false;
                res *= base;
            }
        else if (base)
            res = 1; // parse '3^0'
        else
            return false; // '0^0' invalid
    } else if (ch)
        return false;
    assert(res <= max);
    if (sign > 0 && res == max)
        return false;
    res *= sign;
    *res_ptr = res;
    return true;
}

const char *kissat_parse_option_name(const char *arg, const char *name)
{
    if (arg[0] != '-' || arg[1] != '-')
        return 0;
    const char *p = arg + 2, *q = name;
    while (*p && *p == *q)
        p++, q++;
    if (*q)
        return 0;
    if (*p != '=')
        return 0;
    return p + 1;
}

#ifdef NOPTIONS

void kissat_init_options(void)
{
    check_table_sorted();
}

int kissat_options_get(const char *name)
{
    const opt *o = kissat_options_has(name);
    return o ? o->value : 0;
}

#else

#include "format.h"

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>

static void kissat_printf_usage(const char *option, const char *fmt, ...)
{
    va_list ap;
    printf("  %-26s ", option);
    va_start(ap, fmt);
    vprintf(fmt, ap);
    va_end(ap);
    fputc('\n', stdout);
}

static void check_ranges(void)
{
#define OPTION(N, V, L, H, D)                                                                             \
    do {                                                                                                  \
        if ((int)(L) > (int)(H))                                                                          \
            kissat_fatal("minimum '%d' of option '%s' above maximum '%d'", (int)(L), #N, (int)(H));       \
        if ((int)(V) < (int)(L))                                                                          \
            kissat_fatal("default value '%d' of option '%s' below minimum '%d'", (int)(V), #N, (int)(L)); \
        if ((int)(V) > (int)(H))                                                                          \
            kissat_fatal("default value '%d' of option '%s' above maximum '%d'", (int)(V), #N, (int)(H)); \
    } while (0);
    OPTIONS
#undef OPTION
}

static void check_name_length(void)
{
#ifndef NDEBUG
#define OPTION(N, V, L, H, D)                                 \
    if (strlen(#N) + 1 > kissat_options_max_name_buffer_size) \
        kissat_fatal("option '%s' name length %zu "           \
            "exceeds maximum name buffer size %zu",           \
            #N, strlen(#N), kissat_options_max_name_buffer_size);
    OPTIONS
#undef OPTION
#endif
}

int kissat_options_get(const options *options, const char *name)
{
    const int *p = kissat_options_ref(options, kissat_options_has(name));
    return p ? *p : 0;
}

int kissat_options_set_opt(options *options, const opt *o, int value)
{
    assert(kissat_options_begin <= o);
    assert(o < kissat_options_end);
    int *p = (int *)options + (o - table);
    int res = *p;
    if (value == res)
        return res;
    if (value < o->low)
        value = o->low;
    if (value > o->high)
        value = o->high;
    *p = value;
    return res;
}

int kissat_options_set(options *options, const char *name, int value)
{
    const opt *o = kissat_options_has(name);
    if (!o)
        return 0;
    return kissat_options_set_opt(options, o, value);
}

void kissat_init_options(options *options)
{
    check_ranges();
    check_name_length();
    check_table_sorted();
#define OPTION(N, V, L, H, D) \
    assert((L) <= (V));       \
    assert((V) <= (H));       \
    options->N = (V);
    OPTIONS
#undef OPTION
}

#define FORMAT_OPTION_LIMIT(V) (((V) == INT_MIN || (V) == INT_MAX) ? "." : kissat_format_value(&format, false, (V)))

void kissat_options_usage(void)
{
    check_ranges();
    check_name_length();
    check_table_sorted();
    format format;
    memset(&format, 0, sizeof format);
#define OPTION(N, V, L, H, D)                                       \
    do {                                                            \
        const bool b = ((L) == 0 && (H) == 1);                      \
        char buffer[96];                                            \
        if (b)                                                      \
            sprintf(buffer, "--%s=<bool>", #N);                     \
        else {                                                      \
            const char *low_str = FORMAT_OPTION_LIMIT((L));         \
            const char *high_str = FORMAT_OPTION_LIMIT((H));        \
            sprintf(buffer, "--%s=%s..%s", #N, low_str, high_str);  \
        }                                                           \
        const char *val_str = kissat_format_value(&format, b, (V)); \
        kissat_printf_usage(buffer, "%s [%s]", D, val_str);         \
    } while (0);
    OPTIONS
#undef OPTION
}

bool kissat_options_parse_arg(const char *arg, char *buffer, int *val_ptr)
{
    if (arg[0] != '-' || arg[1] != '-')
        return false;
    const char *name = arg + 2, *p = name;
    int ch;
    while ((ch = *p) && ch != '=')
        p++;
    if (ch) {
        assert(ch == '=');
        const size_t len = p - name;
        if (len >= kissat_options_max_name_buffer_size)
            return false;
        memcpy(buffer, name, len);
        buffer[len] = 0;
        const opt *o = kissat_options_has(buffer);
        if (!o)
            return false;
        int value;
        if (!kissat_parse_option_value(p + 1, &value))
            return false;
        if (value < o->low || value > o->high)
            return false;
        *val_ptr = value;
    } else {
        int value = 0;
        if (arg[2] == 'n' && arg[3] == 'o' && arg[4] == '-') {
            name += 3;
            const opt *o = kissat_options_has(name);
            if (!o || o->low > (value = 0))
                return false;
        } else {
            const opt *o = kissat_options_has(name);
            if (!o || o->high < (value = 1))
                return false;
        }
        assert(strlen(name) < kissat_options_max_name_buffer_size);
        strcpy(buffer, name);
        *val_ptr = value;
    }
    return true;
}

static bool ignore_embedded_option_for_fuzzing(const char *name)
{
#ifndef NEMBEDDED
    if (!strcmp(name, "embedded"))
        return true;
#endif
#ifndef QUIET
    if (!strcmp(name, "quiet"))
        return true;
#endif
    (void)name;
    return false;
}

void kissat_print_embedded_option_list(void)
{
#define OPTION(N, V, L, H, D)                    \
    if (!ignore_embedded_option_for_fuzzing(#N)) \
        printf("c --%s=%d\n", #N, (int)(V));
    OPTIONS
#undef OPTION
}

static bool ignore_range_option_for_fuzzing(const char *name)
{
#ifdef LOGGING
    if (!strcmp(name, "log"))
        return true;
#endif
#ifndef NEMBEDDED
    if (!strcmp(name, "embedded"))
        return true;
#endif
#ifndef QUIET
    if (!strcmp(name, "quiet"))
        return true;
#endif
    if (!strcmp(name, "reduce"))
        return true;
    if (!strcmp(name, "reluctant"))
        return true;
    if (!strcmp(name, "rephase"))
        return true;
    if (!strcmp(name, "restart"))
        return true;
    return false;
}

void kissat_print_option_range_list(void)
{
#define OPTION(N, V, L, H, D)                 \
    if (!ignore_range_option_for_fuzzing(#N)) \
        printf("%s %d %d %d\n", #N, (int)(L), (int)(V), (int)(H));
    OPTIONS
#undef OPTION
}

#endif
