/* An implementation of vsnprintf() for systems that don't have it.
 *
 * IRC Services is copyright (c) 1996-2009 Andrew Church.
 *     E-mail: <achurch@achurch.org>
 * Parts written by Andrew Kempe and others.
 * This program is free but copyrighted software; see the file GPL.txt for
 * details.
 */

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

typedef int
    (*_pfmt_writefunc_t)(const char *buf, size_t len, void *arg1, void *arg2);

int my_vsnprintf(char *string, size_t size, const char *format, va_list args);
int my_snprintf(char *string, size_t size, const char *format, ...);

/*************************************************************************/

/* Basic format routine for *printf() interfaces.  Takes a writing function
 * and two (optional) pointer parameters for that function which are passed
 * on unmodified.  The function should return the number of bytes written,
 * or 0 (not -1!) on write failure.
 */

static int _pfmt(const char *format, va_list args,
                 _pfmt_writefunc_t writefunc, void *arg1, void *arg2)
{
    int total = 0;      /* Total bytes written */
    const char *startptr;/* Beginning of non-token text in format string.
                          *    Used for writing in bulk instead of
                          *    character-at-a-time. */
    int n;              /* Bytes written in last writefunc() call */
    int valid;          /* Was this a valid %-token? */
    int alt_form;       /* "Alternate form"? (# flag) */
    int zero_pad;       /* Zero-pad value? */
    int left_justify;   /* Left-justify?  (0 means right-justify) */
    int always_sign;    /* Always add sign value? */
    int insert_blank;   /* Insert blank before positive values for %d/%i? */
    int width;          /* Field width */
    int precision;      /* Precision */
    int argsize;        /* Size of argument: 0=normal, 1=short, 2=long,
                         *    3=long long */
    int what;           /* What are we working on?  0=flags, 1=width,
                         *    2=precision, 3=argsize, 4=argtype */
    long intval;        /* Integer value */
    char *strval;       /* String value */
    void *ptrval;       /* Pointer value */
    char numbuf[64];    /* Temporary buffer for printing numbers; this MUST
                         *    be large enough to hold the longest possible
                         *    number (size is not checked in processing) */
    char *numptr;       /* Pointer to start of printed number in `numbuf' */


    intval = 0;
    strval = NULL;
    ptrval = NULL;

    startptr = format;
    while (*format) {
        if (*format != '%') {
            format++;
            continue;
        }
        if (startptr != format) {
            /* Write out accumulated text */
            n = writefunc(startptr, format-startptr, arg1, arg2);
            total += n;
            /* Abort on short write */
            if (n != format-startptr)
                break;
            /* Point to this token, in case it's a bad one */
            startptr = format;
        }

        /* Begin %-token processing */
        valid = 0;      /* 1 if valid, -1 if known not valid (syntax error) */
        alt_form = 0;
        left_justify = 0;
        always_sign = 0;
        insert_blank = 0;
        zero_pad = 0;
        width = -1;
        precision = -1;
        argsize = 0;
        what = 0;

        while (!valid && *++format) {   /* Broken out of by terminal chars */
            switch (*format) {

                /* Flags */
                case '#':
                    if (what != 0) {
                        valid = -1;
                        break;
                    }
                    alt_form = 1;
                    break;
                case '-':
                    if (what != 0) {
                        valid = -1;
                        break;
                    }
                    left_justify = 1;
                    break;
                case '+':
                    if (what != 0) {
                        valid = -1;
                        break;
                    }
                    always_sign = 1;
                    break;
                case ' ':
                    if (what != 0) {
                        valid = -1;
                        break;
                    }
                    insert_blank = 1;
                    break;
                case '\'':
                    /* Comma-grouping by locale; not supported */
                    valid = -1;
                    break;
                case '0':
                    if (what == 0) {
                        zero_pad = 1;
                        break;
                    }
                    /* else fall through */

                /* Field widths */
                case '1':
                case '2':
                case '3':
                case '4':
                case '5':
                case '6':
                case '7':
                case '8':
                case '9':
                    if (what == 0)
                        what = 1;
                    else if (what > 2) {
                        valid = -1;
                        break;
                    }
                    if (what == 1) {
                        if (width < 0)
                            width = 0;
                        width = width*10 + (*format)-'0';
                    } else {
                        if (precision < 0)
                            precision = 0;
                        precision = precision*10 + (*format)-'0';
                    }
                    break;
                case '*':
                    if (what == 0)
                        what = 1;
                    else if (what > 2) {
                        valid = -1;
                        break;
                    }
                    if (what == 1) {
                        width = va_arg(args, int);
                        if (width < 0) {
                            width = -width;
                            left_justify = 1;
                        }
                    } else {
                        precision = va_arg(args, int);
                    }
                    break;
                case '.':
                    if (what >= 2) {
                        valid = -1;
                        break;
                    }
                    what = 2;
                    break;

                /* Argument sizes */
                case 'h':
                    if (what > 3) {
                        valid = -1;
                        break;
                    }
                    argsize = 1;
                    what = 4;
                    break;
                case 'l':
                    if (what > 3) {
                        valid = -1;
                        break;
                    }
                    argsize = 2;
                    what = 4;
                    break;
                case 'L':
                    /* Long long (64 bits); not supported */
                    valid = -1;
                    break;
                /* Argument types */
                case 'd':
                case 'i':
                case 'o':
                case 'u':
                case 'x':
                case 'X':
                    if (argsize == 1)
                        intval = va_arg(args, int);
                    else if (argsize == 2)
                        intval = va_arg(args, long);
                    else
                        intval = va_arg(args, int);
                    valid = 1;
                    break;
                case 'c':
                    intval = va_arg(args, int);
                    valid = 1;
                    break;
                case 's':
                    strval = va_arg(args, char *);
                    valid = 1;
                    break;
                case 'p':
                    ptrval = va_arg(args, void *);
                    valid = 1;
                    break;
                case 'n':
                    *((int *)va_arg(args, int *)) = total;
                    valid = 1;
                    break;
                case '%':
                    valid = 1;
                    break;

                /* All other characters */
                default:
                    valid = -1;
                    break;
            } /* switch (*format) */
        }
        if (valid != 1) {
            /* Not a valid %-token; start loop over (token will get printed
             * out next time through). */
            continue;
        }

        /* Don't zero-pad if a precision was given or left-justifying */
        if (precision != -1 || left_justify)
            zero_pad = 0;

        /* For numbers, limit precision to the size of the print buffer */
        if ((*format=='d' || *format=='i' || *format=='o' || *format=='u'
                          || *format=='x' || *format=='X')
                        && precision > (signed) sizeof(numbuf))
        {
            precision = sizeof(numbuf);
        }

        switch (*format++) {    /* Do something with this token */
            case '%':
                (*writefunc)(format-1, 1, arg1, arg2);
                total++;
                break;

            case 'p':
                /* Print the NULL value specially */
                if (ptrval == NULL) {
                    strval = "(null)";
                    goto handle_string;
                }
                /* For all other values, pretend it's really %#.8x */
                alt_form = 1;
                zero_pad = 0;
                precision = 8;
                intval = (long) ptrval;
                /* Fall through */

            case 'x':
            case 'X': {
                static const char x_chars[] = "0123456789abcdef0x";
                static const char X_chars[] = "0123456789ABCDEF0X";
                const char *chars = (format[-1]=='X') ? X_chars : x_chars;
                const char *padstr = zero_pad ? "0" : " ";
                unsigned long uintval;
                int len;

                uintval = (unsigned long) intval;
                if (alt_form && uintval != 0) {
                    n = writefunc(chars+16, 2, arg1, arg2);
                    total += n;
                    if (n != 2)
                        break;
                    width -= 2;
                }
                if (precision < 1)
                    precision = 1;
                numptr = numbuf + sizeof(numbuf);
                for (len = 0; len < precision || uintval != 0; len++) {
                    *--numptr = chars[uintval%16];
                    uintval /= 16;
                }
                if (left_justify) {
                    n = writefunc(numptr, len, arg1, arg2);
                    total += n;
                    if (n != len)
                        break;
                }
                while (len < width) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify)
                    total += writefunc(numptr, len, arg1, arg2);
                break;
            } /* case 'x', 'X' */

            case 'o': {
                const char *padstr = zero_pad ? "0" : " ";
                unsigned long uintval;
                int len;

                uintval = (unsigned long) intval;
                if (precision < 1)
                    precision = 1;
                numptr = numbuf + sizeof(numbuf);
                for (len = 0; len < precision || uintval != 0; len++) {
                    *--numptr = '0' + uintval%8;
                    uintval /= 8;
                }
                if (alt_form && *numptr != '0') {
                    *--numptr = '0';
                    len++;
                }
                if (left_justify) {
                    n = writefunc(numptr, len, arg1, arg2);
                    total += n;
                    if (n != len)
                        break;
                }
                while (len < width) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify)
                    total += writefunc(numptr, len, arg1, arg2);
                break;
            } /* case 'o' */

            case 'u': {
                const char *padstr = zero_pad ? "0" : " ";
                unsigned long uintval;
                int len;

                uintval = (unsigned long) intval;
                if (precision < 1)
                    precision = 1;
                numptr = numbuf + sizeof(numbuf);
                for (len = 0; len < precision || uintval != 0; len++) {
                    *--numptr = '0' + uintval%10;
                    uintval /= 10;
                }
                if (left_justify) {
                    n = writefunc(numptr, len, arg1, arg2);
                    total += n;
                    if (n != len)
                        break;
                }
                while (len < width) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify)
                    total += writefunc(numptr, len, arg1, arg2);
                break;
            } /* case 'u' */

            case 'd':
            case 'i': {
                const char *padstr = zero_pad ? "0" : " ";
                int len;
                char sign_char;

                numptr = numbuf + sizeof(numbuf);
                len = 0;
                sign_char = 0;
                if (intval < 0) {
                    sign_char = '-';
                    intval = -intval;
                    if (intval < 0) {   /* true for 0x800...0 */
                        *numptr-- = '0' - intval%10;
                        len++;
                        intval /= 10;
                        intval = -intval;
                    }
                }
                if (precision < 1)
                    precision = 1;
                for (; len < precision || intval != 0; len++) {
                    *--numptr = '0' + intval%10;
                    intval /= 10;
                }
                if (!sign_char) {
                    if (always_sign)
                        sign_char = '+';
                    else if (insert_blank)
                        sign_char = ' ';
                }
                if (sign_char) {
                    if (zero_pad) {
                        if (1 != writefunc(&sign_char, 1, arg1, arg2))
                            break;
                        total++;
                        width--;
                    } else {
                        *--numptr = sign_char;
                        len = 0;
                    }
                }
                if (left_justify) {
                    n = writefunc(numptr, len, arg1, arg2);
                    total += n;
                    if (n != len)
                        break;
                }
                while (len < width) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify)
                    total += writefunc(numptr, len, arg1, arg2);
                break;
            } /* case 'd', 'i' */

            case 'c': {
                const char *padstr = zero_pad ? "0" : " ";
                unsigned char c = (unsigned char) intval;

                if (left_justify) {
                    if (1 != writefunc((char *)&c, 1, arg1, arg2))
                        break;
                    total++;
                }
                while (width > 1) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify)
                    total += writefunc((char *)&c, 1, arg1, arg2);
                break;
            } /* case 'c' */

            case 's': {
                const char *padstr;
                int len;

                /* Catch null strings */
                if (strval == NULL) {
                    strval = "(null)";
                }
              handle_string:  /* NULL pointers for %p come here */
                padstr = zero_pad ? "0" : " ";
                len = strlen(strval);
                if (precision < 0 || precision > len)
                    precision = len;
                if (left_justify && precision > 0) {
                    n = writefunc(strval, precision, arg1, arg2);
                    total += n;
                    if (n != precision)
                        break;
                }
                while (width > precision) {
                    if (1 != writefunc(padstr, 1, arg1, arg2))
                        break;
                    total++;
                    width--;
                }
                if (!left_justify && precision > 0)
                    total += writefunc(strval, precision, arg1, arg2);
                break;
            } /* case 's' */

        } /* switch (*format++) */

        startptr = format;      /* Start again after this %-token */
    } /* while (*format) */

    /* Write anything left over. */
    if (startptr != format)
        total += writefunc(startptr, format-startptr, arg1, arg2);

    /* Return total bytes written. */
    return total;
}

/*************************************************************************/

static int writefunc(const char *buf, size_t len, char **string, size_t *size)
{
    if ((*size) <= 0)
        return 0;
    if (len > (*size)-1)
        len = (*size)-1;
    if (len > 0)
        memcpy((*string), buf, len);
    (*string) += len;
    (*string)[0] = 0;
    (*size) -= len;
    return len;
}

int my_vsnprintf(char *string, size_t size, const char *format, va_list args)
{
    int ret;

    if (size <= 0)
        return 0;
    ret = _pfmt(format, args, (_pfmt_writefunc_t) writefunc, &string, &size);
    return ret;
}

int my_snprintf(char *string, size_t size, const char *format, ...)
{
    va_list args;
    int ret;

    va_start(args, format);
    ret = my_vsnprintf(string, size, format, args);
    va_end(args);
    return ret;
}

/*************************************************************************/

/*
 * Local variables:
 *   c-file-style: "stroustrup"
 *   c-file-offsets: ((case-label . *) (statement-case-intro . *))
 *   indent-tabs-mode: nil
 * End:
 *
 * vim: expandtab shiftwidth=4:
 */
