//
// tardy - a tar post-processor
// Copyright (C) 1991-2001, 2002, 2004, 2008, 2009, 2011, 2012 Peter Miller
//
// 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, see <http://www.gnu.org/licenses/>.
//

#include <libtardy/ac/assert.h>
#include <libtardy/ac/errno.h>
#include <libtardy/ac/stdarg.h>
#include <libtardy/ac/stdio.h>
#include <libtardy/ac/stdlib.h>
#include <libtardy/ac/string.h>
#include <libexplain/output.h>
#include <libexplain/realloc.h>

#include <libtardy/mprintf.h>
#include <libtardy/rcstring.h>

//
// size to grow memory by
//
#define QUANTUM 200

//
// maximum width for numbers
//
#define MAX_WIDTH (QUANTUM - 1)

//
// the buffer for storing results
//
static size_t   tmplen;
static size_t   length;
static char     *tmp;


//
// NAME
//      bigger - grow dynamic memory buffer
//
// SYNOPSIS
//      int bigger(void);
//
// DESCRIPTION
//      The bigger function is used to grow the dynamic memory buffer
//      used by vmprintf to store the formatting results.
//      The buffer is increased by QUANTUM bytes.
//
// RETURNS
//      int; zero if failed to realloc memory, non-zero if successful.
//
// CAVEATS
//      The existing buffer is still valid after failure.
//

static int
bigger(void)
{
    size_t nbytes = tmplen + QUANTUM;
    char *hold = (char *)explain_realloc_or_die(tmp, nbytes);
    tmplen = nbytes;
    tmp = hold;
    return 1;
}


static void
build_fake(char *fake, size_t fakesize, int flag, int width, int precision,
    int qualifier, int specifier)
{
    char *fp = fake;
    *fp++ = '%';
    if (flag)
        *fp++ = flag;
    if (width > 0)
    {
        snprintf(fp, fake + fakesize - fp, "%d", width);
        fp += strlen(fp);
    }
    *fp++ = '.';
    snprintf(fp, fake + fakesize - fp, "%d", precision);
    fp += strlen(fp);
    if (qualifier)
        *fp++ = qualifier;
    *fp++ = specifier;
    *fp = 0;
}


char *
vmprintf_errok(const char *fmt, va_list ap)
{

    //
    // Build the result string in a temporary buffer.
    // Grow the temporary buffer as necessary.
    //
    // It is important to only make one pass across the variable argument
    // list.  Behaviour is undefined for more than one pass.
    //
    if (!tmplen)
    {
        tmplen = 500;
        errno = ENOMEM;
        tmp = (char *)malloc(tmplen);
        if (!tmp)
            return 0;
    }

    length = 0;
    const char *s = fmt;
    while (*s)
    {
        int c = *s++;
        if (c != '%')
        {
            normal:
            if (length >= tmplen && !bigger())
                return 0;
            tmp[length++] = c;
            continue;
        }
        c = *s++;

        //
        // get optional flag
        //
        int flag = 0;
        switch (c)
        {
        case '+':
        case '-':
        case '#':
        case '0':
        case ' ':
            flag = c;
            c = *s++;
            break;

        default:
            break;
        }

        //
        // get optional width
        //
        int width = 0;
        int width_set = 0;
        switch (c)
        {
        case '*':
            width = va_arg(ap, int);
            if (width < 0)
            {
                flag = '-';
                width = -width;
            }
            c = *s++;
            width_set = 1;
            break;

        case '0': case '1': case '2': case '3': case '4':
        case '5': case '6': case '7': case '8': case '9':
            for (;;)
            {
                width = width * 10 + c - '0';
                c = *s++;
                switch (c)
                {
                default:
                    break;

                case '0': case '1': case '2': case '3':
                case '4': case '5': case '6': case '7':
                case '8': case '9':
                    continue;
                }
                break;
            }
            width_set = 1;
            break;

        default:
            break;
        }

        //
        // get optional precision
        //
        int prec = 0;
        int prec_set = 0;
        if (c == '.')
        {
            c = *s++;
            switch (c)
            {
            default:
                prec_set = 1;
                break;

            case '*':
                c = *s++;
                prec = va_arg(ap, int);
                if (prec < 0)
                {
                    prec = 0;
                    break;
                }
                prec_set = 1;
                break;

            case '0': case '1': case '2': case '3': case '4':
            case '5': case '6': case '7': case '8': case '9':
                for (;;)
                {
                    prec = prec * 10 + c - '0';
                    c = *s++;
                    switch (c)
                    {
                    default:
                        break;

                    case '0': case '1': case '2': case '3':
                    case '4': case '5': case '6': case '7':
                    case '8': case '9':
                        continue;
                    }
                    break;
                }
                prec_set = 1;
                break;
            }
        }

        //
        // get the optional qualifier
        //
        int qualifier = 0;
        switch (c)
        {
        default:
            break;

        case 'l':
        case 'h':
        case 'L':
            qualifier = c;
            c = *s++;
            break;
        }

        //
        // get conversion specifier
        //
        switch (c)
        {
        default:
            errno = EINVAL;
            return 0;

        case '%':
            goto normal;

        case 'c':
            {
                int a = (unsigned char)va_arg(ap, int);
                if (!prec_set)
                    prec = 1;
                if (width > MAX_WIDTH)
                    width = MAX_WIDTH;
                if (prec > MAX_WIDTH)
                    prec = MAX_WIDTH;
                char fake[QUANTUM - 1];
                build_fake(fake, sizeof(fake), flag, width, prec, 0, c);
                char num[MAX_WIDTH + 1];
                snprintf(num, sizeof(num), fake, a);
                size_t len = strlen(num);
                assert(len < QUANTUM);
                if (length + len > tmplen && !bigger())
                    return 0;
                memcpy(tmp + length, num, len);
                length += len;
            }
            break;

        case 'd':
        case 'i':
            {
                long a = 0;
                switch (qualifier)
                {
                case 'l':
                    a = va_arg(ap, long);
                    break;

                case 'h':
                    a = (short)va_arg(ap, int);
                    break;

                default:
                    a = va_arg(ap, int);
                    break;
                }
                if (!prec_set)
                    prec = 1;
                if (width > MAX_WIDTH)
                    width = MAX_WIDTH;
                if (prec > MAX_WIDTH)
                    prec = MAX_WIDTH;
                char fake[QUANTUM - 1];
                build_fake(fake, sizeof(fake), flag, width, prec, 'l', c);
                char num[MAX_WIDTH + 1];
                snprintf(num, sizeof(num), fake, a);
                size_t len = strlen(num);
                assert(len < QUANTUM);
                if (length + len > tmplen && !bigger())
                    return 0;
                memcpy(tmp + length, num, len);
                length += len;
            }
            break;

        case 'e':
        case 'f':
        case 'g':
        case 'E':
        case 'F':
        case 'G':
            {
                //
                // Ignore "long double" for now,
                // traditional implementations no grok.
                //
                double a = va_arg(ap, double);
                if (!prec_set)
                    prec = 6;
                if (width > MAX_WIDTH)
                    width = MAX_WIDTH;
                if (prec > MAX_WIDTH)
                    prec = MAX_WIDTH;
                char fake[QUANTUM - 1];
                build_fake(fake, sizeof(fake), flag, width, prec, 0, c);
                char num[MAX_WIDTH + 1];
                snprintf(num, sizeof(num), fake, a);
                size_t len = strlen(num);
                assert(len < QUANTUM);
                if (length + len > tmplen && !bigger())
                    return 0;
                memcpy(tmp + length, num, len);
                length += len;
            }
            break;

        case 'n':
            switch (qualifier)
            {
            case 'l':
                {
                    long *a = va_arg(ap, long *);
                    *a = length;
                }
                break;

            case 'h':
                {
                    short *a = va_arg(ap, short *);
                    *a = length;
                }
                break;

            default:
                {
                    int *a = va_arg(ap, int *);
                    *a = length;
                }
                break;
            }
            break;

        case 'u':
        case 'o':
        case 'x':
        case 'X':
            {
                unsigned long a = 0;
                switch (qualifier)
                {
                case 'l':
                    a = va_arg(ap, unsigned long);
                    break;

                case 'h':
                    a = (unsigned short)va_arg(ap, unsigned int);
                    break;

                default:
                    a = va_arg(ap, unsigned int);
                    break;
                }
                if (!prec_set)
                    prec = 1;
                if (prec > MAX_WIDTH)
                    prec = MAX_WIDTH;
                if (width > MAX_WIDTH)
                    width = MAX_WIDTH;
                char fake[QUANTUM - 1];
                build_fake(fake, sizeof(fake), flag, width, prec, 'l', c);
                char num[MAX_WIDTH + 1];
                snprintf(num, sizeof(num), fake, a);
                size_t len = strlen(num);
                assert(len < QUANTUM);
                if (length + len > tmplen && !bigger())
                    return 0;
                memcpy(tmp + length, num, len);
                length += len;
            }
            break;

        case 's':
            {
                char *a = va_arg(ap, char *);
                size_t len = 0;
                if (prec_set)
                {
                    char *ep = (char *)memchr(a, 0, prec);
                    if (ep)
                        len = ep - a;
                    else
                        len = prec;
                }
                else
                    len = strlen(a);
                if (!prec_set || (int)len < prec)
                    prec = len;
                if (!width_set || width < prec)
                    width = prec;
                len = width;
                while (length + len > tmplen)
                {
                    if (!bigger())
                        return 0;
                }
                if (flag != '-')
                {
                    while (width > prec)
                    {
                        tmp[length++] = ' ';
                        width--;
                    }
                }
                memcpy(tmp + length, a, prec);
                length += prec;
                width -= prec;
                if (flag == '-')
                {
                    while (width > 0)
                    {
                        tmp[length++] = ' ';
                        width--;
                    }
                }
            }
            break;
        }
    }

    //
    // append a trailing NUL
    //
    if (length >= tmplen && !bigger())
            return 0;
    tmp[length] = 0;

    //
    // return the temporary string
    //
    return tmp;
}


char *
mprintf_errok(const char *fmt, ...)
{
        va_list ap;
        va_start(ap, fmt);
        char *result = vmprintf_errok(fmt, ap);
        va_end(ap);
        return result;
}


char *
vmprintf(const char *fmt, va_list ap)
{
    char *result = vmprintf(fmt, ap);
    if (!result)
        explain_output_error_and_die("mprintf \"%s\"", fmt);
    return result;
}


char *
mprintf(const char *fmt, ...)
{
        va_list ap;
        va_start(ap, fmt);
        char *result = vmprintf(fmt, ap);
        va_end(ap);
        return result;
}


rcstring
vmprintf_str(const char *fmt, va_list ap)
{
    if (!vmprintf_errok(fmt, ap))
        explain_output_error_and_die("mprintf \"%s\"", fmt);
    return rcstring(tmp, length);
}


// vim: set ts=8 sw=4 et :
