package draw

import "core:log"
import "core:mem"
import sdl "vendor:sdl3"

Texture_Id :: distinct u32
INVALID_TEXTURE :: Texture_Id(0) // Slot 0 is reserved/unused

Texture_Kind :: enum u8 {
	Static, // Uploaded once, never changes (QR codes, decoded PNGs, icons)
	Dynamic, // Updatable via update_texture_region
	Stream, // Frequent full re-uploads (video, procedural)
}

Sampler_Preset :: enum u8 {
	Linear_Clamp,
	Nearest_Clamp,
	Nearest_Repeat,
	Linear_Repeat,
}

SAMPLER_PRESET_COUNT :: 4

Fit_Mode :: enum u8 {
	Stretch, // Fill rect, may distort aspect ratio (default)
	Fit, // Preserve aspect, letterbox (may leave margins)
	Fill, // Preserve aspect, center-crop (may crop edges)
	Tile, // Repeat at native texture size
	Center, // 1:1 pixel size, centered, no scaling
}

Texture_Desc :: struct {
	width:           u32,
	height:          u32,
	depth_or_layers: u32,
	type:            sdl.GPUTextureType,
	format:          sdl.GPUTextureFormat,
	usage:           sdl.GPUTextureUsageFlags,
	mip_levels:      u32,
	kind:            Texture_Kind,
}

//INTERNAL
Texture_Slot :: struct {
	gpu_texture: ^sdl.GPUTexture,
	desc:        Texture_Desc,
	generation:  u32,
}

// State stored in GLOB
// This file references:
//   GLOB.device                 : ^sdl.GPUDevice
//   GLOB.texture_slots          : [dynamic]Texture_Slot
//   GLOB.texture_free_list      : [dynamic]u32
//   GLOB.pending_texture_releases : [dynamic]Texture_Id
//   GLOB.samplers               : [SAMPLER_PRESET_COUNT]^sdl.GPUSampler

Clay_Image_Data :: struct {
	texture_id: Texture_Id,
	fit:        Fit_Mode,
	tint:       Color,
}

clay_image_data :: proc(id: Texture_Id, fit: Fit_Mode = .Stretch, tint: Color = WHITE) -> Clay_Image_Data {
	return {texture_id = id, fit = fit, tint = tint}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Registration -------------
// ---------------------------------------------------------------------------------------------------------------------

// Register a texture. Draw owns the GPU resource and releases it on unregister.
// `data` is tightly-packed row-major bytes matching desc.format.
// The caller may free `data` immediately after this proc returns.
@(require_results)
register_texture :: proc(desc: Texture_Desc, data: []u8) -> (id: Texture_Id, ok: bool) {
	device := GLOB.device
	if device == nil {
		log.error("register_texture called before draw.init()")
		return INVALID_TEXTURE, false
	}

	assert(desc.width > 0, "Texture_Desc.width must be > 0")
	assert(desc.height > 0, "Texture_Desc.height must be > 0")
	assert(desc.depth_or_layers > 0, "Texture_Desc.depth_or_layers must be > 0")
	assert(desc.mip_levels > 0, "Texture_Desc.mip_levels must be > 0")
	assert(desc.usage != {}, "Texture_Desc.usage must not be empty (e.g. {.SAMPLER})")

	// Create the GPU texture
	gpu_texture := sdl.CreateGPUTexture(
		device,
		sdl.GPUTextureCreateInfo {
			type = desc.type,
			format = desc.format,
			usage = desc.usage,
			width = desc.width,
			height = desc.height,
			layer_count_or_depth = desc.depth_or_layers,
			num_levels = desc.mip_levels,
			sample_count = ._1,
		},
	)
	if gpu_texture == nil {
		log.errorf("Failed to create GPU texture (%dx%d): %s", desc.width, desc.height, sdl.GetError())
		return INVALID_TEXTURE, false
	}

	// Upload pixel data via a transfer buffer
	if len(data) > 0 {
		data_size := u32(len(data))
		transfer := sdl.CreateGPUTransferBuffer(
			device,
			sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size},
		)
		if transfer == nil {
			log.errorf("Failed to create texture transfer buffer: %s", sdl.GetError())
			sdl.ReleaseGPUTexture(device, gpu_texture)
			return INVALID_TEXTURE, false
		}
		defer sdl.ReleaseGPUTransferBuffer(device, transfer)

		mapped := sdl.MapGPUTransferBuffer(device, transfer, false)
		if mapped == nil {
			log.errorf("Failed to map texture transfer buffer: %s", sdl.GetError())
			sdl.ReleaseGPUTexture(device, gpu_texture)
			return INVALID_TEXTURE, false
		}
		mem.copy(mapped, raw_data(data), int(data_size))
		sdl.UnmapGPUTransferBuffer(device, transfer)

		cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
		if cmd_buffer == nil {
			log.errorf("Failed to acquire command buffer for texture upload: %s", sdl.GetError())
			sdl.ReleaseGPUTexture(device, gpu_texture)
			return INVALID_TEXTURE, false
		}
		copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
		sdl.UploadToGPUTexture(
			copy_pass,
			sdl.GPUTextureTransferInfo{transfer_buffer = transfer},
			sdl.GPUTextureRegion{texture = gpu_texture, w = desc.width, h = desc.height, d = desc.depth_or_layers},
			false,
		)
		sdl.EndGPUCopyPass(copy_pass)
		if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
			log.errorf("Failed to submit texture upload: %s", sdl.GetError())
			sdl.ReleaseGPUTexture(device, gpu_texture)
			return INVALID_TEXTURE, false
		}
	}

	// Allocate a slot (reuse from free list or append)
	slot_index: u32
	if len(GLOB.texture_free_list) > 0 {
		slot_index = pop(&GLOB.texture_free_list)
		GLOB.texture_slots[slot_index] = Texture_Slot {
			gpu_texture = gpu_texture,
			desc        = desc,
			generation  = GLOB.texture_slots[slot_index].generation + 1,
		}
	} else {
		slot_index = u32(len(GLOB.texture_slots))
		append(&GLOB.texture_slots, Texture_Slot{gpu_texture = gpu_texture, desc = desc, generation = 1})
	}

	return Texture_Id(slot_index), true
}

// Queue a texture for release at the end of the current frame.
// The GPU resource is not freed immediately — see "Deferred release" in the README.
unregister_texture :: proc(id: Texture_Id) {
	if id == INVALID_TEXTURE do return
	append(&GLOB.pending_texture_releases, id)
}

// Re-upload a sub-region of a Dynamic texture.
update_texture_region :: proc(id: Texture_Id, region: Rectangle, data: []u8) {
	if id == INVALID_TEXTURE do return
	slot := &GLOB.texture_slots[u32(id)]
	if slot.gpu_texture == nil do return

	device := GLOB.device
	data_size := u32(len(data))
	if data_size == 0 do return

	transfer := sdl.CreateGPUTransferBuffer(
		device,
		sdl.GPUTransferBufferCreateInfo{usage = .UPLOAD, size = data_size},
	)
	if transfer == nil {
		log.errorf("Failed to create transfer buffer for texture region update: %s", sdl.GetError())
		return
	}
	defer sdl.ReleaseGPUTransferBuffer(device, transfer)

	mapped := sdl.MapGPUTransferBuffer(device, transfer, false)
	if mapped == nil {
		log.errorf("Failed to map transfer buffer for texture region update: %s", sdl.GetError())
		return
	}
	mem.copy(mapped, raw_data(data), int(data_size))
	sdl.UnmapGPUTransferBuffer(device, transfer)

	cmd_buffer := sdl.AcquireGPUCommandBuffer(device)
	if cmd_buffer == nil {
		log.errorf("Failed to acquire command buffer for texture region update: %s", sdl.GetError())
		return
	}
	copy_pass := sdl.BeginGPUCopyPass(cmd_buffer)
	sdl.UploadToGPUTexture(
		copy_pass,
		sdl.GPUTextureTransferInfo{transfer_buffer = transfer},
		sdl.GPUTextureRegion {
			texture = slot.gpu_texture,
			x = u32(region.x),
			y = u32(region.y),
			w = u32(region.width),
			h = u32(region.height),
			d = 1,
		},
		false,
	)
	sdl.EndGPUCopyPass(copy_pass)
	if !sdl.SubmitGPUCommandBuffer(cmd_buffer) {
		log.errorf("Failed to submit texture region update: %s", sdl.GetError())
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Helpers -------------
// ---------------------------------------------------------------------------------------------------------------------

// Compute UV rect, recommended sampler, and inner rect for a given fit mode.
// `rect` is the target drawing area; `texture_id` identifies the texture whose
// pixel dimensions are looked up via texture_size().
// For Fit mode, `inner_rect` is smaller than `rect` (centered). For all other modes, `inner_rect == rect`.
fit_params :: proc(
	fit: Fit_Mode,
	rect: Rectangle,
	texture_id: Texture_Id,
) -> (
	uv_rect: Rectangle,
	sampler: Sampler_Preset,
	inner_rect: Rectangle,
) {
	size := texture_size(texture_id)
	texture_width := f32(size.x)
	texture_height := f32(size.y)
	rect_width := rect.width
	rect_height := rect.height
	inner_rect = rect

	if texture_width == 0 || texture_height == 0 || rect_width == 0 || rect_height == 0 {
		return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
	}

	texture_aspect := texture_width / texture_height
	rect_aspect := rect_width / rect_height

	switch fit {
	case .Stretch: return {0, 0, 1, 1}, .Linear_Clamp, inner_rect

	case .Fill: if texture_aspect > rect_aspect {
				// Texture wider than rect — crop sides
				scale := rect_aspect / texture_aspect
				margin := (1 - scale) * 0.5
				return {margin, 0, 1 - margin, 1}, .Linear_Clamp, inner_rect
			} else {
				// Texture taller than rect — crop top/bottom
				scale := texture_aspect / rect_aspect
				margin := (1 - scale) * 0.5
				return {0, margin, 1, 1 - margin}, .Linear_Clamp, inner_rect
			}

	case .Fit:
		// Preserve aspect, fit inside rect. Returns a shrunken inner_rect.
		if texture_aspect > rect_aspect {
			// Image wider — letterbox top/bottom
			fit_height := rect_width / texture_aspect
			padding := (rect_height - fit_height) * 0.5
			inner_rect = Rectangle{rect.x, rect.y + padding, rect_width, fit_height}
		} else {
			// Image taller — letterbox left/right
			fit_width := rect_height * texture_aspect
			padding := (rect_width - fit_width) * 0.5
			inner_rect = Rectangle{rect.x + padding, rect.y, fit_width, rect_height}
		}
		return {0, 0, 1, 1}, .Linear_Clamp, inner_rect

	case .Tile:
		uv_width := rect_width / texture_width
		uv_height := rect_height / texture_height
		return {0, 0, uv_width, uv_height}, .Linear_Repeat, inner_rect

	case .Center:
		u_half := rect_width / (2 * texture_width)
		v_half := rect_height / (2 * texture_height)
		return {0.5 - u_half, 0.5 - v_half, 0.5 + u_half, 0.5 + v_half}, .Nearest_Clamp, inner_rect
	}

	return {0, 0, 1, 1}, .Linear_Clamp, inner_rect
}

texture_size :: proc(id: Texture_Id) -> [2]u32 {
	if id == INVALID_TEXTURE do return {0, 0}
	slot := &GLOB.texture_slots[u32(id)]
	return {slot.desc.width, slot.desc.height}
}

texture_format :: proc(id: Texture_Id) -> sdl.GPUTextureFormat {
	if id == INVALID_TEXTURE do return .INVALID
	return GLOB.texture_slots[u32(id)].desc.format
}

texture_kind :: proc(id: Texture_Id) -> Texture_Kind {
	if id == INVALID_TEXTURE do return .Static
	return GLOB.texture_slots[u32(id)].desc.kind
}

// Get the raw GPU texture pointer for binding during draw.
//INTERNAL
texture_gpu_handle :: proc(id: Texture_Id) -> ^sdl.GPUTexture {
	if id == INVALID_TEXTURE do return nil
	idx := u32(id)
	if idx >= u32(len(GLOB.texture_slots)) do return nil
	return GLOB.texture_slots[idx].gpu_texture
}

// Deferred release (called from end / clear_global).
//INTERNAL
process_pending_texture_releases :: proc() {
	device := GLOB.device
	for id in GLOB.pending_texture_releases {
		idx := u32(id)
		if idx >= u32(len(GLOB.texture_slots)) do continue
		slot := &GLOB.texture_slots[idx]
		if slot.gpu_texture != nil {
			sdl.ReleaseGPUTexture(device, slot.gpu_texture)
			slot.gpu_texture = nil
		}
		slot.generation += 1
		append(&GLOB.texture_free_list, idx)
	}
	clear(&GLOB.pending_texture_releases)
}

//INTERNAL
get_sampler :: proc(preset: Sampler_Preset) -> ^sdl.GPUSampler {
	idx := int(preset)
	if GLOB.samplers[idx] != nil do return GLOB.samplers[idx]

	// Lazily create
	min_filter, mag_filter: sdl.GPUFilter
	address_mode: sdl.GPUSamplerAddressMode

	switch preset {
	case .Nearest_Clamp:
		min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .CLAMP_TO_EDGE
	case .Linear_Clamp:
		min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .CLAMP_TO_EDGE
	case .Nearest_Repeat:
		min_filter = .NEAREST; mag_filter = .NEAREST; address_mode = .REPEAT
	case .Linear_Repeat:
		min_filter = .LINEAR; mag_filter = .LINEAR; address_mode = .REPEAT
	}

	sampler := sdl.CreateGPUSampler(
		GLOB.device,
		sdl.GPUSamplerCreateInfo {
			min_filter = min_filter,
			mag_filter = mag_filter,
			mipmap_mode = .LINEAR,
			address_mode_u = address_mode,
			address_mode_v = address_mode,
			address_mode_w = address_mode,
		},
	)
	if sampler == nil {
		log.errorf("Failed to create sampler preset %v: %s", preset, sdl.GetError())
		return GLOB.core_2d.sampler // fallback to existing default sampler
	}

	GLOB.samplers[idx] = sampler
	return sampler
}

// Destroy all sampler pool entries. Called from destroy().
//INTERNAL
destroy_sampler_pool :: proc() {
	device := GLOB.device
	for &s in GLOB.samplers {
		if s != nil {
			sdl.ReleaseGPUSampler(device, s)
			s = nil
		}
	}
}

// Destroy all registered textures. Called from destroy().
//INTERNAL
destroy_all_textures :: proc() {
	device := GLOB.device
	for &slot in GLOB.texture_slots {
		if slot.gpu_texture != nil {
			sdl.ReleaseGPUTexture(device, slot.gpu_texture)
			slot.gpu_texture = nil
		}
	}
	delete(GLOB.texture_slots)
	delete(GLOB.texture_free_list)
	delete(GLOB.pending_texture_releases)
}
