/*
 * 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;
}
