/* Copyright © 2013 Bart Massey */
/* This program is licensed under the GPL version 2 or later.
   Please see the file COPYING.GPL2 in this distribution for
   license terms. */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
#include <wchar.h>
#include <locale.h>

#ifndef __PICOLIBC__
#define printf_float(x) ((double)(x))
#endif

#define TEST_ASPRINTF

#ifdef __PICOLIBC__
#define TEST_ASNPRINTF
#endif

#define PRINTF_BUF_SIZE 512

static char    buf[PRINTF_BUF_SIZE];
static wchar_t wbuf[PRINTF_BUF_SIZE];

static void
failmsg(int serial, char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    printf("test %d failed: ", serial);
    vprintf(fmt, ap);
    printf("\n");
    va_end(ap);
}

#if ((__GNUC__ == 4 && __GNUC_MINOR__ >= 2) || __GNUC__ > 4)
#pragma GCC diagnostic ignored "-Wpragmas"
#pragma GCC diagnostic ignored "-Wunknown-warning-option"
/* 'bsize' is used directly with malloc/realloc which confuses -fanalyzer */
#pragma GCC diagnostic ignored "-Wanalyzer-va-arg-type-mismatch"
#pragma GCC diagnostic ignored "-Wanalyzer-va-list-exhausted"
#endif

#define FAIL_LEN ((size_t)-1)

static int
vtestl(int serial, char *expect, size_t expect_len, char *fmt, va_list ap)
{
    char *abuf = NULL;
    char *as = NULL;
    int   n;
    int   ret = 0;
    int   expect_ret = (int)expect_len;
    int   expect_fail = expect_len == FAIL_LEN;
    if (expect_fail) {
        expect_ret = -1;
        expect_len = strlen(expect);
    }
#ifdef TEST_ASPRINTF
    int     an;
    va_list aap;
    va_copy(aap, ap);
#endif
#ifdef TEST_ASNPRINTF
    static char as_buf[16];
    size_t      as_len;
    va_list     aanp;
    va_copy(aanp, ap);
#endif
#ifndef __IO_NO_FLOATING_POINT
#ifdef _HAS_IO_FLOAT
    uint32_t dv;
#else
    double dv;
#endif
#ifndef NO_LONG_DOUBLE
    long double ldv;
#endif
    char *star;
    char *long_double;
#endif
    switch (fmt[strlen(fmt) - 1]) {
    case 'e':
    case 'E':
    case 'f':
    case 'F':
    case 'g':
    case 'G':
    case 'a':
    case 'A':
#ifdef __IO_NO_FLOATING_POINT
        return 0;
#else
        star = strchr(fmt, '*');
        long_double = strchr(fmt, 'L');
        if (star) {
            if (strchr(star + 1, '*')) {
                int iv1 = va_arg(ap, int);
                int iv2 = va_arg(ap, int);
#ifndef NO_LONG_DOUBLE
                if (long_double) {
                    ldv = va_arg(ap, long double);
                    n = snprintf(buf, PRINTF_BUF_SIZE, fmt, iv1, iv2, ldv);
#ifdef TEST_ASPRINTF
                    an = asprintf(&abuf, fmt, iv1, iv2, ldv);
#endif
#ifdef TEST_ASNPRINTF
                    as_len = sizeof(as_buf);
                    as = asnprintf(as_buf, &as_len, fmt, iv1, iv2, ldv);
#endif
                } else
#endif
                {
                    dv = va_arg(ap, __typeof(dv));
                    n = snprintf(buf, PRINTF_BUF_SIZE, fmt, iv1, iv2, dv);
#ifdef TEST_ASPRINTF
                    an = asprintf(&abuf, fmt, iv1, iv2, dv);
#endif
#ifdef TEST_ASNPRINTF
                    as_len = sizeof(as_buf);
                    as = asnprintf(as_buf, &as_len, fmt, iv1, iv2, dv);
#endif
                }
            } else {
                int iv = va_arg(ap, int);
#ifndef NO_LONG_DOUBLE
                if (long_double) {
                    ldv = va_arg(ap, long double);
                    n = snprintf(buf, PRINTF_BUF_SIZE, fmt, iv, ldv);
#ifdef TEST_ASPRINTF
                    an = asprintf(&abuf, fmt, iv, ldv);
#endif
#ifdef TEST_ASNPRINTF
                    as_len = sizeof(as_buf);
                    as = asnprintf(as_buf, &as_len, fmt, iv, ldv);
#endif
                } else
#endif
                {
                    dv = va_arg(ap, __typeof(dv));
                    n = snprintf(buf, PRINTF_BUF_SIZE, fmt, iv, dv);
#ifdef TEST_ASPRINTF
                    an = asprintf(&abuf, fmt, iv, dv);
#endif
#ifdef TEST_ASNPRINTF
                    as_len = sizeof(as_buf);
                    as = asnprintf(as_buf, &as_len, fmt, iv, dv);
#endif
                }
            }
        } else {
#ifndef NO_LONG_DOUBLE
            if (long_double) {
                ldv = va_arg(ap, long double);
                n = snprintf(buf, PRINTF_BUF_SIZE, fmt, ldv);
#ifdef TEST_ASPRINTF
                an = asprintf(&abuf, fmt, ldv);
#endif
#ifdef TEST_ASNPRINTF
                as_len = sizeof(as_buf);
                as = asnprintf(as_buf, &as_len, fmt, ldv);
#endif
            } else
#endif
            {
                dv = va_arg(ap, __typeof(dv));
                n = snprintf(buf, PRINTF_BUF_SIZE, fmt, dv);
#ifdef TEST_ASPRINTF
                an = asprintf(&abuf, fmt, dv);
#endif
#ifdef TEST_ASNPRINTF
                as_len = sizeof(as_buf);
                as = asnprintf(as_buf, &as_len, fmt, dv);
#endif
            }
        }
        break;
#endif
    default:
        n = vsnprintf(buf, PRINTF_BUF_SIZE, fmt, ap);
#ifdef TEST_ASPRINTF
        an = vasprintf(&abuf, fmt, aap);
#endif
#ifdef TEST_ASNPRINTF
        as_len = sizeof(as_buf);
        as = vasnprintf(as_buf, &as_len, fmt, aanp);
#endif
        break;
    }
    //    printf("serial %d expect \"%*.*s\" got \"%s\"\n", serial, expect_len, expect_len, expect,
    //    buf);
    if (n >= PRINTF_BUF_SIZE) {
        failmsg(serial, "buffer overflow");
        ret = 1;
    }
    if (n != expect_ret) {
        failmsg(serial, "expected \"%s\" (%d), got \"%s\" (%d)", expect, expect_ret, buf, n);
        ret = 1;
    }
    if (memcmp(buf, expect, expect_len)) {
        failmsg(serial, "expected \"%*.*s\", got \"%*.*s\"", expect_len, expect_len, expect,
                expect_len, expect_len, buf);
        ret = 1;
    }
#ifdef TEST_ASPRINTF
    va_end(aap);
    if (an != n) {
        failmsg(serial, "asprintf return %d sprintf return %d", an, n);
        ret = 1;
    }
    if (abuf && strcmp(abuf, buf)) {
        failmsg(serial, "sprintf return %s asprintf return %s", buf, abuf);
        ret = 1;
    }
#endif
#ifdef TEST_ASNPRINTF
    va_end(aanp);
    if (expect_fail && as != NULL) {
        failmsg(serial, "asnprintf return %s expected failure", as);
        ret = 1;
    }
    if (as && as_len != (size_t)n) {
        failmsg(serial, "asnprintf return %d sprintf return %d", as_len, n);
        ret = 1;
    }
    if (as && strcmp(as, buf)) {
        failmsg(serial, "sprintf return %s asnprintf return %s", buf, as);
        ret = 1;
    }
    if (as_len + 1 <= sizeof(as_buf)) {
        if (as != as_buf)
            failmsg(serial, "asnprintf shouldn't have allocated at %d\n", n);
    } else {
        if (as == as_buf)
            failmsg(serial, "asnprintf should have allocated at %d\n", n);
    }
    if (as == as_buf)
        as = NULL;
#endif
    if (as == abuf)
        as = NULL;
    free(as);
    free(abuf);
    return ret;
}

static int
testl(int serial, char *expect, size_t expect_len, char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int ret = vtestl(serial, expect, expect_len, fmt, ap);
    va_end(ap);
    return ret;
}

static int
test(int serial, char *expect, char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int ret = vtestl(serial, expect, strlen(expect), fmt, ap);
    va_end(ap);
    return ret;
}

static void
failmsgw(int serial, wchar_t *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    printf("test %d failed: ", serial);
    static wchar_t f_wbuf[PRINTF_BUF_SIZE];
    static char    f_buf[PRINTF_BUF_SIZE];
    vswprintf(f_wbuf, PRINTF_BUF_SIZE, fmt, ap);
    wcstombs(f_buf, f_wbuf, PRINTF_BUF_SIZE);
    printf("%s\n", f_buf);
    va_end(ap);
}

static int
vtestwl(int serial, wchar_t *expect, size_t expect_len, wchar_t *fmt, va_list ap)
{
    wchar_t *abuf = NULL;
    int      n;
#ifndef __IO_NO_FLOATING_POINT
#ifdef _HAS_IO_FLOAT
    uint32_t dv;
#else
    double dv;
#endif
    wchar_t *star;
#endif
    switch (fmt[wcslen(fmt) - 1]) {
    case 'e':
    case 'E':
    case 'f':
    case 'F':
    case 'g':
    case 'G':
    case 'a':
    case 'A':
#ifdef __IO_NO_FLOATING_POINT
        return 0;
#else
        star = wcschr(fmt, '*');
        if (star) {
            if (wcschr(star + 1, '*')) {
                int iv1 = va_arg(ap, int);
                int iv2 = va_arg(ap, int);
                dv = va_arg(ap, __typeof(dv));
                n = swprintf(wbuf, PRINTF_BUF_SIZE, fmt, iv1, iv2, dv);
            } else {
                int iv = va_arg(ap, int);
                dv = va_arg(ap, __typeof(dv));
                n = swprintf(wbuf, PRINTF_BUF_SIZE, fmt, iv, dv);
            }
        } else {
            dv = va_arg(ap, __typeof(dv));
            n = swprintf(wbuf, PRINTF_BUF_SIZE, fmt, dv);
        }
        break;
#endif
    default:
        n = vswprintf(wbuf, PRINTF_BUF_SIZE, fmt, ap);
        break;
    }
    //    printf("serial %d expect \"%s\" got \"%s\"\n", serial, expect, wbuf);
    if (n >= PRINTF_BUF_SIZE) {
        failmsgw(serial, L"buffer overflow");
        free(abuf);
        return 1;
    }
    if (n != (int)expect_len) {
        failmsgw(serial, L"expected \"%*.*ls\" (%d), got \"%*.*ls\" (%d)", expect_len, expect_len,
                 expect, expect_len, n, n, wbuf, n);
        free(abuf);
        return 1;
    }
    if (wcscmp(wbuf, expect)) {
        failmsgw(serial, L"expected \"%*.*ls\", got \"%*.*ls\"", expect_len, expect_len, expect, n,
                 n, wbuf);
        free(abuf);
        return 1;
    }
    return 0;
}

int testwl(int serial, wchar_t *expect, size_t expect_len, wchar_t *fmt, ...);

int
testwl(int serial, wchar_t *expect, size_t expect_len, wchar_t *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int ret = vtestwl(serial, expect, expect_len, fmt, ap);
    va_end(ap);
    return ret;
}

static int
testw(int serial, wchar_t *expect, wchar_t *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int ret = vtestwl(serial, expect, wcslen(expect), fmt, ap);
    va_end(ap);
    return ret;
}

int
main(void)
{
    int result = 0;
#if !defined(__PICOLIBC__) || defined(__MB_CAPABLE)
    if (!setlocale(LC_CTYPE, "C.UTF-8")) {
        printf("setlocale(LC_CTYPE, \"C.UTF-8\") failed\n");
        return 1;
    }
#endif
#include "test-printf-testcases.h"
    return result;
}
