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

fmt_int := fn(v: @Any(), str: ^u8, radix: @TypeOf(v)): uint {
	is_negative := signed_int(@TypeOf(v)) & 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
}

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

fmt_float := fn(v: @Any(), str: ^u8, precision: uint, radix: int): uint {
	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(@TypeOf(v), 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
}

fmt_container := fn(v: @Any(), str: ^u8, opts: FormatOptions): uint {
	T2 := @TypeOf(v)
	kind := Kind.of(T2)
	i := 0
	len := 0
	if kind == .Struct {
		*@as(^[@lenof(@nameof(T2))]u8, @bitcast(str + len)) = *@bitcast(@nameof(T2))
		len += @lenof(@nameof(T2));
		*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(".(\0")
		len += 2
	} else if kind == .Slice {
		*@as(^[@lenof(@nameof(@ChildOf(T2)))]u8, @bitcast(str + len)) = *@bitcast(@nameof(@ChildOf(T2)))
		len += @lenof(@nameof(@ChildOf(T2)));
		*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(".[\0")
		len += 2
	} else if kind == .Tuple {
		*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(".(\0")
		len += 2
	}
	$loop {
		v_sub := v[i]
		len += @inline(format, v_sub, str + len, opts)
		i += 1
		if i == @lenof(T2) break else {
			*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(", \0")
			len += 2
		}
	}
	if kind == .Struct | kind == .Tuple {
		*@as(^[1]u8, @bitcast(str + len)) = *@bitcast(")\0")
		len += 1
	} else if kind == .Slice {
		*@as(^[1]u8, @bitcast(str + len)) = *@bitcast("]\0")
		len += 1
	}
	return len
}

fmt_nullable := fn(v: @Any(), str: ^u8, opts: FormatOptions): uint {
	if v == null {
		*@as(^[4]u8, @bitcast(str)) = *@bitcast("null\0")
		return 4
	} else {
		return @inline(format, @as(@ChildOf(@TypeOf(v)), v), str, opts)
	}
}

fmt_enum := fn(v: @Any(), str: ^u8, opts: FormatOptions): uint {
	T := @TypeOf(v)
	len := @lenof(@nameof(T));
	*@as(^[@lenof(@nameof(T))]u8, @bitcast(str)) = *@bitcast(@nameof(T));
	*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(".(\0")
	len += 2
	len += @inline(fmt_int, @as(usize(T), @bitcast(v)), str + len, 10);
	*@as(^[2]u8, @bitcast(str + len)) = *@bitcast(")\0")
	return len + 2
}

/* TODO:
* Custom formatters using struct methods (T._fmt(self, str): uint),
* 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
*/

FormatOptions := struct {
	precision: uint = 2,
	radix: uint = 10,
	// temporarily here, will change later maybe
	log: LogLevel = .Info,
}

/* SAFETY:
 * Assumes the buffer is wide enough for the formatted text and a null char
 * Does not clear the buffer for you
*/

format := fn(v: @Any(), str: ^u8, opts: FormatOptions): uint {
	T := @TypeOf(v)
	match Kind.of(T) {
		.Pointer => if T != ^u8 return @inline(fmt_int, @as(uint, @bitcast(v)), str, 16) else {
			i := 0
			loop if *(v + i) == '\0' break else {
				*(str + i) = *(v + i)
				i += 1
			}
			return i
		},
		.Builtin => {
			if integer(T) {
				return @inline(fmt_int, v, str, @intcast(opts.radix))
			} else if T == bool {
				return @inline(fmt_bool, v, str)
			} else if float(T) {
				return @inline(fmt_float, v, str, opts.precision, @intcast(opts.radix))
			}
		},
		.Opt => return @inline(fmt_nullable, v, str, opts),
		.Enum => return @inline(fmt_enum, v, str, opts),
		.Struct => return @inline(fmt_container, v, str, opts),
		.Tuple => return @inline(fmt_container, v, str, opts),
		.Slice => return @inline(fmt_container, v, str, opts),
		_ => @error("Type: \"\0", T, "\" is not supported.\0"),
	}
}

format_with_str := fn(v: @Any(), read: ^u8, write: ^u8, opts: FormatOptions): uint {
	T := @TypeOf(v)
	n := string.count(read, '{')
	if n != string.count(read, '}') panic("Missing closing '}' in format string.\0")
	if Kind.of(T) == .Tuple {
		if @lenof(T) != n panic("Format string has different number of '{}' than args given.\0")
		m := 0
		i := 0
		j := 0
		$loop if m > @lenof(T) break else {
			if m == @lenof(T) {
				loop if *(read + i) == '\0' break else {
					*(write + j) = *(read + i)
					i += 1
					j += 1
				}
				m += 1
			} else {
				v2 := v[m]
				loop if *(read + i) == '\0' break else {
					if *(read + i) == '{' & *(read + i + 1) == '}' {
						j += format(v2, write + j, opts) + 1
						i += 2
						break
					} else {
						*(write + j) = *(read + i)
						i += 1
						j += 1
					}
				}
				m += 1
			}
		}
		return j
	} else if n > 1 {
		panic("Format string has multiple '{}' but value provided is not a tuple.\0")
	} else {
		i := 0
		j := 0
		loop if *(read + i) == '\0' break else {
			if *(read + i) == '{' & *(read + i + 1) == '}' {
				j += format(v, write + j, opts) + 1
				i += 2
			} else {
				*(write + j) = *(read + i)
				i += 1
				j += 1
			}
		}
		return j
	}
}