/* 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 COPYING 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) parameters, one pointer and one integer, for that
 * function which are passed on unmodified.  The function should return the
 * number of bytes written, and 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
			 *    string (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;

		/* All other characters--this neatly catches "%%" too */
		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 'p':
		/* Print the NULL value specially */
		if (ptrval == NULL) {
		    total += writefunc("(null)", 6, arg1, arg2);
		    break;
		}
		/* 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(&c, 1, arg1, arg2))
			break;
		    total++;
		}
		while (width > 1) {
		    if (1 != writefunc(padstr, 1, arg1, arg2))
			break;
		    total++;
		    width--;
		}
		if (!left_justify)
		    total += writefunc(&c, 1, arg1, arg2);
		break;
	    } /* case 'c' */

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

		/* Catch null strings */
		if (strval == NULL) {
		    total += writefunc("(null)", 6, arg1, arg2);
		    break;
		}
		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)
	return 0;
    memcpy(*string, buf, len);
    (*string) += len;
    (*string)[0] = 0;
    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;
}

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