.{Kind, string, unsigned_int, signed_int, float, integer, memory, panic} := @use("stn")

format_int := fn($T: type, v: T, str: ^u8, radix: T): uint {
	if integer(T) {
		is_negative := signed_int(T) & v < 0
		if is_negative v = -v

		prefix_len := 0
		if radix == 16 {
			*str = '0';
			*(str + 1) = 'x'
			prefix_len = 2
		} else if radix == 2 {
			*str = '0';
			*(str + 1) = 'b'
			prefix_len = 2
		} else if radix == 8 {
			*str = '0';
			*(str + 1) = 'o'
			prefix_len = 2
		}

		if v == 0 {
			*(str + prefix_len) = '0'
			return prefix_len + 1
		}

		i := 0
		loop if v <= 0 break else {
			remainder := v % radix
			v /= radix
			if remainder > 9 {
				*(str + prefix_len + i) = @intcast(remainder - 10 + 'A')
			} else {
				*(str + prefix_len + i) = @intcast(remainder + '0')
			}
			i += 1
		}

		if is_negative {
			*(str + prefix_len + i) = '-'
			i += 1
		}

		@inline(string.reverse, str + prefix_len)
		return prefix_len + i
	} else {
		panic("Type is not an integer\0")
	}
}

format_bool := fn(v: bool, str: ^u8): uint {
	if v {
		*@as(^[u8; 4], @bitcast(str)) = *@bitcast("true\0")
		return 4
	} else {
		*@as(^[u8; 5], @bitcast(str)) = *@bitcast("false\0")
		return 5
	}
}

format_float := fn($T: type, v: T, str: ^u8, precision: uint, radix: int): uint {
	if float(T) {
		is_negative := v < 0
		i := 0

		prefix_len := 0
		if radix == 16 {
			*str = '0';
			*(str + 1) = 'x'
			prefix_len = 2
		} else if radix == 2 {
			*str = '0';
			*(str + 1) = 'b'
			prefix_len = 2
		} else if radix == 8 {
			*str = '0';
			*(str + 1) = 'o'
			prefix_len = 2
		}

		if is_negative v = -v

		integer_part := @fti(v)
		fractional_part := v - @itf(integer_part)

		loop if integer_part == 0 & i > 0 break else {
			remainder := integer_part % radix
			integer_part /= radix
			if remainder > 9 {
				*(str + prefix_len + i) = @intcast(remainder - 10 + 'A')
			} else {
				*(str + prefix_len + i) = @intcast(remainder + '0')
			}
			i += 1
		}

		if is_negative {
			*(str + prefix_len + i) = '-'
			i += 1
		}

		@inline(string.reverse, str + prefix_len);
		*(str + prefix_len + i) = '.'
		i += 1

		p := precision
		tolerance := @as(T, 0.00000001)
		loop if p <= 0 | fractional_part < tolerance break else {
			fractional_part *= @itf(radix)
			digit := @fti(fractional_part)
			if digit > 9 {
				*(str + prefix_len + i) = @intcast(digit - 10 + 'A')
			} else {
				*(str + prefix_len + i) = @intcast(digit + '0')
			}
			i += 1
			fractional_part -= @itf(digit)
			p -= 1
		}

		return prefix_len + i
	} else {
		panic("Type is not a floating point\0")
	}
}

format_inner := fn($T: type, v: T, str: ^u8, opts: FormatOptions): uint {
	match @as(Kind, @bitcast(@kindof(T))) {
		.Pointer => return @inline(format_int, uint, @bitcast(v), str, 16),
		.Builtin => {
			if integer(T) {
				return @inline(format_int, T, v, str, @intcast(opts.radix))
			} else if T == bool {
				return @inline(format_bool, v, str)
			} else if float(T) {
				return @inline(format_float, T, v, str, opts.decimal_digits, @intcast(opts.radix))
			}
		},
		.Struct => {
			// name := ;
			i := 0;
			*@as(^[u8; @len(@nameof(T))], @bitcast(str)) = *@bitcast(@nameof(T))
			len := @len(@nameof(T));
			*@as(^[u8; 2], @bitcast(str + len)) = *@bitcast(".(\0")
			len += 2
			$loop {
				v_sub := v[i]
				TSub := @TypeOf(v_sub)
				len += @inline(format_inner, TSub, v_sub, str + len, opts)
				i += 1
				if i == @len(T) break else {
					*@as(^[u8; 2], @bitcast(str + len)) = *@bitcast(", \0")
					len += 2
				}
			};
			*@as(^[u8; 1], @bitcast(str + len)) = *@bitcast(")\0")
			len += 1
			return len
		},
		_ => panic("unsupported format type\0"),
	}
}

/* TODO:
* Custom formatters using struct methods (T._fmt(self, str): uint),
* Format struct names "Name.(x, y, z)"
* Format struct fields "Name.{a: x, b: y, c: z}"
* Optionally tabulate
* Add more FormatOption fields
* Support scientific notation for floating point
* Support format string (impossible right now)
* Support nullables (impossible right now)
* Support pointers
*/

FormatOptions := struct {
	decimal_digits: uint = 1 << 32,
	radix: uint = 10,
}

/* SAFETY:
 * Assumes the buffer is wide enough for the formatted text and a null char
*/
format := fn($T: type, v: T, str: ^u8): ^u8 return @inline(format_args, T, v, str, .())

format_args := fn($T: type, v: T, str: ^u8, opts: FormatOptions): ^u8 {
	@inline(string.clear, str)
	_ = @inline(format_inner, T, v, str, opts)
	return str
}