#include "say.h"

#ifdef __has_include
#	if __has_include(<unistd.h>)
#		define _say_use_ansi
#		define _say_use_fd
#	endif
#elif defined(__unix__) || defined(__linux__) || defined(__unix)
#	define _say_use_ansi
#	define _say_use_fd
#endif

#ifdef _say_use_fd
#	include <unistd.h>
#endif

static void buffer_pushm
(	buffer*   buf,
	uintmax_t val,
	bool      neg,
	uint8_t   base,
	bool      ucase
) {
	char nb [48] = {};
	size_t digit = 48;
	do {
		uintmax_t v = val%base;
		if (v < 0xa) {
			nb[-- digit] = '0' + v;
		} else {
			if (base <= 36) {
				nb[-- digit] = (ucase?'A':'a') + v - 10;
			} else {
				if (v<36) nb[-- digit] = 'a' + v - 10;
				     else nb[-- digit] = 'A' + v - 36;
			}
		}
		val /= base;
	} while(val > 0);
	if (neg) nb[-- digit] = '-';
	buffer_push(buf, (strp){(sizeof nb) - digit, &nb[digit]});
}

size_t saybuf(buffer* buf, FILE* dest, const char* fmt, say_arg* args) {
	say_arg const* arg = args;
#	ifdef _say_use_fd
		bool rich = dest == nullptr? true : isatty(fileno(dest));
#	elif defined(_say_use_ansi)
		bool const rich = true;
#	endif

#	define pushraw(s) do{if(canPrint)buffer_pushl(buf, s);}while(0)
#	define pushcond(ch, won, woff) \
		case ch: if (on) pushraw(won); else pushraw(woff); break
#	define ansisw(ch, num) pushcond(ch, "\x1b[" #num "m", "\x1b[2" #num "m")
#	define ansiclr(ch, num) \
		case (ch)     : pushraw("\x1b[3" #num "m"); break; \
		case (ch)-0x20: pushraw("\x1b[4" #num "m"); break; \

	uint8_t base = 10;
	enum {normal, only_when_rich, only_when_plain, silent} exclude = normal;
	for (size_t i = 0; fmt[i] != 0; ++i) {
		char m = fmt[i];
		char n = fmt[i+1];
		bool canPrint;
		switch (exclude) {
			case normal: canPrint = true;  break;
			case silent: canPrint = false; break;
			case only_when_rich:
#				ifdef _say_use_ansi
					canPrint = rich;
#				else
					canPrint = false;
#				endif
				break;
			case only_when_plain:
#				ifdef _say_use_ansi
					canPrint = !rich;
#				else
					canPrint = true;
#				endif
				break;
		}
		if (n == 0) goto push; /* prevent bugs on broken seqs */
		if (m == '[') {
			bool on = true;
			size_t param = 0;
			while((m=fmt[++i]) != ']') {
#				ifdef _say_use_ansi
				if (rich) {
					switch(m) {
						case '+': on = true;  break;
						case '-': on = false; break;
						case '=': pushraw("\x1b[m"); break;

						ansisw('e', 1); ansisw('d', 2);
						ansisw('i', 3); ansisw('u', 4);
						ansisw('h', 7);

						ansiclr('k', 0); ansiclr('r', 1);
						ansiclr('g', 2); ansiclr('y', 3);
						ansiclr('b', 4); ansiclr('p', 5);
						ansiclr('c', 6); ansiclr('w', 7);
						ansiclr('x', 9);

						case 'L': pushraw("\x1b[2J");
						default: goto fallback;
					}
					continue;
				}
#				endif

				fallback: switch (m) {
					case '^': exclude = only_when_rich; break;
					case '~': exclude = only_when_plain; break;
					case '|': exclude = normal; break;
					case '#': exclude = silent; break;
					case '?': exclude = *((bool*)*(arg++)) ? normal : silent; break;
					case '@': base = param;      param = 0; break;
					case '$': arg = args[param]; param = 0; break;
					default: goto enter_param;
				} continue;

				enter_param: if (m >= '0' && m <= '9') {
					param *= 10;
					param += m - '0';
				}
			}
		} else if (m == '%') {
			++i;
			switch (n) {
				case '[': m = '[';
				case '%': goto push;
				case 'z': {
					char const* str = (char const*)*(arg++);
					if (canPrint) {
						if (str == nullptr) buffer_pushs(buf, "<null>");
						else buffer_pushs(buf, str);
					}
					break;
				}
				case 's': {
					strp const* a = *(arg++);
					if (a -> ptr == nullptr) buffer_pushs(buf, "<null>");
					else buffer_push(buf, *a);
					break;
				}
				case 'i': case 'I':
				case 'u': case 'U': {
					bool neg=false; uintmax_t mag;
					if (n=='i') {
						intmax_t s = *((intmax_t*)*(arg++));
						if (s < 0) { mag = -s; neg = true; }
						else mag = s;
					} else mag = *((uintmax_t*)*(arg++));
					buffer_pushm(buf, mag, neg, base, n < 'a');
					break;
				}
				case 'f': case 'F': {// fraction
					fraction f = *((fraction*)*(arg++));
					buffer_pushm(buf, fractionNum(f), fractionNeg(f), base, n<'a');
					buffer_pushs(buf, "/");
					buffer_pushm(buf, fractionDenom(f), false, base, n<'a');
				} break;
				case 'x': case 'X': {// fixed
					fixed f = *((fixed*)*(arg++));
					bool neg = fixedVal(f) < 0;
					f = fixedAbs(f);
					buffer_pushm(buf, fixedIntPart(f), neg, base, n<'a');
					uint32_t fracpt = fixedFracPart(f);
					if (fracpt != 0) {
						intmax_t mul = base;
						/* find prec multiplicand */
						while (mul < fixedPrec(f)) {
							if (mul * base < mul) {
								buffer_pushs(buf, "<fixed-point precision error>");
								break;
							}
							mul *= base;
						}
						uintmax_t r = ((intmax_t)fracpt * mul*mul) / ((intmax_t)fixedPrec(f)*mul);
						buffer_pushs(buf, ".");
						buffer_pushm(buf, r, false, base, n<'a');
					}
				} break;
				case 'n': case 'N': {// inexact
				} break;
				case '~': {
					char const* subfmt = (char const*)*(arg++);
					say_arg* subargs = (say_arg*)*(arg++);
					saybuf(buf, dest, subfmt, subargs);
					break;
				}
			}
		} else goto push;
		continue;

		push: if (canPrint) buffer_push(buf, (strp){1, &m});
	}
	return arg - args;
#	undef ansiclr
#	undef ansisw
#	undef pushcond
#	undef pushraw
}

static inline void
sayprep(FILE* dest) {
#	ifndef _say_use_fd
		setvbuf(dest, nullptr, _IONBF, 0); // hax
#	endif
}

void sayto(FILE* dest, char const* fmt, say_arg* args) {
	static _Thread_local buffer buf = {};
	if (buf.run == 0) buffer_init(&buf);
	             else buf.sz = 0;

	saybuf(&buf, dest, fmt, args);

#	ifdef _say_use_fd
		write(fileno(dest), buf.ptr, buf.sz);
#	else
		fprintf(dest, "%.*s", (int)buf.sz, buf.ptr);
#	endif
	buffer_crush(&buf, 2048);
}

void say(char const* fmt, say_arg* args) {
	static bool stprep = false;
	if (!stprep) { stprep=true; sayprep(stderr); };
	sayto(stderr, fmt, args);
}
