/*
 * vsnprintf.c
 *
 * vsnprintf(), from which the rest of the printf()
 * family is built
 */

#include <stdarg.h>
#include <stddef.h>
#include <inttypes.h>
#include <string.h>
#include <limits.h>
#include <stdio.h>

enum flags {
    FL_ZERO = 0x01,		/* Zero modifier */
    FL_MINUS = 0x02,		/* Minus modifier */
    FL_PLUS = 0x04,		/* Plus modifier */
    FL_TICK = 0x08,		/* ' modifier */
    FL_SPACE = 0x10,		/* Space modifier */
    FL_HASH = 0x20,		/* # modifier */
    FL_SIGNED = 0x40,		/* Number is signed */
    FL_UPPER = 0x80		/* Upper case digits */
};

/* These may have to be adjusted on certain implementations */
enum ranks {
    rank_char = -2,
    rank_short = -1,
    rank_int = 0,
    rank_long = 1,
    rank_longlong = 2
};

#define MIN_RANK	rank_char
#define MAX_RANK	rank_longlong

#define INTMAX_RANK	rank_longlong
#define SIZE_T_RANK	rank_long
#define PTRDIFF_T_RANK	rank_long

#define EMIT(x) ({ if (o<n){*q++ = (x);} o++; })

static size_t
format_int(char *q, size_t n, uintmax_t val, enum flags flags,
	   int base, int width, int prec)
{
    char *qq;
    size_t o = 0, oo;
    static const char lcdigits[] = "0123456789abcdef";
    static const char ucdigits[] = "0123456789ABCDEF";
    const char *digits;
    uintmax_t tmpval;
    int minus = 0;
    int ndigits = 0, nchars;
    int tickskip, b4tick;

    /* Select type of digits */
    digits = (flags & FL_UPPER) ? ucdigits : lcdigits;

    /* If signed, separate out the minus */
    if (flags & FL_SIGNED && (intmax_t) val < 0) {
	minus = 1;
	val = (uintmax_t) (-(intmax_t) val);
    }

    /* Count the number of digits needed.  This returns zero for 0. */
    tmpval = val;
    while (tmpval) {
	tmpval /= base;
	ndigits++;
    }

    /* Adjust ndigits for size of output */

    if (flags & FL_HASH && base == 8) {
	if (prec < ndigits + 1)
	    prec = ndigits + 1;
    }

    if (ndigits < prec) {
	ndigits = prec;		/* Mandatory number padding */
    } else if (val == 0) {
	ndigits = 1;		/* Zero still requires space */
    }

    /* For ', figure out what the skip should be */
    if (flags & FL_TICK) {
	tickskip = (base == 16) ? 4 : 3;
    } else {
	tickskip = ndigits;	/* No tick marks */
    }

    /* Tick marks aren't digits, but generated by the number converter */
    ndigits += (ndigits - 1) / tickskip;

    /* Now compute the number of nondigits */
    nchars = ndigits;

    if (minus || (flags & (FL_PLUS | FL_SPACE)))
	nchars++;		/* Need space for sign */
    if ((flags & FL_HASH) && base == 16) {
	nchars += 2;		/* Add 0x for hex */
    }

    /* Emit early space padding */
    if (!(flags & (FL_MINUS | FL_ZERO)) && width > nchars) {
	while (width > nchars) {
	    EMIT(' ');
	    width--;
	}
    }

    /* Emit nondigits */
    if (minus)
	EMIT('-');
    else if (flags & FL_PLUS)
	EMIT('+');
    else if (flags & FL_SPACE)
	EMIT(' ');

    if ((flags & FL_HASH) && base == 16) {
	EMIT('0');
	EMIT((flags & FL_UPPER) ? 'X' : 'x');
    }

    /* Emit zero padding */
    if ((flags & (FL_MINUS | FL_ZERO)) == FL_ZERO && width > ndigits) {
	while (width > nchars) {
	    EMIT('0');
	    width--;
	}
    }

    /* Generate the number.  This is done from right to left. */
    q += ndigits;		/* Advance the pointer to end of number */
    o += ndigits;
    qq = q;
    oo = o;			/* Temporary values */

    b4tick = tickskip;
    while (ndigits > 0) {
	if (!b4tick--) {
	    qq--;
	    oo--;
	    ndigits--;
	    if (oo < n)
		*qq = '_';
	    b4tick = tickskip - 1;
	}
	qq--;
	oo--;
	ndigits--;
	if (oo < n)
	    *qq = digits[val % base];
	val /= base;
    }

    /* Emit late space padding */
    while ((flags & FL_MINUS) && width > nchars) {
	EMIT(' ');
	width--;
    }

    return o;
}

int vsnprintf(char *buffer, size_t n, const char *format, va_list ap)
{
    const char *p = format;
    char ch;
    char *q = buffer;
    size_t o = 0;		/* Number of characters output */
    uintmax_t val = 0;
    int rank = rank_int;	/* Default rank */
    int width = 0;
    int prec = -1;
    int base;
    size_t sz;
    enum flags flags = 0;
    enum {
	st_normal,		/* Ground state */
	st_flags,		/* Special flags */
	st_width,		/* Field width */
	st_prec,		/* Field precision */
	st_modifiers		/* Length or conversion modifiers */
    } state = st_normal;
    const char *sarg;		/* %s string argument */
    char carg;			/* %c char argument */
    int slen;			/* String length */

    while ((ch = *p++)) {
	switch (state) {
	case st_normal:
	    if (ch == '%') {
		state = st_flags;
		flags = 0;
		rank = rank_int;
		width = 0;
		prec = -1;
	    } else {
		EMIT(ch);
	    }
	    break;

	case st_flags:
	    switch (ch) {
	    case '-':
		flags |= FL_MINUS;
		break;
	    case '+':
		flags |= FL_PLUS;
		break;
	    case '\'':
		flags |= FL_TICK;
		break;
	    case ' ':
		flags |= FL_SPACE;
		break;
	    case '#':
		flags |= FL_HASH;
		break;
	    case '0':
		flags |= FL_ZERO;
		break;
	    default:
		state = st_width;
		p--;		/* Process this character again */
		break;
	    }
	    break;

	case st_width:
	    if (ch >= '0' && ch <= '9') {
		width = width * 10 + (ch - '0');
	    } else if (ch == '*') {
		width = va_arg(ap, int);
		if (width < 0) {
		    width = -width;
		    flags |= FL_MINUS;
		}
	    } else if (ch == '.') {
		prec = 0;	/* Precision given */
		state = st_prec;
	    } else {
		state = st_modifiers;
		p--;		/* Process this character again */
	    }
	    break;

	case st_prec:
	    if (ch >= '0' && ch <= '9') {
		prec = prec * 10 + (ch - '0');
	    } else if (ch == '*') {
		prec = va_arg(ap, int);
		if (prec < 0)
		    prec = -1;
	    } else {
		state = st_modifiers;
		p--;		/* Process this character again */
	    }
	    break;

	case st_modifiers:
	    switch (ch) {
		/* Length modifiers - nonterminal sequences */
	    case 'h':
		rank--;		/* Shorter rank */
		break;
	    case 'l':
		rank++;		/* Longer rank */
		break;
	    case 'j':
		rank = INTMAX_RANK;
		break;
	    case 'z':
		rank = SIZE_T_RANK;
		break;
	    case 't':
		rank = PTRDIFF_T_RANK;
		break;
	    case 'L':
	    case 'q':
		rank += 2;
		break;
	    default:
		/* Output modifiers - terminal sequences */
		state = st_normal;	/* Next state will be normal */
		if (rank < MIN_RANK)	/* Canonicalize rank */
		    rank = MIN_RANK;
		else if (rank > MAX_RANK)
		    rank = MAX_RANK;

		switch (ch) {
		case 'P':	/* Upper case pointer */
		    flags |= FL_UPPER;
		    /* fall through */
		case 'p':	/* Pointer */
		    base = 16;
		    prec = (CHAR_BIT * sizeof(void *) + 3) / 4;
		    flags |= FL_HASH;
		    val = (uintmax_t) (uintptr_t) va_arg(ap, void *);
		    goto is_integer;

		case 'd':	/* Signed decimal output */
		case 'i':
		    base = 10;
		    flags |= FL_SIGNED;
		    switch (rank) {
		    case rank_char:
			/* Yes, all these casts are needed... */
			val =
			    (uintmax_t) (intmax_t) (signed char)va_arg(ap,
								       signed
								       int);
			break;
		    case rank_short:
			val =
			    (uintmax_t) (intmax_t) (signed short)va_arg(ap,
									signed
									int);
			break;
		    case rank_int:
			val = (uintmax_t) (intmax_t) va_arg(ap, signed int);
			break;
		    case rank_long:
			val = (uintmax_t) (intmax_t) va_arg(ap, signed long);
			break;
		    case rank_longlong:
			val =
			    (uintmax_t) (intmax_t) va_arg(ap, signed long long);
			break;
		    }
		    goto is_integer;
		case 'o':	/* Octal */
		    base = 8;
		    goto is_unsigned;
		case 'u':	/* Unsigned decimal */
		    base = 10;
		    goto is_unsigned;
		case 'X':	/* Upper case hexadecimal */
		    flags |= FL_UPPER;
		    /* fall through */
		case 'x':	/* Hexadecimal */
		    base = 16;
		    goto is_unsigned;

is_unsigned:
		    switch (rank) {
		    case rank_char:
			val =
			    (uintmax_t) (unsigned char)va_arg(ap, unsigned int);
			break;
		    case rank_short:
			val =
			    (uintmax_t) (unsigned short)va_arg(ap,
							       unsigned int);
			break;
		    case rank_int:
			val = (uintmax_t) va_arg(ap, unsigned int);
			break;
		    case rank_long:
			val = (uintmax_t) va_arg(ap, unsigned long);
			break;
		    case rank_longlong:
			val = (uintmax_t) va_arg(ap, unsigned long long);
			break;
		    }
		    /* fall through */

is_integer:
		    sz = format_int(q, (o < n) ? n - o : 0, val, flags, base,
				    width, prec);
		    q += sz;
		    o += sz;
		    break;

		case 'c':	/* Character */
		    carg = (char)va_arg(ap, int);
		    sarg = &carg;
		    slen = 1;
		    goto is_string;
		case 's':	/* String */
		    sarg = va_arg(ap, const char *);
		    sarg = sarg ? sarg : "(null)";
		    slen = strlen(sarg);
		    goto is_string;

is_string:
		    {
			char sch;
			int i;

			if (prec != -1 && slen > prec)
			    slen = prec;

			if (width > slen && !(flags & FL_MINUS)) {
			    char pad = (flags & FL_ZERO) ? '0' : ' ';
			    while (width > slen) {
				EMIT(pad);
				width--;
			    }
			}
			for (i = slen; i; i--) {
			    sch = *sarg++;
			    EMIT(sch);
			}
			if (width > slen && (flags & FL_MINUS)) {
			    while (width > slen) {
				EMIT(' ');
				width--;
			    }
			}
		    }
		    break;

		case 'n':	/* Output the number of characters written */
		    {
			switch (rank) {
			case rank_char:
			    *va_arg(ap, signed char *) = o;
			    break;
			case rank_short:
			    *va_arg(ap, signed short *) = o;
			    break;
			case rank_int:
			    *va_arg(ap, signed int *) = o;
			    break;
			case rank_long:
			    *va_arg(ap, signed long *) = o;
			    break;
			case rank_longlong:
			    *va_arg(ap, signed long long *) = o;
			    break;
			}
		    }
		    break;

		default:	/* Anything else, including % */
		    EMIT(ch);
		    break;
		}
	    }
	}
    }

    /* Null-terminate the string */
    if (o < n)
	*q = '\0';		/* No overflow */
    else if (n > 0)
	buffer[n - 1] = '\0';	/* Overflow - terminate at end of buffer */

    return o;
}
