package template

import "core:bytes"
import "core:mem"
import "core:os"
import "core:reflect"
import "core:strings"

@(private)
Field_Kind :: enum {
	Text,
	Value,
	Block_Start,
	Block_End,
}

@(private)
Field :: struct {
	kind:    Field_Kind,
	content: string,
}

Template :: struct {
	_template: []u8,
	fields:    [dynamic]Field,
}

@(private)
tpl_name_from_tag_value :: proc(value: string) -> (tpl_name, extra: string) {
	tpl_name = value
	if comma_index := strings.index_byte(tpl_name, ','); comma_index >= 0 {
		tpl_name = tpl_name[:comma_index]
		extra = value[1 + comma_index:]
	}
	return
}

Write_Error :: enum {
	None = 0,
	Not_A_Struct,
	Not_A_Slice,
	Unsupported_Type,
}

@(private)
next_key :: proc(template: ^[]u8) -> (key: string, offset: [2]int, ok: bool) {
	key_start_idx := bytes.index(template^, {'{', '{'})
	if key_start_idx > -1 {
		key_end_idx := key_start_idx + bytes.index(template[key_start_idx:], {'}', '}'})
		if key_end_idx > -1 {
			key = string(template[key_start_idx + 2:key_end_idx])

			offset = {key_start_idx + 2, key_end_idx}
			ok = true
			template^ = template[key_end_idx + 2:]
			return
		}
	}
	return string(template[:]), {}, false
}

compile :: proc {
	compile_file,
	compile_bytes,
}

compile_file :: proc(tpl: ^Template, filepath: string) -> bool {
	raw_tpl, err := os.read_entire_file_from_path(filepath, context.allocator)
	if err != nil {
		return false
	}
	defer delete(raw_tpl)

	compile_bytes(tpl, raw_tpl)

	return true
}

compile_bytes :: proc(tpl: ^Template, template: []u8) {
	tpl._template = bytes.clone(template)
	tpl.fields = make([dynamic]Field, 0, 10)

	offset := 0
	current := tpl._template
	for key, key_offsets in next_key(¤t) {
		key_start, key_end := offset + key_offsets[0], offset + key_offsets[1]
		if key_offsets[0] - 2 != 0 {
			append(&tpl.fields, Field{kind = .Text, content = string(tpl._template[offset:key_start - 2])})
		}
		if key[0] == '#' {
			append(&tpl.fields, Field{kind = .Block_Start, content = key[1:]})
		} else if key[0] == '/' {
			append(&tpl.fields, Field{kind = .Block_End, content = key[1:]})
		} else {
			append(&tpl.fields, Field{kind = .Value, content = key})
		}

		offset = key_end + 2
	}
	if offset < len(tpl._template) {
		append(&tpl.fields, Field{kind = .Text, content = string(tpl._template[offset:])})
	}
}

destroy :: proc(tpl: ^Template) {
	delete(tpl.fields)
	delete(tpl._template)
}

@(private)
write_value :: proc(b: ^strings.Builder, data: any) -> Write_Error {
	ti := reflect.type_info_base(type_info_of(data.id))

	#partial switch info in ti.variant {
	case reflect.Type_Info_Integer:
		switch d in data {
		case int:
			strings.write_int(b, d)
		case uint:
			strings.write_uint(b, d)
		}

	// TODO: Add num decimals
	case reflect.Type_Info_Float:
		switch f in data {
		case f16:
			strings.write_f16(b, f, 'f')
		case f32:
			strings.write_f32(b, f, 'f')
		case f64:
			strings.write_f64(b, f, 'f')
		}

	case reflect.Type_Info_Rune:
		strings.write_rune(b, data.(rune))

	case reflect.Type_Info_String:
		strings.write_string(b, data.(string))

	case reflect.Type_Info_Pointer:
		raw_ptr, _ := reflect.as_pointer(data)
		if raw_ptr != nil {
			write_value(b, reflect.deref(data))
		}

	case:
		return .Unsupported_Type
	}

	return .None
}

@(private)
parse_block :: proc(block: []Field, b: ^strings.Builder, data: any) {
	for i := 0; i < len(block); i += 1 {
		f := block[i]
		key := f.content

		#partial switch f.kind {
		case .Text:
			strings.write_string(b, f.content)

		case .Value:
			ti := reflect.type_info_base(type_info_of(data.id))
			if info, ok := ti.variant.(reflect.Type_Info_Struct); ok {
				for name, u in info.names[:info.field_count] {
					tpl_name, _ := tpl_name_from_tag_value(
						reflect.struct_tag_get(reflect.Struct_Tag(info.tags[u]), "tpl"),
					)
					if tpl_name == "-" {
						continue
					}

					if tpl_name == key || name == key {
						data := uintptr(data.data) + info.offsets[u]
						write_value(b, any{rawptr(data), info.types[u].id})
						break
					}
				}
			} else {
				write_value(b, data)
			}

		case .Block_Start:
			ti := reflect.type_info_base(type_info_of(data.id))
			if info, ok := ti.variant.(reflect.Type_Info_Struct); ok {
				for name, u in info.names[:info.field_count] {
					tpl_name, _ := tpl_name_from_tag_value(
						reflect.struct_tag_get(reflect.Struct_Tag(info.tags[u]), "tpl"),
					)
					if tpl_name == "-" {
						continue
					}

					if tpl_name == key || name == key {
						data := uintptr(data.data) + info.offsets[u]
						a := any{rawptr(data), info.types[u].id}

						block_end := -1
						for j in i ..< len(block) {
							if block[j].kind == .Block_End && block[j].content == f.content {
								block_end = j
								break
							}
						}
						if block_end == -1 {
							continue
						}

						#partial switch block_info in info.types[u].variant {
						case reflect.Type_Info_Slice:
							slice := cast(^mem.Raw_Slice)data
							for si in 0 ..< slice.len {
								slice_data := uintptr(slice.data) + uintptr(si * block_info.elem_size)
								parse_block(block[i + 1:block_end], b, any{rawptr(slice_data), block_info.elem.id})
							}

						case reflect.Type_Info_Struct:
							parse_block(block[i + 1:block_end], b, a)

						case reflect.Type_Info_Pointer:
							raw_ptr, _ := reflect.as_pointer(a)
							if raw_ptr != nil {
								parse_block(block[i + 1:block_end], b, reflect.deref(a))
							}
						}

						i = block_end
						break
					}
				}
			}
		}
	}
}

render :: proc(tpl: ^Template, b: ^strings.Builder, data: any) {
	parse_block(tpl.fields[:], b, data)
}