// SPDX-License-Identifier: MIT
/*
 * This file is based on klibc.
 */

#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <limits.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 */

				/* Next state will be normal */
				state = st_normal;

				/* Canonicalize rank */
				if (rank < MIN_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;
}
