#include <inttypes.h>
#include <stdio.h>
#include <limits.h>
#include <float.h>
#include <math.h> // TODO need this?
#include "unit_test.h"
#include <slow5/slow5.h>
#include "slow5_extra.h"
#include "slow5_misc.h"

char double_overflow[] = " .000000";

int ato_xintx_valid(void) {
    int err;
    char buf[256];

    ASSERT(slow5_ato_uint8("0", &err) == 0);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint8("1", &err) == 1);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint8("100", &err) == 100);
    ASSERT(err == 0);
    sprintf(buf, "%" PRIu32, UINT8_MAX);
    ASSERT(slow5_ato_uint8(buf, &err) == UINT8_MAX);
    ASSERT(err == 0);

    ASSERT(slow5_ato_uint32("0", &err) == 0);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint32("1", &err) == 1);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint32("100", &err) == 100);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint32("2000", &err) == 2000);
    ASSERT(err == 0);
    sprintf(buf, "%" PRIu32, UINT32_MAX);
    ASSERT(slow5_ato_uint32(buf, &err) == UINT32_MAX);
    ASSERT(err == 0);

    ASSERT(slow5_ato_uint64("0", &err) == 0);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint64("1", &err) == 1);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint64("100", &err) == 100);
    ASSERT(err == 0);
    ASSERT(slow5_ato_uint64("2000", &err) == 2000);
    ASSERT(err == 0);
    sprintf(buf, "%" PRIu32, UINT32_MAX);
    ASSERT(slow5_ato_uint64(buf, &err) == UINT32_MAX);
    ASSERT(err == 0);
    sprintf(buf, "%" PRId64, INT64_MAX);
    ASSERT(slow5_ato_uint64(buf, &err) == INT64_MAX);
    ASSERT(err == 0);
    sprintf(buf, "%" PRIu64, UINT64_MAX);
    ASSERT(slow5_ato_uint64(buf, &err) == UINT64_MAX);
    ASSERT(err == 0);

    sprintf(buf, "%" PRId16, (int16_t) INT16_MIN);
    ASSERT(slow5_ato_int16(buf, &err) == (int16_t) INT16_MIN);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("-2000", &err) == -2000);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("-100", &err) == -100);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("-1", &err) == -1);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("0", &err) == 0);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("1", &err) == 1);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("100", &err) == 100);
    ASSERT(err == 0);
    ASSERT(slow5_ato_int16("2000", &err) == 2000);
    ASSERT(err == 0);
    sprintf(buf, "%" PRId16, (int16_t) INT16_MAX);
    ASSERT(slow5_ato_int16(buf, &err) == (int16_t) INT16_MAX);
    ASSERT(err == 0);

    return EXIT_SUCCESS;
}

int ato_xintx_invalid(void) {
    int err;
    char buf[256];

    ASSERT(slow5_ato_uint8("-1", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint8("-100", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint8("-1000", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint8("-lol", &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRIu16, (uint16_t) ( (uint16_t) UINT8_MAX + 1));
    ASSERT(slow5_ato_uint8(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRIu16, (uint16_t) UINT16_MAX);
    ASSERT(slow5_ato_uint8(buf, &err) == 0);
    ASSERT(err == -1);

    ASSERT(slow5_ato_uint32("-1", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint32("-100", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint32("-1000", &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRIu64, (uint64_t) (UINT32_MAX) + 1);
    ASSERT(slow5_ato_uint32(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRIu64, UINT64_MAX);
    ASSERT(slow5_ato_uint32(buf, &err) == 0);
    ASSERT(err == -1);

    ASSERT(slow5_ato_uint64("-1", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint64("-100", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_ato_uint64("-1000", &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRId64, INT64_MIN);
    ASSERT(slow5_ato_uint64(buf, &err) == 0);
    ASSERT(err == -1);

    sprintf(buf, "%" PRIu16, (uint16_t) UINT16_MAX);
    ASSERT(slow5_ato_int16(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRId32, (int32_t) (INT16_MIN) - 1);
    ASSERT(slow5_ato_int16(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRId32, (int32_t) (INT16_MAX) + 1);
    ASSERT(slow5_ato_int16(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%" PRId64, INT64_MIN);
    ASSERT(slow5_ato_int16(buf, &err) == 0);
    ASSERT(err == -1);

    return EXIT_SUCCESS;
}

int strtod_check_valid(void) {
    int err;
    char buf[512];

    errno=0;

    ASSERT(slow5_strtod_check("0", &err) == (double) 0);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("1", &err) == (double) 1);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("100", &err) == (double) 100);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("-100", &err) == (double) -100);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("0.0", &err) == (double) 0.0);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("2.5", &err) == (double) 2.5);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("-100.7892", &err) == (double) -100.7892);
    ASSERT(err == 0);
    ASSERT(slow5_strtod_check("1.2", &err) == (double) 1.2);
    ASSERT(err == 0);
    sprintf(buf, "%lf", -DBL_MAX);
    ASSERT(slow5_strtod_check(buf, &err) == -DBL_MAX);
    ASSERT(err == 0);
    sprintf(buf, "%lf", DBL_MIN);
    ASSERT(approx(slow5_strtod_check(buf, &err), DBL_MIN));
    ASSERT(err == 0);
    sprintf(buf, "%lf", DBL_MAX);
    ASSERT(slow5_strtod_check(buf, &err) == DBL_MAX);
    ASSERT(err == 0);

    return EXIT_SUCCESS;
}

int strtod_check_invalid(void) {
    int err;
    char buf[8192];

    ASSERT(slow5_strtod_check("hithere", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_strtod_check("sometext", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_strtod_check("inf", &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_strtod_check("-inf", &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%le", 10e3);
    ASSERT(slow5_strtod_check(buf, &err) == 0);
    ASSERT(err == -1);
    sprintf(buf, "%le", -10e-3);
    ASSERT(slow5_strtod_check(buf, &err) == 0);
    ASSERT(err == -1);
    ASSERT(slow5_strtod_check("-inf", &err) == 0);
    ASSERT(err == -1);

    strcpy(buf,double_overflow);

    //sprintf(buf, "%Lf", LDBL_MAX);
    //if (strcmp(buf, "inf") != 0) { // TODO why does valgrind give "inf"?
    ASSERT(slow5_strtod_check(buf, &err) == HUGE_VAL);
    ASSERT(err == -1);
    //}
    //sprintf(buf, "%Lf", -LDBL_MAX);
    strcpy(buf+1,double_overflow);
    buf[0]='-';
    //if (strcmp(buf, "-inf") != 0) {
    ASSERT(slow5_strtod_check(buf, &err) == -HUGE_VAL);
    ASSERT(err == -1);
    //}

    return EXIT_SUCCESS;
}

int path_get_slow5_fmt_test(void) {

    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("test.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("hithere/test.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("testaskdj.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("blow5.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("slow5.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("hi...slow5.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("1234.slow5"));
    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt("myslow5.slow5"));

    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("test.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("hithere///test.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("hithere///test.slow5/test.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("testaslkdjlaskjdfalsdifaslkfdj234.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("blow5.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("slow5.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("hi...blow5.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("1234.blow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt("myblow5.blow5"));

    ASSERT(SLOW5_FORMAT_ASCII == slow5_path_get_fmt(".slow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_path_get_fmt(".blow5"));

    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt("..."));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt("slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt("blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt("blablabla"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt(""));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_path_get_fmt(NULL));

    return EXIT_SUCCESS;
}

int name_get_slow5_fmt_test(void) {

    ASSERT(SLOW5_FORMAT_ASCII == slow5_name_get_fmt("slow5"));
    ASSERT(SLOW5_FORMAT_BINARY == slow5_name_get_fmt("blow5"));

    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt(".slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt(".blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt(".blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("..."));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("slow55"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("blow55"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("blablabla"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt(""));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt(NULL));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("test.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("hithere/test.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("testaskdj.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("blow5.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("slow5.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("hi...slow5.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("1234.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("myslow5.slow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("test.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("hithere///test.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("hithere///test.slow5/test.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("testaslkdjlaskjdfalsdifaslkfdj234.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("blow5.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("slow5.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("hi...blow5.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("1234.blow5"));
    ASSERT(SLOW5_FORMAT_UNKNOWN == slow5_name_get_fmt("myblow5.blow5"));

    return EXIT_SUCCESS;
}

int fmt_get_name_test(void) {
    ASSERT(slow5_fmt_get_name(0) == NULL);
    ASSERT(strcmp(slow5_fmt_get_name(1), "slow5") == 0);
    ASSERT(strcmp(slow5_fmt_get_name(2), "blow5") == 0);
    ASSERT(slow5_fmt_get_name(3) == NULL);
    ASSERT(slow5_fmt_get_name(100) == NULL);
    ASSERT(slow5_fmt_get_name(-991) == NULL);

    return EXIT_SUCCESS;
}

int asprintf_mine_valid(void) {
    char *str = NULL;

    double x = 225.69;
    const char *exp = "225.690000";
    ASSERT(slow5_asprintf(&str, "%f", x) == strlen(exp));
    ASSERT(strcmp(str, exp) == 0);

    free(str);

    x = 225.691234;
    exp = "225.691234";
    ASSERT(slow5_asprintf(&str, "%f", x) == strlen(exp));
    ASSERT(strcmp(str, exp) == 0);

    free(str);

    x = 225.6912345;
    exp = "225.691235"; // Note that exp is different to x here
    ASSERT(slow5_asprintf(&str, "%f", x) == strlen(exp));
    ASSERT(strcmp(str, exp) == 0);

    free(str);

    x = 225.69123456789;
    exp = "225.691235"; // Note that exp is different to x here
    ASSERT(slow5_asprintf(&str, "%f", x) == strlen(exp));
    ASSERT(strcmp(str, exp) == 0);

    free(str);

    return EXIT_SUCCESS;
}

int version_compatible(void) {

    struct slow5_version file_version;
    struct slow5_version max_supported;

    //lib version must be >= file version

    file_version.major=0 ; file_version.minor=1 ; file_version.patch=0 ;
    max_supported.major=0  ; max_supported.minor=1  ; max_supported.patch=0  ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==1);  //file 0.1.0 max_supported 0.1.0

    max_supported.major=0  ; max_supported.minor=1  ; max_supported.patch=1  ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==1);  //file 0.1.0 max_supported 0.1.1

    max_supported.major=1  ; max_supported.minor=0  ; max_supported.patch=0  ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==1);  //file 0.1.0 max_supported 1.0.0

    file_version.major=1 ; file_version.minor=10 ; file_version.patch=10 ;
    max_supported.major=1  ; max_supported.minor=11  ; max_supported.patch=1  ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==1);  //file 1.10.10 max_supported 1.11.1

    max_supported.major=0  ; max_supported.minor=1  ; max_supported.patch=0  ;
    file_version.major=0 ; file_version.minor=1 ; file_version.patch=1 ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==0);  //file 0.1.1 max_supported 0.1.0

    file_version.major=1 ; file_version.minor=0 ; file_version.patch=0 ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==0);  //file 1.0.0 max_supported 0.1.0

    max_supported.major=1  ; max_supported.minor=10  ; max_supported.patch=3  ;
    file_version.major=1 ; file_version.minor=11 ; file_version.patch=1 ;
    ASSERT(slow5_is_version_compatible(file_version, max_supported)==0);  //file 1.11.1 max_supported 1.10.3



    return EXIT_SUCCESS;

}

int double_to_str_valid(void) {

    double x = 3.14;
    const char *x_str = "3.14";
    size_t len;

    char *str = slow5_double_to_str(x, &len);
    ASSERT(str);
    ASSERT(strcmp(str, x_str) == 0);
    ASSERT(len == strlen(x_str));

    free(str);

    double y = 3.00014;
    const char *y_str = "3.00014";

    str = slow5_double_to_str(y, &len);
    ASSERT(str);
    ASSERT(strcmp(str, y_str) == 0);
    ASSERT(len == strlen(y_str));

    free(str);

    /* 1 at the 15th digit (max double digits) */
    /*
     * Doesn't work "0", since we are using default precision (6 decimals)
    double z = -0000000000000.00000000000000100000;
    const char *z_str = "-0.000000000000001";

    str = slow5_double_to_str(z, &len);
    ASSERT(str);
    ASSERT(strcmp(str, z_str) == 0);
    ASSERT(len == strlen(z_str));

    free(str);
    */

    double a = 1467.61;
    const char *a_str = "1467.61";

    str = slow5_double_to_str(a, &len);
    ASSERT(str);
    ASSERT(strcmp(str, a_str) == 0);
    ASSERT(len == strlen(a_str));

    free(str);

    double b = -0.001;
    const char *b_str = "-0.001";

    str = slow5_double_to_str(b, &len);
    ASSERT(str);
    ASSERT(strcmp(str, b_str) == 0);
    ASSERT(len == strlen(b_str));

    free(str);

    /* negative 0 test */
    double c = -0.00000000000000001;
    const char *c_str = "0";

    str = slow5_double_to_str(c, &len);
    ASSERT(str);
    ASSERT(strcmp(str, c_str) == 0);
    ASSERT(len == strlen(c_str));

    free(str);

    double d = -0.000;
    const char *d_str = "0";

    str = slow5_double_to_str(d, &len);
    ASSERT(str);
    ASSERT(strcmp(str, d_str) == 0);
    ASSERT(len == strlen(d_str));

    free(str);

    return EXIT_SUCCESS;
}

int float_to_str_valid(void) {

    float x = 3.14;
    const char *x_str = "3.14";
    size_t len;

    char *str = slow5_float_to_str(x, &len);
    ASSERT(str);
    ASSERT(strcmp(str, x_str) == 0);
    ASSERT(len == strlen(x_str));

    free(str);

    float y = 3.00014;
    const char *y_str = "3.00014";

    str = slow5_float_to_str(y, &len);
    ASSERT(str);
    ASSERT(strcmp(str, y_str) == 0);
    ASSERT(len == strlen(y_str));

    free(str);

    /* 1 at the 6th digit (max float digits) */
    float z = -0000000000000.000001;
    const char *z_str = "-0.000001";

    str = slow5_float_to_str(z, &len);
    ASSERT(str);
    ASSERT(strcmp(str, z_str) == 0);
    ASSERT(len == strlen(z_str));

    free(str);

    /*
     * Doesn't work "1467.609985"
    float a = 1467.61;
    const char *a_str = "1467.61";

    str = slow5_float_to_str(a, &len);
    ASSERT(str);
    ASSERT(strcmp(str, a_str) == 0);
    ASSERT(len == strlen(a_str));

    free(str);
    */

    return EXIT_SUCCESS;
}

int main(void) {

    struct command tests[] = {
        // Helpers
        CMD(ato_xintx_valid)
        CMD(ato_xintx_invalid)

        CMD(strtod_check_valid)
        CMD(strtod_check_invalid)

        CMD(path_get_slow5_fmt_test)
        CMD(name_get_slow5_fmt_test)
        CMD(fmt_get_name_test)

        CMD(asprintf_mine_valid)

        CMD(version_compatible)

        CMD(double_to_str_valid)
        CMD(float_to_str_valid)
    };

    return RUN_TESTS(tests);
}
