/* print.c -- formatted printing routines (Paul Haahr, 12/91) */

#include "rc.h"
#include <setjmp.h>

#define	PRINT_ALLOCSIZE	((size_t)64)
#define	SPRINT_BUFSIZ	((size_t)1024)

#define	MAXCONV 256

/*
 * conversion functions
 *	true return -> flag changes only, not a conversion
 */

#define Flag(name, flag) \
static bool name(Format *format, int c) { \
	format->flags |= flag; \
	return TRUE; \
}

Flag(uconv,	FMT_unsigned)
Flag(hconv,	FMT_short)
Flag(lconv,	FMT_long)

#if HAVE_QUAD_T
Flag(qconv,	FMT_quad)
#endif

Flag(altconv,	FMT_altform)
Flag(leftconv,	FMT_leftside)
Flag(dotconv,	FMT_f2set)

static bool digitconv(Format *format, int c) {
	if (format->flags & FMT_f2set)
		format->f2 = 10 * format->f2 + c - '0';
	else {
		format->flags |= FMT_f1set;
		format->f1 = 10 * format->f1 + c - '0';
	}
	return TRUE;
}

static bool zeroconv(Format *format, int c) {
	if (format->flags & (FMT_f1set | FMT_f2set))
		return digitconv(format, '0');
	format->flags |= FMT_zeropad;
	return TRUE;
}

static void pad(Format *format, size_t len, int c) {
	while (len-- != 0)
		fmtputc(format, c);
}

static bool sconv(Format *format, int c) {
	char *s = va_arg(format->args, char *);
	if ((format->flags & FMT_f1set) == 0)
		fmtcat(format, s);
	else {
		size_t len = strlen(s), width = format->f1 - len;
		if (format->flags & FMT_leftside) {
			fmtappend(format, s, len);
			pad(format, width, ' ');
		} else {
			pad(format, width, ' ');
			fmtappend(format, s, len);
		}
	}
	return FALSE;
}

static char *utoa(unsigned long u, char *t, unsigned int radix, const char *digit) {
	if (u >= radix) {
		t = utoa(u / radix, t, radix, digit);
		u %= radix;
	}
	*t++ = digit[u];
	return t;
}

static void intconv(Format *format, unsigned int radix, int upper, const char *altform) {
	static const char * const table[] = {
		"0123456789abcdefghijklmnopqrstuvwxyz",
		"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ",
	};
	char padchar;
	size_t len, pre, zeroes, padding, width;
	long n, flags;
	unsigned long u;
	char number[64], prefix[20];

	if (radix > 36)
		return;

	flags = format->flags;

#if HAVE_QUAD_T
	if (flags & FMT_quad)
		n = va_arg(format->args, quad_t);
	else
#endif

	if (flags & FMT_long)
		n = va_arg(format->args, long);
	else if (flags & FMT_short)
		n = va_arg(format->args, short);
	else
		n = va_arg(format->args, int);

	pre = 0;
	if ((flags & FMT_unsigned) || n >= 0)
		u = n;
	else {
		prefix[pre++] = '-';
		u = -n;
	}

	if (flags & FMT_altform)
		while (*altform != '\0')
			prefix[pre++] = *altform++;

	len = utoa(u, number, radix, table[upper]) - number;
	if ((flags & FMT_f2set) && (size_t) format->f2 > len)
		zeroes = format->f2 - len;
	else
		zeroes = 0;

	width = pre + zeroes + len;
	if ((flags & FMT_f1set) && (size_t) format->f1 > width) {
		padding = format->f1 - width;
	} else
		padding = 0;

	padchar = ' ';
	if (padding > 0 && flags & FMT_zeropad) {
		padchar = '0';
		if ((flags & FMT_leftside) == 0) {
			zeroes += padding;
			padding = 0;
		}
	}


	if ((flags & FMT_leftside) == 0)
		pad(format, padding, padchar);
	fmtappend(format, prefix, pre);
	pad(format, zeroes, '0');
	fmtappend(format, number, len);
	if (flags & FMT_leftside)
		pad(format, padding, padchar);
}

static bool cconv(Format *format, int c) {
	fmtputc(format, va_arg(format->args, int));
	return FALSE;
}

static bool dconv(Format *format, int c) {
	intconv(format, 10, 0, "");
	return FALSE;
}

static bool oconv(Format *format, int c) {
	intconv(format, 8, 0, "0");
	return FALSE;
}

static bool xconv(Format *format, int c) {
	intconv(format, 16, 0, "0x");
	return FALSE;
}

static bool pctconv(Format *format, int c) {
	fmtputc(format, '%');
	return FALSE;
}

static bool badconv(Format *format, int c) {
	panic("bad conversion character in printfmt");
	/* NOTREACHED */
	return FALSE; /* hush up gcc -Wall */
}


/*
 * conversion table management
 */

static Conv fmttab[MAXCONV];

static void inittab(void) {
	int i;
	for (i = 0; i < MAXCONV; i++)
		fmttab[i] = badconv;

	fmttab['s'] = sconv;
	fmttab['c'] = cconv;
	fmttab['d'] = dconv;
	fmttab['o'] = oconv;
	fmttab['x'] = xconv;
	fmttab['%'] = pctconv;

	fmttab['u'] = uconv;
	fmttab['h'] = hconv;
	fmttab['l'] = lconv;
	fmttab['#'] = altconv;
	fmttab['-'] = leftconv;
	fmttab['.'] = dotconv;

#if HAVE_QUAD_T
	fmttab['q'] = qconv;
#endif

	fmttab['0'] = zeroconv;
	for (i = '1'; i <= '9'; i++)
		fmttab[i] = digitconv;
}

extern bool (*fmtinstall(int c, bool (*f)(Format *, int)))(Format *, int) {
/*Conv fmtinstall(int c, Conv f) {*/
	Conv oldf;
	if (fmttab[0] == NULL)
		inittab();
	c &= MAXCONV - 1;
	oldf = fmttab[c];
	if (f != NULL)
		fmttab[c] = f;
	return oldf;
}


/*
 * functions for inserting strings in the format buffer
 */

extern void fmtappend(Format *format, const char *s, size_t len) {
	while (format->buf + len > format->bufend) {
		size_t split = format->bufend - format->buf;
		memcpy(format->buf, s, split);
		format->buf += split;
		s += split;
		len -= split;
		(*format->grow)(format, len);
	}
	memcpy(format->buf, s, len);
	format->buf += len;
}

extern void fmtcat(Format *format, const char *s) {
	fmtappend(format, s, strlen(s));
}

/*
 * printfmt -- the driver routine
 */

extern int printfmt(Format *format, const char *fmt) {
	unsigned const char *s = (unsigned const char *) fmt;

	if (fmttab[0] == NULL)
		inittab();

	for (;;) {
		int c = *s++;
		switch (c) {
		case '%':
			format->flags = format->f1 = format->f2 = 0;
			do
				c = *s++;
			while ((*fmttab[c])(format, c));
			break;
		case '\0':
			return format->buf - format->bufbegin + format->flushed;
		default:
			fmtputc(format, c);
			break;
		}
	}
}


/*
 * the public entry points
 */

extern int fmtprint(Format *format, const char *fmt,...) {
	int n = -format->flushed;
	va_list ap, saveargs;

	va_start(ap, fmt);
	saveargs = format->args;
	format->args = ap;
	n += printfmt(format, fmt);
	va_end(format->args);
	format->args = saveargs;

	return n + format->flushed;
}

static void fprint_flush(Format *format, size_t more) {
	size_t n = format->buf - format->bufbegin;
	char *buf = format->bufbegin;

	format->flushed += n;
	format->buf = format->bufbegin;
	writeall(format->u.n, buf, n);
}

extern int fprint(int fd, const char *fmt,...) {
	char buf[1024];
	Format format;
	va_list ap;

	format.buf	= buf;
	format.bufbegin	= buf;
	format.bufend	= buf + sizeof buf;
	format.grow	= fprint_flush;
	format.flushed	= 0;
	format.u.n	= fd;

	va_start(ap, fmt);
	format.args = ap;
	printfmt(&format, fmt);
	va_end(format.args);

	fprint_flush(&format, 0);
	return format.flushed;
}

static void memprint_grow(Format *format, size_t more) {
	char *buf;
	size_t len = format->bufend - format->bufbegin + 1;
	len = (len >= more)
		? len * 2
		: ((len + more) + PRINT_ALLOCSIZE) &~ (PRINT_ALLOCSIZE - 1);
	if (format->u.n)
		buf = erealloc(format->bufbegin, len);
	else {
		size_t used = format->buf - format->bufbegin;
		buf = nalloc(len);
		memcpy(buf, format->bufbegin, used);
	}
	format->buf	 = buf + (format->buf - format->bufbegin);
	format->bufbegin = buf;
	format->bufend	 = buf + len - 1;
}

static char *memprint(Format *format, const char *fmt, char *buf, size_t len) {
	format->buf	 = buf;
	format->bufbegin = buf;
	format->bufend	 = buf + len - 1;
	format->grow	 = memprint_grow;
	format->flushed	 = 0;
	printfmt(format, fmt);
	*format->buf = '\0';
	return format->bufbegin;
}

extern char *mprint(const char *fmt,...) {
	Format format;
	char *result;
	va_list ap;

	format.u.n = 1;
	va_start(ap, fmt);
	format.args = ap;
	result = memprint(&format, fmt, ealloc(PRINT_ALLOCSIZE), PRINT_ALLOCSIZE);
	va_end(format.args);
	return result;
}

extern char *nprint(const char *fmt,...) {
	Format format;
	char *result;
	va_list ap;

	format.u.n = 0;
	va_start(ap, fmt);
	format.args = ap;
	result = memprint(&format, fmt, nalloc(PRINT_ALLOCSIZE), PRINT_ALLOCSIZE);
	va_end(format.args);
	return result;
}


/* THESE ARE UNUSED IN rc */

#if 0

extern int print(const char *fmt,...) {
	char buf[1024];
	Format format;

	format.buf	= buf;
	format.bufbegin	= buf;
	format.bufend	= buf + sizeof buf;
	format.grow	= fprint_flush;
	format.flushed	= 0;
	format.u.n	= 1;

	va_start(format.args, fmt);
	printfmt(&format, fmt);
	va_end(format.args);

	fprint_flush(&format, 0);
	return format.flushed;
}

extern int eprint(const char *fmt,...) {
	char buf[1024];
	Format format;

	format.buf	= buf;
	format.bufbegin	= buf;
	format.bufend	= buf + sizeof buf;
	format.grow	= fprint_flush;
	format.flushed	= 0;
	format.u.n	= 2;

	va_start(format.args, fmt);
	printfmt(&format, fmt);
	va_end(format.args);

	fprint_flush(&format, 0);
	return format.flushed;
}

static void snprint_grow(Format *format, size_t more) {
	longjmp(format->u.p, 1);
}

extern int snprint(char *buf, int buflen, const char *fmt,...) {
	int n;
	jmp_buf jbuf;
	Format format;

	if (setjmp(jbuf)) {
		*format.buf = '\0';
		return format.buf - format.bufbegin;
	}

	format.buf	= buf;
	format.bufbegin	= buf;
	format.bufend	= buf + buflen - 1;
	format.grow	= snprint_grow;
	format.flushed	= 0;
	format.u.p	= jbuf;

	va_start(format.args, fmt);
	n = printfmt(&format, fmt);
	va_end(format.args);

	*format.buf = '\0';
	return n;
}

extern int sprint(char *buf, const char *fmt,...) {
	int n;
	jmp_buf jbuf;
	Format format;

	if (setjmp(jbuf)) {
		*format.buf = '\0';
		return format.buf - format.bufbegin;
	}

	format.buf	= buf;
	format.bufbegin	= buf;
	format.bufend	= buf + SPRINT_BUFSIZ - 1;
	format.grow	= snprint_grow;
	format.flushed	= 0;
	format.u.p	= jbuf;

	va_start(format.args, fmt);
	n = printfmt(&format, fmt);
	va_end(format.args);

	*format.buf = '\0';
	return n;
}
#endif
