/* Written by Tibor 'Igor2' Palinkas in 2024. Placed in the public domain
   or licensed under Creative Commons CC0 (as the user sees fit). */

#ifndef GENRPRINTS_H
#define GENRPRINTS_H

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

#ifndef GENPRINTS_INLINE
#define GENPRINTS_INLINE static
#endif

typedef enum Pformat_flags_e { /* bitfield + padding char in the lowest 8 bits */
	/* alignment (2 bits), only when padded */
	PFF_ALEFT = 0,           /* default alignment */
	PFF_ARIGHT = 256,
	PFF_ACENT = 512,

	PFF_ALWAYS_SIGN = 1024,   /* if not specified: explicit '-', omit '+' */
	PFF_NO_L0 = 2048,         /* omit leading zeros */
	PFF_NO_T0 = 4096          /* omit trailing zeros */
} Pformat_flags_t;

#define PFF_PAD(chr) (chr & 0xFF)
#define PFF_ALIGNMENT (PFF_ALEFT | PFF_ARIGHT | PFF_ACENT)


static const char PMAGIC[] = "PMAGIC";

typedef struct Pformatting_s {
	union {
		long lng;
		double dbl;
		void *ptr;
		char *str;
	} data;
} Pfmt_t;

typedef long Pprinter_t(void *, Pfmt_t *, void put_ch(void *, int), void *);

#define P_GET_PAD_CHR(flg)   (((flg & 0xFF) == 0) ? ' ' : (flg & 0xFF))

#define P_PRINT_STR(str) \
	do { \
		const char *s; \
		for(s = str; *s != '\0'; s++) \
			put_ch(pctx, *s); \
	} while(0)

#define P_PRINT_PAD(len, c) \
	do { \
		long n; \
		for(n = 0; n < len; n++) \
			put_ch(pctx, c); \
	} while(0)

GENPRINTS_INLINE long Palign_str(const char *str, int slen, long width, Pformat_flags_t flg, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	int pad_chr;
	long pl, pl1, pl2;

	if (slen >= width) {
		P_PRINT_STR(str);
		return slen;
	}

	pad_chr = P_GET_PAD_CHR(flg);
	pl = width - slen;

	switch(flg & PFF_ALIGNMENT) {
		case PFF_ARIGHT:
			P_PRINT_PAD(pl, pad_chr);
			P_PRINT_STR(str);
			break;
		case PFF_ALEFT:
			P_PRINT_STR(str);
			P_PRINT_PAD(pl, pad_chr);
			break;
		case PFF_ACENT:
			pl1 = pl2 = pl/2;
			if (pl1 + pl2 < pl)
				pl1++;
			P_PRINT_PAD(pl1, pad_chr);
			P_PRINT_STR(str);
			P_PRINT_PAD(pl2, pad_chr);
			break;
	}
	return width;
}

GENPRINTS_INLINE long Pprint_long(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	char tmp[128];
	int len;

	len = sprintf(tmp, "%ld", *((long *)val));

	if ((format != NULL) && (format[0].data.lng == 2))
		len = Palign_str(tmp, len, format[1].data.lng, format[2].data.lng, put_ch, pctx);
	else
		P_PRINT_STR(tmp);

	return len;
}

GENPRINTS_INLINE long Pprint_int(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	long tmp = *((int *)val);
	return Pprint_long(&tmp, format, put_ch, pctx);
}

GENPRINTS_INLINE long Pprint_ulong(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	char tmp[128];
	int len;

	len = sprintf(tmp, "%lu", *((unsigned long *)val));

	if ((format != NULL) && (format[0].data.lng == 2))
		len = Palign_str(tmp, len, format[1].data.lng, format[2].data.lng, put_ch, pctx);
	else
		P_PRINT_STR(tmp);

	return len;
}

GENPRINTS_INLINE long Pprint_uint(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	unsigned long tmp = *((unsigned int *)val);
	return Pprint_ulong(&tmp, format, put_ch, pctx);
}

GENPRINTS_INLINE long Pprint_double(const void *val_, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	double val = *((double *)val_);
	char fmts[32], tmp[128], *s, *start;
	long len = 0;

	if ((format != NULL) && (format[0].data.lng == 3)) {
		int neg = val < 0.0, wi = format[1].data.lng, wf = format[2].data.lng, id, n;
		double aval = neg ? -val : val;
		long flg = format[3].data.lng;
		int align = flg & PFF_ALIGNMENT, want_width, fleft = 0, fright = 0;
		char pad = P_GET_PAD_CHR(flg);

		if (wi > 60) wi = 60;
		if (wf > 60) wf = 60;
		sprintf(fmts, "%%.%df",  wf);

		start = tmp+64;
		len = sprintf(start, fmts, aval);

		/* remove trailing zeros */
		if (flg & PFF_NO_T0) {
			for(s = start+len-1; *s == '0'; s--) {
				*s = '\0';
				len--;
			}
		}

		/* figure alignment */
		want_width = wi+wf+1;
		s = strchr(start, '.');
		id = (s != NULL) ? s - start : 0; /* integer digits produced by sprintf */

		if (align == PFF_ACENT) {
			int fill = want_width - len;
			fleft = fright = fill/2;
			if (fleft + fright < fill/2)
				fleft++;
		}
		else if ((id < wi) && (align == PFF_ARIGHT))
			fleft = wi-id;
		else if (align == PFF_ALEFT)
			fright = want_width - len;

		/* fill on the left for alignment */
		if (fleft > 0) {
			start -= fleft;
			len += fleft;
			for(s = start, n = 0; n < fleft; n++,s++)
				*s = pad;
		}

		/* sign is before the fill */
		if (neg || (flg & PFF_ALWAYS_SIGN)) {
			start--;
			len++;
			*start = neg ? '-' : '+';
		}

		/* fill on the right for alignment */
		if (fright > 0) {
			for(n = fright, s = start+len; n > 0; n--,s++,len++)
				*s = pad;
			*s = '\0';
		}

		/* the sign may have made the output longer than we wanted, truncate it;
		   do this only if full length is specified */
		if ((wi != 0) && (wf != 0) && (len > want_width)) {
			len = want_width;
			start[len] = '\0';
		}

		P_PRINT_STR(start);
	}
	else {
		len = sprintf(tmp, "%f", val);
		P_PRINT_STR(tmp);
	}

	return len;
}

GENPRINTS_INLINE long Pprint_chr(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	const int chr = *(int *)val;
	char tmp[2];
	long len = 1;

	tmp[0] = chr;
	tmp[1] = '\0';

	if ((format != NULL) && (format[0].data.lng == 2))
		len = Palign_str(tmp, len, format[1].data.lng, format[2].data.lng, put_ch, pctx);
	else
		P_PRINT_STR(tmp);

	return len;

}

GENPRINTS_INLINE long Pprint_str(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	const char *str = *(char **)val;
	long len;
	
	if (str == NULL)
		str = "(null)";
	len = strlen(str);

	if ((format != NULL) && (format[0].data.lng == 2))
		len = Palign_str(str, len, format[1].data.lng, format[2].data.lng, put_ch, pctx);
	else
		P_PRINT_STR(str);

	return len;
}

GENPRINTS_INLINE long Pprint_ptr(const void *val, Pfmt_t *format, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	char tmp[256];
	long len = sprintf(tmp, "%p", *(const void **)val);
	
	if ((format != NULL) && (format[0].data.lng == 2))
		len = Palign_str(tmp, len, format[1].data.lng, format[2].data.lng, put_ch, pctx);
	else
		P_PRINT_STR(tmp);

	return len;
}

GENPRINTS_INLINE long Pprint_err(const char *str, void (*put_ch)(void *pctx, int ch), void *pctx)
{
	P_PRINT_STR(str);
	return strlen(str);
}


#define PTYPE(a,b)        ((int)(a) + ((int)(b)<<8))

/* standard types with no formatting */
#define Pchar(val)        PMAGIC, PTYPE('s', 'i'), Pprint_chr, NULL, (int)(val)
#define Pschar(val)       Pint(val)
#define Puchar(val)       Puint(val)
#define Pshort(val)       Pint(val)
#define Pushort(val)      Puint(val)
#define Pint(val)         PMAGIC, PTYPE('s', 'i'), Pprint_int, NULL, (int)(val)
#define Puint(val)        PMAGIC, PTYPE('u', 'i'), Pprint_uint, NULL, (unsigned int)(val)
#define Plong(val)        PMAGIC, PTYPE('s', 'l'), Pprint_long, NULL, (long)(val)
#define Pulong(val)       PMAGIC, PTYPE('u', 'l'), Pprint_ulong, NULL, (unsigned long)(val)
#define Pdouble(val)      PMAGIC, PTYPE(' ', 'd'), Pprint_double, NULL, (double)(val)
#define Pfloat(val)       Pdouble(val)
#define Pstring(val)      PMAGIC, PTYPE(' ', 's'), Pprint_str, NULL, (const char *)(val)
#define Ppointer(val)         PMAGIC, PTYPE(' ', 'p'), Pprint_ptr, NULL, (const void *)(val)

/* standard types with formatting */
#define PcharF(val, width, flags)         PMAGIC, PTYPE('s', 'i'), Pprint_chr, "ll", (long)(width), (long)(flags), (int)(val)
#define PscharF(val, width, flags)        PintF(val, width, flags)
#define PucharF(val, width, flags)        PuintF(val, width, flags)
#define PshortF(val, width, flags)        PintF(val, width, flags)
#define PushortF(val, width, flags)       PuintF(val, width, flags)
#define PintF(val, width, flags)          PMAGIC, PTYPE('s', 'i'), Pprint_int, "ll", (long)(width), (long)(flags), (int)(val)
#define PuintF(val, width, flags)         PMAGIC, PTYPE('u', 'i'), Pprint_uint, "ll", (long)(width), (long)(flags), (unsigned int)(val)
#define PlongF(val, width, flags)         PMAGIC, PTYPE('s', 'l'), Pprint_long, "ll", (long)(width), (long)(flags), (long)(val)
#define PulongF(val, width, flags)        PMAGIC, PTYPE('u', 'l'), Pprint_ulong, "ll", (long)(width), (long)(flags), (long)(unsigned val)
#define PdoubleF(val, iwid, fwid, flags)  PMAGIC, PTYPE(' ', 'd'), Pprint_double, "lll", (long)(iwid), (long)(fwid), (long)(flags), (double)(val)
#define PfloatF(val, iwid, fwid, flags)   PdoubleF(val, iwid, fwid, flags)
#define PstringF(val, width, flags)       PMAGIC, PTYPE(' ', 's'), Pprint_str, "ll", (long)(width), (long)(flags), (const char *)(val)
#define PpointerF(val, width, flags)      PMAGIC, PTYPE(' ', 'p'), Pprint_ptr, "ll", (long)(width), (long)(flags), (const void *)(val)

/* shorthand names */
#define Pchr(val)                         Pchar(val)
#define Pdbl(val)                         Pdouble(val)
#define Pstr(val)                         Pstring(val)
#define Pptr(val)                         Ppointer(val)
#define PchrF(val)                        PcharF(val, width, flags)
#define PdblF(val, wi, wf, flags)         PdoubleF(val, wi, wf, flags)
#define PstrF(val, width, flags)          PstringF(val, width, flags)
#define PptrF(val, width, flags)          PpointerF(val, width, flags)

#define P_MAX_FMTS 16

GENPRINTS_INLINE long vcprints(void put_ch(void *pctx, int ch), void *pctx, const char *first, va_list ap)
{
	const char *curr;
	long len = 0;
	Pfmt_t format[16];

	for(curr = first; curr != NULL; curr = va_arg(ap, const char *)) {
		if (curr == PMAGIC) {
			int n, typ = va_arg(ap, int);
			Pprinter_t *printer = (Pprinter_t *)va_arg(ap, Pprinter_t *);
			const char *fmt_lst = va_arg(ap, const char *), *fs;

			/* load formatting details from vararg and store them in format[] */
			format[0].data.lng = 0;
			if (fmt_lst != NULL) {
				for(n = 1, fs = fmt_lst; *fs != '\0'; fs++,n++) {
					if (n == P_MAX_FMTS)
						len += Pprint_err("<<too many formatting instructions>>", put_ch, pctx);
					if (n >= P_MAX_FMTS)
						continue;
					switch(*fs) {
						case 'd': format[n].data.dbl = va_arg(ap, double); break;
						case 'l': format[n].data.lng = va_arg(ap, long); break;
						case 'p': format[n].data.ptr = va_arg(ap, void *); break;
						case 's': format[n].data.ptr = va_arg(ap, char *); break;
						default: {
							len += Pprint_err("<<invalid formatting instructions>>", put_ch, pctx);
						}
					}
				format[0].data.lng++;
				}
			}

			/* call the type specific printer */
			switch(typ) {

				case PTYPE('s', 'i'): {
						int i = va_arg(ap, int);
						len += printer(&i, format, put_ch, pctx);
					} break;

				case PTYPE('s', 'l'): {
						long l = va_arg(ap, long);
						len += printer(&l, format, put_ch, pctx);
					} break;

				case PTYPE('u', 'i'): {
						unsigned int u = va_arg(ap, unsigned int);
						len += printer(&u, format, put_ch, pctx);
					} break;

				case PTYPE('u', 'l'): {
						long u = va_arg(ap, unsigned long);
						len += printer(&u, format, put_ch, pctx);
					} break;

				case PTYPE(' ', 'd'): {
						double d = va_arg(ap, double);
						len += printer(&d, format, put_ch, pctx);
					} break;

				case PTYPE(' ', 's'): {
						const char *s = va_arg(ap, const char *);
						len += printer(&s, format, put_ch, pctx);
					} break;


				case PTYPE(' ', 'p'): {
						const void *p = va_arg(ap, const void *);
						len += printer(&p, format, put_ch, pctx);
					} break;

#ifdef GENPRINTS_EXTRA_TYPE_DISPATCH
				/* optional, set by the caller; should use capital letters for type ID */
				GENPRINTS_EXTRA_TYPE_DISPATCH
#endif

				default:
					len += Pprint_err("<<unknown type>>", put_ch, pctx);
			}
		}
		else
			len += Pprint_str(&curr, NULL, put_ch, pctx);
	}

	return len;
}


GENPRINTS_INLINE void Pput_ch_stdout(void *pctx, int ch)
{
	putchar(ch);
}


GENPRINTS_INLINE long prints(const char *first, ...)
{
	long len;

	va_list ap;
	va_start(ap, first);

	len = vcprints(Pput_ch_stdout, NULL, first, ap);

	va_end(ap);
	return len;
}

GENPRINTS_INLINE void Pput_ch_file(void *pctx, int ch)
{
	fputc(ch, (FILE *)pctx);
}

GENPRINTS_INLINE long fprints(FILE *f, const char *first, ...)
{
	long len;

	va_list ap;
	va_start(ap, first);

	len = vcprints(Pput_ch_file, f, first, ap);

	va_end(ap);
	return len;
}


typedef struct {
	char *dst;
	long remain, termi;
} genprints_snprints_t;

GENPRINTS_INLINE void Pput_ch_sn(void *pctx_, int ch)
{
	genprints_snprints_t *pctx = pctx_;
	if (pctx->remain <= 0)
		return;
	pctx->remain--;
	pctx->dst[pctx->termi] = ch;
	pctx->termi++;
}

GENPRINTS_INLINE long vsnprints(char *dst, long size, const char *first, va_list ap)
{
	long len;
	genprints_snprints_t pctx;

	pctx.dst = dst;
	pctx.remain = size - 1;
	pctx.termi = 0;

	len = vcprints(Pput_ch_sn, &pctx, first, ap);

	dst[pctx.termi] = '\0';

	return len;
}


GENPRINTS_INLINE long snprints(char *dst, long size, const char *first, ...)
{
	long len;
	va_list ap;
	va_start(ap, first);

	len = vsnprints(dst, size, first, ap);

	va_end(ap);
	return len;
}


#endif
