/*
 *  Minimal vsnprintf(), snprintf(), sprintf(), and sscanf() for Duktape.
 *  The supported conversion formats narrowly match what Duktape needs.
 */

#include <stdarg.h>  /* va_list etc */
#include <stddef.h>  /* size_t */
#include <stdint.h>  /* SIZE_MAX */

/* Write character with bound checking.  Offset 'off' is updated regardless
 * of whether an actual write is made.  This is necessary to satisfy snprintf()
 * return value semantics.
 */
#define DUK__WRITE_CHAR(c) do { \
		if (off < size) { \
			str[off] = (char) c; \
		} \
		off++; \
	} while (0)

/* Digits up to radix 16. */
static const char duk__format_digits[16] = {
	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};

/* Format an unsigned long with various options.  An unsigned long is large
 * enough for formatting all supported types.
 */
static size_t duk__format_long(char *str,
                               size_t size,
                               size_t off,
                               int fixed_length,
                               char pad,
                               int radix,
                               int neg_sign,
                               unsigned long v) {
	char buf[24];  /* 2^64 = 18446744073709552000, length 20 */
	char *required;
	char *p;
	int i;

	/* Format in reverse order first.  Ensure at least one digit is output
	 * to handle '0' correctly.  Note that space padding and zero padding
	 * handle negative sign differently:
	 *
	 *     %9d and -321  => '     -321'
	 *     %09d and -321 => '-00000321'
	 */

	for (i = 0; i < (int) sizeof(buf); i++) {
		buf[i] = pad;  /* compiles into memset() equivalent, avoid memset() dependency */
	}

	p = buf;
	do {
		*p++ = duk__format_digits[v % radix];
		v /= radix;
	} while (v != 0);

	required = buf + fixed_length;
	if (p < required && pad == (char) '0') {
		/* Zero padding and we didn't reach maximum length: place
		 * negative sign at the last position.  We can't get here
		 * with fixed_length == 0 so that required[-1] is safe.
		 *
		 * Technically we should only do this for 'neg_sign == 1',
		 * but it's OK to advance the pointer even when that's not
		 * the case.
		 */
		p = required - 1;
	}
	if (neg_sign) {
		*p++ = (char) '-';
	}
	if (p < required) {
		p = required;
	}

	/* Now [buf,p[ contains the result in reverse; copy into place. */

	while (p > buf) {
		p--;
		DUK__WRITE_CHAR(*p);
	}

	return off;
}

/* Parse a pointer.  Must parse whatever is produced by '%p' in sprintf(). */
static int duk__parse_pointer(const char *str, void **out) {
	const unsigned char *p;
	unsigned char ch;
	int count;
	int limit;
	long val;  /* assume void * fits into long */

	/* We only need to parse what our minimal printf() produces, so that
	 * we can check for a '0x' prefix, and assume all hex digits are
	 * lowercase.
	 */

	p = (const unsigned char *) str;
	if (p[0] != (unsigned char) '0' || p[1] != (unsigned char) 'x') {
		return 0;
	}
	p += 2;

	for (val = 0, count = 0, limit = sizeof(void *) * 2; count < limit; count++) {
		ch = *p++;

		val <<= 4;
		if (ch >= (unsigned char) '0' && ch <= (unsigned char) '9') {
			val += ch - (unsigned char) '0';
		} else if (ch >= (unsigned char) 'a' && ch <= (unsigned char) 'f') {
			val += ch - (unsigned char) 'a' + 0x0a;
		} else {
			return 0;
		}
	}

	/* The input may end at a NUL or garbage may follow.  As long as we
	 * parse the '%p' correctly, garbage is allowed to follow, and the
	 * JX pointer parsing also relies on that.
	 */

	*out = (void *) val;
	return 1;
}

/* Minimal vsnprintf() entry point. */
int duk_minimal_vsnprintf(char *str, size_t size, const char *format, va_list ap) {
	size_t off = 0;
	const char *p;
#if 0
	const char *p_tmp;
	const char *p_fmt_start;
#endif
	char c;
	char pad;
	int fixed_length;
	int is_long;

	/* Assume str != NULL unless size == 0.
	 * Assume format != NULL.
	 */

	p = format;
	for (;;) {
		c = *p++;
		if (c == (char) 0) {
			break;
		}
		if (c != (char) '%') {
			DUK__WRITE_CHAR(c);
			continue;
		}

		/* Start format sequence.  Scan flags and format specifier. */

#if 0
		p_fmt_start = p - 1;
#endif
		is_long = 0;
		pad = ' ';
		fixed_length = 0;
		for (;;) {
			c = *p++;
			if (c == (char) 'l') {
				is_long = 1;
			} else if (c == (char) '0') {
				/* Only support pad character '0'. */
				pad = '0';
			} else if (c >= (char) '1' && c <= (char) '9') {
				/* Only support fixed lengths 1-9. */
				fixed_length = (int) (c - (char) '0');
			} else if (c == (char) 'd') {
				long v;
				int neg_sign = 0;
				if (is_long) {
					v = va_arg(ap, long);
				} else {
					v = (long) va_arg(ap, int);
				}
				if (v < 0) {
					neg_sign = 1;
					v = -v;
				}
				off = duk__format_long(str, size, off, fixed_length, pad, 10, neg_sign, (unsigned long) v);
				break;
			} else if (c == (char) 'u') {
				unsigned long v;
				if (is_long) {
					v = va_arg(ap, unsigned long);
				} else {
					v = (unsigned long) va_arg(ap, unsigned int);
				}
				off = duk__format_long(str, size, off, fixed_length, pad, 10, 0, v);
				break;
			} else if (c == (char) 'x') {
				unsigned long v;
				if (is_long) {
					v = va_arg(ap, unsigned long);
				} else {
					v = (unsigned long) va_arg(ap, unsigned int);
				}
				off = duk__format_long(str, size, off, fixed_length, pad, 16, 0, v);
				break;
			} else if (c == (char) 'c') {
				char v;
				v = (char) va_arg(ap, int);  /* intentionally not 'char' */
				DUK__WRITE_CHAR(v);
				break;
			} else if (c == (char) 's') {
				const char *v;
				char c_tmp;
				v = va_arg(ap, const char *);
				if (v) {
					for (;;) {
						c_tmp = *v++;
						if (c_tmp) {
							DUK__WRITE_CHAR(c_tmp);
						} else {
							break;
						}
					}
				}
				break;
			} else if (c == (char) 'p') {
				/* Assume a void * can be represented by 'long'.  This is not
				 * always the case.  NULL pointer is printed out as 0x0000...
				 */
				void *v;
				v = va_arg(ap, void *);
				DUK__WRITE_CHAR('0');
				DUK__WRITE_CHAR('x');
				off = duk__format_long(str, size, off, sizeof(void *) * 2, '0', 16, 0, (unsigned long) v);
				break;
			} else {
				/* Unrecognized, bail out early.  We could also emit the format
				 * specifier verbatim, but it'd be a waste of footprint because
				 * this case should never happen in practice.
				 */
#if 0
				DUK__WRITE_CHAR('!');
#endif
#if 0
				for (p_tmp = p_fmt_start; p_tmp != p; p_tmp++) {
					DUK__WRITE_CHAR(*p_tmp);
				}
				break;
#endif
				goto finish;
			}
		}
	}

 finish:
	if (off < size) {
		str[off] = (char) 0;  /* No increment for 'off', not counted in return value. */
	} else if (size > 0) {
		/* Forced termination. */
		str[size - 1] = 0;
	}

	return (int) off;
}

/* Minimal snprintf() entry point. */
int duk_minimal_snprintf(char *str, size_t size, const char *format, ...) {
	va_list ap;
	int ret;

	va_start(ap, format);
	ret = duk_minimal_vsnprintf(str, size, format, ap);
	va_end(ap);

	return ret;
}

/* Minimal sprintf() entry point. */
int duk_minimal_sprintf(char *str, const char *format, ...) {
	va_list ap;
	int ret;

	va_start(ap, format);
	ret = duk_minimal_vsnprintf(str, SIZE_MAX, format, ap);
	va_end(ap);

	return ret;
}

/* Minimal sscanf() entry point. */
int duk_minimal_sscanf(const char *str, const char *format, ...) {
	va_list ap;
	int ret;
	void **out;

	/* Only the exact "%p" format is supported. */
	if (format[0] != (char) '%' ||
	    format[1] != (char) 'p' ||
	    format[2] != (char) 0) {
	}

	va_start(ap, format);
	out = va_arg(ap, void **);
	ret = duk__parse_pointer(str, out);
	va_end(ap);

	return ret;
}

#undef DUK__WRITE_CHAR