package draw

import "core:c"
import "core:log"
import "core:strings"
import sdl "vendor:sdl3"
import sdl_ttf "vendor:sdl3/ttf"

Font_Id :: u16

Font_Key :: struct {
	id:   Font_Id,
	size: u16,
}

Cache_Source :: enum u8 {
	Custom,
	Clay,
}

Cache_Key :: struct {
	id:     u32,
	source: Cache_Source,
}

Text_Cache :: struct {
	engine:     ^sdl_ttf.TextEngine,
	font_bytes: [dynamic][]u8,
	sdl_fonts:  map[Font_Key]^sdl_ttf.Font,
	cache:      map[Cache_Key]^sdl_ttf.Text,
}

// Internal for fetching SDL TTF font pointer for rendering
get_font :: proc(id: Font_Id, size: u16) -> ^sdl_ttf.Font {
	assert(int(id) < len(GLOB.text_cache.font_bytes), "Invalid font ID.")
	key := Font_Key{id, size}
	font := GLOB.text_cache.sdl_fonts[key]

	if font == nil {
		log.debug("Font with id:", id, "and size:", size, "not found. Adding..")

		font_bytes := GLOB.text_cache.font_bytes[id]
		if font_bytes == nil {
			log.panicf("Font must first be registered with register_font before using (id=%d)", id)
		}

		font_io := sdl.IOFromConstMem(raw_data(font_bytes[:]), len(font_bytes))
		if font_io == nil {
			log.panicf("Failed to create IOStream for font id=%d: %s", id, sdl.GetError())
		}

		sdl_font := sdl_ttf.OpenFontIO(font_io, true, f32(size))
		if sdl_font == nil {
			log.panicf("Failed to create SDL font for font id=%d size=%d: %s", id, size, sdl.GetError())
		}

		if !sdl_ttf.SetFontSizeDPI(sdl_font, f32(size), 72 * i32(GLOB.dpi_scaling), 72 * i32(GLOB.dpi_scaling)) {
			log.panicf("Failed to set font DPI for font id=%d size=%d: %s", id, size, sdl.GetError())
		}

		GLOB.text_cache.sdl_fonts[key] = sdl_font
		return sdl_font
	} else {
		return font
	}
}

// Returns `false` if there are more than max(u16) fonts
register_font :: proc(bytes: []u8) -> (id: Font_Id, ok: bool) #optional_ok {
	if GLOB.text_cache.engine == nil {
		log.panicf("Cannot register font: text system not initialized. Call init() first.")
	}
	if len(GLOB.text_cache.font_bytes) > int(max(Font_Id)) do return 0, false

	log.debug("Registering font...")
	append(&GLOB.text_cache.font_bytes, bytes)
	return Font_Id(len(GLOB.text_cache.font_bytes) - 1), true
}

Text :: struct {
	sdl_text: ^sdl_ttf.Text,
	position: Vec2,
	color:    Color,
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Text cache lookup -------------
// ---------------------------------------------------------------------------------------------------------------------

// Shared cache lookup/create/update logic used by both the `text` proc and the Clay render path.
// Returns the cached (or newly created) TTF_Text pointer.
@(private)
cache_get_or_update :: proc(key: Cache_Key, c_str: cstring, font: ^sdl_ttf.Font) -> ^sdl_ttf.Text {
	existing, found := GLOB.text_cache.cache[key]
	if !found {
		sdl_text := sdl_ttf.CreateText(GLOB.text_cache.engine, font, c_str, 0)
		if sdl_text == nil {
			log.panicf("Failed to create SDL text: %s", sdl.GetError())
		}
		GLOB.text_cache.cache[key] = sdl_text
		return sdl_text
	} else {
		if !sdl_ttf.SetTextString(existing, c_str, 0) {
			log.panicf("Failed to update SDL text string: %s", sdl.GetError())
		}
		return existing
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Text drawing ------------------
// ---------------------------------------------------------------------------------------------------------------------

// Draw text at a position with optional rotation and origin.
//
// When `id` is nil (the default), the text is created and destroyed each frame — simple and
// leak-free, appropriate for HUDs and moderate UI (up to ~50 text elements per frame).
//
// When `id` is set, the TTF_Text object is cached across frames keyed by the provided u32.
// This avoids per-frame HarfBuzz shaping and allocation, which matters for text-heavy apps
// (editors, terminals, chat). The user is responsible for choosing unique IDs per logical text
// element and calling `clear_text_cache` or `clear_text_cache_entry` when cached entries are
// no longer needed. Custom text IDs occupy a separate namespace from Clay text IDs, so
// collisions between the two are impossible.
//
// `origin` is in pixels from the text block's top-left corner (raylib convention).
// The point whose local coords equal `origin` lands at `pos` in world space.
// `rotation` is in degrees, counter-clockwise.
text :: proc(
	layer: ^Layer,
	text_string: string,
	position: Vec2,
	font_id: Font_Id,
	font_size: u16 = DFT_FONT_SIZE,
	color: Color = DFT_TEXT_COLOR,
	origin: Vec2 = {},
	rotation: f32 = 0,
	id: Maybe(u32) = nil,
	temp_allocator := context.temp_allocator,
) {
	c_str := strings.clone_to_cstring(text_string, temp_allocator)
	defer delete(c_str, temp_allocator)

	sdl_text: ^sdl_ttf.Text
	cached := false

	if cache_id, ok := id.?; ok {
		cached = true
		sdl_text = cache_get_or_update(Cache_Key{cache_id, .Custom}, c_str, get_font(font_id, font_size))
	} else {
		sdl_text = sdl_ttf.CreateText(GLOB.text_cache.engine, get_font(font_id, font_size), c_str, 0)
		if sdl_text == nil {
			log.panicf("Failed to create SDL text: %s", sdl.GetError())
		}
	}

	if needs_transform(origin, rotation) {
		dpi_scale := GLOB.dpi_scaling
		transform := build_pivot_rotation(position * dpi_scale, origin * dpi_scale, rotation)
		prepare_text_transformed(layer, Text{sdl_text, {0, 0}, color}, transform)
	} else {
		prepare_text(layer, Text{sdl_text, position, color})
	}

	if !cached {
		// Don't destroy now — the draw data (atlas texture, vertices) is still referenced
		// by the batch buffers until end() submits to the GPU. Deferred to clear_global().
		append(&GLOB.tmp_uncached_text, sdl_text)
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Public text measurement -------
// ---------------------------------------------------------------------------------------------------------------------

// Measure a string in logical pixels (pre-DPI-scaling) using the same font backend as the renderer.
measure_text :: proc(
	text_string: string,
	font_id: Font_Id,
	font_size: u16 = DFT_FONT_SIZE,
	allocator := context.temp_allocator,
) -> Vec2 {
	c_str := strings.clone_to_cstring(text_string, allocator)
	defer delete(c_str, allocator)
	width, height: c.int
	if !sdl_ttf.GetStringSize(get_font(font_id, font_size), c_str, 0, &width, &height) {
		log.panicf("Failed to measure text: %s", sdl.GetError())
	}
	return {f32(width) / GLOB.dpi_scaling, f32(height) / GLOB.dpi_scaling}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Text anchor helpers -----------
// ---------------------------------------------------------------------------------------------------------------------

center_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return size * 0.5
}

top_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	return {0, 0}
}

top_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {size.x * 0.5, 0}
}

top_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {size.x, 0}
}

left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {0, size.y * 0.5}
}

right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {size.x, size.y * 0.5}
}

bottom_left_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {0, size.y}
}

bottom_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return {size.x * 0.5, size.y}
}

bottom_right_of_text :: proc(text_string: string, font_id: Font_Id, font_size: u16 = DFT_FONT_SIZE) -> Vec2 {
	size := measure_text(text_string, font_id, font_size)
	return size
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Cache management --------------
// ---------------------------------------------------------------------------------------------------------------------

// Destroy all cached text objects (both custom and Clay entries). Call on scene transitions,
// view changes, or periodically in apps that produce many distinct cached text entries over time.
// After calling this, subsequent text draws with an `id` will re-create their cache entries.
clear_text_cache :: proc() {
	for _, sdl_text in GLOB.text_cache.cache {
		append(&GLOB.pending_text_releases, sdl_text)
	}
	clear(&GLOB.text_cache.cache)
}

// Destroy a specific cached custom text entry by its u32 id (the same value passed to the
// `text` proc's `id` parameter). This only affects custom text entries — Clay text entries
// are managed internally and are not addressable by the user.
// No-op if the id is not in the cache.
clear_text_cache_entry :: proc(id: u32) {
	key := Cache_Key{id, .Custom}
	sdl_text, ok := GLOB.text_cache.cache[key]
	if ok {
		append(&GLOB.pending_text_releases, sdl_text)
		delete_key(&GLOB.text_cache.cache, key)
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Internal cache lifecycle ------
// ---------------------------------------------------------------------------------------------------------------------

@(private, require_results)
init_text_cache :: proc(
	device: ^sdl.GPUDevice,
	allocator := context.allocator,
) -> (
	text_cache: Text_Cache,
	ok: bool,
) {
	log.debug("Initializing text state")
	if !sdl_ttf.Init() {
		log.errorf("Failed to initialize SDL_ttf: %s", sdl.GetError())
		return text_cache, false
	}

	engine := sdl_ttf.CreateGPUTextEngine(device)
	if engine == nil {
		log.errorf("Failed to create GPU text engine: %s", sdl.GetError())
		sdl_ttf.Quit()
		return text_cache, false
	}
	sdl_ttf.SetGPUTextEngineWinding(engine, .COUNTER_CLOCKWISE)

	text_cache = Text_Cache {
		engine = engine,
		cache  = make(map[Cache_Key]^sdl_ttf.Text, allocator = allocator),
	}

	log.debug("Done initializing text cache")
	return text_cache, true
}

destroy_text_cache :: proc() {
	for _, font in GLOB.text_cache.sdl_fonts {
		sdl_ttf.CloseFont(font)
	}
	for _, sdl_text in GLOB.text_cache.cache {
		sdl_ttf.DestroyText(sdl_text)
	}
	delete(GLOB.text_cache.sdl_fonts)
	delete(GLOB.text_cache.font_bytes)
	delete(GLOB.text_cache.cache)
	sdl_ttf.DestroyGPUTextEngine(GLOB.text_cache.engine)
	sdl_ttf.Quit()
}
