package engine

import "core:fmt"
import "core:mem"
import "core:path/filepath"
import sdl "vendor:sdl3"

Vertex :: struct {
	pos: [2]f32, // clip space, -1..1
	uv:  [2]f32, // 0..1 atlas coords (used later in draw_sprite)
}

SPRITE_VERT_COUNT :: 6
MAX_SPRITES :: 128
SPRITE_VERTS_SIZE :: SPRITE_VERT_COUNT * size_of(Vertex)
VERTEX_BUFFER_SIZE :: MAX_SPRITES * SPRITE_VERTS_SIZE

Queued_Sprite :: struct {
	texture:     ^sdl.GPUTexture,
	verts:       [SPRITE_VERT_COUNT]Vertex,
	batch_group: u32, // 0 = strict order; nonzero = caller permits regrouping
}

Draw_Batch :: struct {
	start:   int,
	count:   int,
	texture: ^sdl.GPUTexture,
}

App :: struct {
	window:            ^sdl.Window,
	device:            ^sdl.GPUDevice,
	shader:            Shader_Runtime,
	pipeline:          ^sdl.GPUGraphicsPipeline,
	sampler:           ^sdl.GPUSampler,
	cmd:               ^sdl.GPUCommandBuffer,
	render_pass:       ^sdl.GPURenderPass,
	swapchain_texture: ^sdl.GPUTexture,
	swapchain_w:       u32,
	swapchain_h:       u32,
	vertex_buffer:     ^sdl.GPUBuffer,
	transfer_buffer:   ^sdl.GPUTransferBuffer,
	draw_list:         [dynamic]Queued_Sprite,
	clear_color:       sdl.FColor,
	camera:            Camera,
}

Shader_Backend :: enum {
	Vulkan_SPIRV,
	DSD12_DXIL,
	Metal_MSL,
}

Shader_Runtime :: struct {
	backend:    Shader_Backend,
	format:     sdl.GPUShaderFormat,
	shader_dir: string,
	entrypoint: cstring,
}

init :: proc(app: ^App, title: cstring, width, height: i32) -> bool {
	if !sdl.Init({.VIDEO}) {
		fmt.eprintfln("SDL_Init failed: %s", sdl.GetError())
		return false
	}

	app.window = sdl.CreateWindow(title, width, height, {})

	if app.window == nil {
		fmt.eprintfln("CreateWindow failed: %s", sdl.GetError())
		return false
	}

	requested: sdl.GPUShaderFormat = {.SPIRV, .DXIL, .MSL}
	app.device = sdl.CreateGPUDevice(requested, false, nil)
	if app.device == nil {
		fmt.eprintfln("CreateGPUDevice failed: %s", sdl.GetError())
		return false
	}

	if !sdl.ClaimWindowForGPUDevice(app.device, app.window) {
		fmt.eprintfln("ClaimWindowForGPUDevice failed: %s", sdl.GetError())
		return false
	}

	if !sdl.SetGPUAllowedFramesInFlight(app.device, 3) {
		fmt.eprintfln("SetGPUAllowedFramesInFlight failed: %s", sdl.GetError())
	}

	// Prefer uncapped present for profiling; fall back if unsupported.
	present := sdl.GPUPresentMode.VSYNC
	if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .IMMEDIATE) {
		present = .IMMEDIATE
	} else if sdl.WindowSupportsGPUPresentMode(app.device, app.window, .MAILBOX) {
		present = .MAILBOX
	}
	if present != .VSYNC {
		if !sdl.SetGPUSwapchainParameters(app.device, app.window, .SDR, present) {
			fmt.eprintfln("SetGPUSwapchainParameters failed: %s", sdl.GetError())
		}
	}

	ok: bool
	app.shader, ok = choose_shader_runtime(app.device)
	if !ok do return false

	app.pipeline = create_sprite_pipeline(app)
	if app.pipeline == nil {
		fmt.eprintfln("create_sprite_pipeline failed: %s", sdl.GetError())
		return false
	}

	app.sampler = sdl.CreateGPUSampler(
		app.device,
		{
			min_filter = .NEAREST,
			mag_filter = .NEAREST,
			mipmap_mode = .NEAREST,
			address_mode_u = .CLAMP_TO_EDGE,
			address_mode_v = .CLAMP_TO_EDGE,
			address_mode_w = .CLAMP_TO_EDGE,
		},
	)

	if app.sampler == nil {
		fmt.eprintfln("CreateGPUSampler failed: %s", sdl.GetError())
		return false
	}

	app.vertex_buffer = sdl.CreateGPUBuffer(
		app.device,
		{usage = {.VERTEX}, size = VERTEX_BUFFER_SIZE},
	)

	if app.vertex_buffer == nil {
		fmt.eprintfln("CreateGPUBuffer failed: %s", sdl.GetError())
		return false
	}

	app.transfer_buffer = sdl.CreateGPUTransferBuffer(
		app.device,
		{usage = .UPLOAD, size = VERTEX_BUFFER_SIZE},
	)

	if app.transfer_buffer == nil {
		fmt.eprintfln("CreateGPUTransferBuffer failed: %s", sdl.GetError())
		return false
	}

	app.draw_list = make([dynamic]Queued_Sprite)

	app.camera = camera_default()

	return true
}

shutdown :: proc(app: ^App) {
	delete(app.draw_list)

	if app.device != nil {
		ok := sdl.WaitForGPUIdle(app.device)
		if !ok {
			fmt.eprintfln("WaitForGPUIdle failed")
		}

		if app.transfer_buffer != nil {
			sdl.ReleaseGPUTransferBuffer(app.device, app.transfer_buffer)
		}

		if app.vertex_buffer != nil {
			sdl.ReleaseGPUBuffer(app.device, app.vertex_buffer)
		}

		if app.sampler != nil {
			sdl.ReleaseGPUSampler(app.device, app.sampler)
		}

		if app.pipeline != nil {
			sdl.ReleaseGPUGraphicsPipeline(app.device, app.pipeline)
		}

		if app.window != nil {
			sdl.ReleaseWindowFromGPUDevice(app.device, app.window)
		}
		sdl.DestroyGPUDevice(app.device)
	}

	if app.window != nil {
		sdl.DestroyWindow(app.window)
	}

	sdl.Quit()
	app^ = {}
}

events :: proc() -> bool {
	event: sdl.Event

	for sdl.PollEvent(&event) {
		if event.type == .QUIT {
			return false
		}
	}

	return true
}

begin_frame :: proc(app: ^App, clear_color: sdl.FColor = {0.12, 0.12, 0.16, 1}) {
	clear(&app.draw_list)
	app.clear_color = clear_color
	app.render_pass = nil
	app.swapchain_texture = nil

	app.cmd = sdl.AcquireGPUCommandBuffer(app.device)
	if app.cmd == nil {
		fmt.eprintfln("AcquireGPUCommandBuffer failed: %s", sdl.GetError())
		return
	}

	ok := sdl.WaitAndAcquireGPUSwapchainTexture(
		app.cmd,
		app.window,
		&app.swapchain_texture,
		&app.swapchain_w,
		&app.swapchain_h,
	)

	if !ok || app.swapchain_texture == nil {
		app.swapchain_texture = nil
		return
	}
}

end_frame :: proc(app: ^App) {
	if app.cmd == nil {
		clear(&app.draw_list)
		return
	}

	cmd := app.cmd
	defer {
		app.render_pass = nil
		app.swapchain_texture = nil
		app.cmd = nil
		clear(&app.draw_list)
	}

	if app.swapchain_texture == nil {
		if !sdl.SubmitGPUCommandBuffer(cmd) {
			fmt.eprintfln("SubmitGPUCommandBuffer failed")
		}
		return
	}

	n := len(app.draw_list)
	batches: [dynamic]Draw_Batch
	defer delete(batches)
	if n > 0 {
		prepare_draw_batches(app.draw_list[:], &batches)

		map_ptr := sdl.MapGPUTransferBuffer(app.device, app.transfer_buffer, false)
		if map_ptr == nil {
			fmt.eprintfln("MapGPUTransferBuffer failed: %s", sdl.GetError())
			if !sdl.SubmitGPUCommandBuffer(cmd) {
				fmt.eprintfln("SubmitGPUCommandBuffer failed")
			}
			return
		}

		for i in 0 ..< n {
			q := &app.draw_list[i]
			offset := i * SPRITE_VERTS_SIZE
			mem.copy(
				rawptr(uintptr(map_ptr) + uintptr(offset)),
				raw_data(q.verts[:]),
				SPRITE_VERTS_SIZE,
			)
		}
		sdl.UnmapGPUTransferBuffer(app.device, app.transfer_buffer)

		copy_pass := sdl.BeginGPUCopyPass(cmd)
		src := sdl.GPUTransferBufferLocation {
			transfer_buffer = app.transfer_buffer,
			offset          = 0,
		}
		dst := sdl.GPUBufferRegion {
			buffer = app.vertex_buffer,
			offset = 0,
			size   = u32(n * SPRITE_VERTS_SIZE),
		}
		sdl.UploadToGPUBuffer(copy_pass, src, dst, false)
		sdl.EndGPUCopyPass(copy_pass)
	}

	color_info := sdl.GPUColorTargetInfo {
		texture     = app.swapchain_texture,
		clear_color = app.clear_color,
		load_op     = .CLEAR,
		store_op    = .STORE,
	}
	app.render_pass = sdl.BeginGPURenderPass(cmd, &color_info, 1, nil)
	sdl.BindGPUGraphicsPipeline(app.render_pass, app.pipeline)

	for batch in batches {
		sampler_binding := sdl.GPUTextureSamplerBinding {
			texture = batch.texture,
			sampler = app.sampler,
		}
		sdl.BindGPUFragmentSamplers(app.render_pass, 0, &sampler_binding, 1)

		vb_binding := sdl.GPUBufferBinding {
			buffer = app.vertex_buffer,
			offset = u32(batch.start * SPRITE_VERTS_SIZE),
		}
		sdl.BindGPUVertexBuffers(app.render_pass, 0, &vb_binding, 1)

		sdl.DrawGPUPrimitives(app.render_pass, u32(batch.count * SPRITE_VERT_COUNT), 1, 0, 0)
	}
	sdl.EndGPURenderPass(app.render_pass)
	app.render_pass = nil

	if !sdl.SubmitGPUCommandBuffer(cmd) {
		fmt.eprintfln("SubmitGPUCommandBuffer failed")
	}
}

load_gpu_shader :: proc(
	device: ^sdl.GPUDevice,
	code: []u8,
	stage: sdl.GPUShaderStage,
	format: sdl.GPUShaderFormat,
	entrypoint: cstring,
	num_samplers: u32,
) -> ^sdl.GPUShader {
	return sdl.CreateGPUShader(
		device,
		{
			code_size = len(code),
			code = raw_data(code),
			entrypoint = entrypoint,
			format = format,
			stage = stage,
			num_samplers = num_samplers,
			num_storage_textures = 0,
			num_storage_buffers = 0,
			num_uniform_buffers = 0,
		},
	)
}

shader_filenames :: proc(backend: Shader_Backend) -> (vert_name, frag_name: string) {
	switch backend {
	case .Vulkan_SPIRV:
		return "sprite.vert.spv", "sprite.frag.spv"
	case .DSD12_DXIL:
		return "sprite.vert.dxil", "sprite.frag.dxil"
	case .Metal_MSL:
		return "sprite.vert.msl", "sprite.frag.msl"
	}
	return "", ""
}

create_sprite_pipeline :: proc(app: ^App) -> ^sdl.GPUGraphicsPipeline {
	vert_name, frag_name := shader_filenames(app.shader.backend)
	vert_path, _ := filepath.join({app.shader.shader_dir, vert_name})
	frag_path, _ := filepath.join({app.shader.shader_dir, frag_name})
	defer {
		delete(vert_path)
		delete(frag_path)
	}

	vert_code, vok := load_shader_blob(vert_path)
	if !vok do return nil
	defer delete(vert_code)

	frag_code, fok := load_shader_blob(frag_path)
	if !fok do return nil
	defer delete(frag_code)

	vert := load_gpu_shader(
		app.device,
		vert_code,
		.VERTEX,
		app.shader.format,
		app.shader.entrypoint,
		0,
	)
	if vert == nil do return nil

	frag := load_gpu_shader(
		app.device,
		frag_code,
		.FRAGMENT,
		app.shader.format,
		app.shader.entrypoint,
		1,
	)
	if frag == nil {
		sdl.ReleaseGPUShader(app.device, vert)
		return nil
	}
	swap_format := sdl.GetGPUSwapchainTextureFormat(app.device, app.window)


	blend := sdl.GPUColorTargetBlendState {
		src_color_blendfactor = .SRC_ALPHA,
		dst_color_blendfactor = .ONE_MINUS_SRC_ALPHA,
		color_blend_op        = .ADD,
		src_alpha_blendfactor = .ONE,
		dst_alpha_blendfactor = .ONE_MINUS_SRC_ALPHA,
		alpha_blend_op        = .ADD,
		enable_blend          = true,
	}

	color_target := sdl.GPUColorTargetDescription {
		format      = swap_format,
		blend_state = blend,
	}

	vb_desc := sdl.GPUVertexBufferDescription {
		slot       = 0,
		pitch      = u32(size_of(Vertex)),
		input_rate = .VERTEX,
	}

	attrs := [2]sdl.GPUVertexAttribute {
		{location = 0, buffer_slot = 0, format = .FLOAT2, offset = 0},
		{location = 1, buffer_slot = 0, format = .FLOAT2, offset = u32(offset_of(Vertex, uv))},
	}

	pipeline_info := sdl.GPUGraphicsPipelineCreateInfo {
		vertex_shader = vert,
		fragment_shader = frag,
		vertex_input_state = {
			vertex_buffer_descriptions = &vb_desc,
			num_vertex_buffers = 1,
			vertex_attributes = raw_data(attrs[:]),
			num_vertex_attributes = 2,
		},
		primitive_type = .TRIANGLELIST,
		rasterizer_state = {fill_mode = .FILL},
		target_info = {color_target_descriptions = &color_target, num_color_targets = 1},
	}

	pipeline := sdl.CreateGPUGraphicsPipeline(app.device, pipeline_info)

	sdl.ReleaseGPUShader(app.device, vert)
	sdl.ReleaseGPUShader(app.device, frag)

	return pipeline
}

texture_run_len :: proc(list: []Queued_Sprite, start: int) -> int {
	if start < 0 || start >= len(list) do return 0

	tex := list[start].texture
	n := 1

	for i in start + 1 ..< len(list) {
		if list[i].texture != tex do break
		n += 1
	}

	return n
}

texture_run_count :: proc(list: []Queued_Sprite) -> int {
	if len(list) == 0 do return 0
	count := 0
	i := 0
	for i < len(list) {
		run := texture_run_len(list, i)
		count += 1
		i += run
	}
	return count
}

prepare_draw_batches :: proc(list: []Queued_Sprite, batches: ^[dynamic]Draw_Batch) {
	clear(batches)
	group_texture_runs(list)
	i := 0
	for i < len(list) {
		run := texture_run_len(list, i)
		append(batches, Draw_Batch{start = i, count = run, texture = list[i].texture})
		i += run
	}
}

// Within each contiguous nonzero batch_group, stably sort by texture so
// consecutive same-texture sprites become one draw. Group 0 and group
// boundaries are never crossed.
group_texture_runs :: proc(list: []Queued_Sprite) {
	start := 0
	for start < len(list) {
		group := list[start].batch_group
		if group == 0 {
			start += 1
			continue
		}

		end := start + 1
		for end < len(list) && list[end].batch_group == group {
			end += 1
		}

		stable_group_by_texture(list[start:end])
		start = end
	}
}

// Stably orders window by texture pointer. Fast path for 1–2 textures
// (common); insertion sort for 3+.
stable_group_by_texture :: proc(window: []Queued_Sprite) {
	n := len(window)
	if n <= 1 do return

	already := true
	for i in 1 ..< n {
		if uintptr(window[i].texture) < uintptr(window[i - 1].texture) {
			already = false
			break
		}
	}
	if already do return

	first := uintptr(window[0].texture)
	second: uintptr
	has_second := false
	third := false
	for i in 1 ..< n {
		t := uintptr(window[i].texture)
		if t == first do continue
		if !has_second {
			second = t
			has_second = true
			continue
		}
		if t != second {
			third = true
			break
		}
	}

	if !has_second do return

	if third {
		for i in 1 ..< n {
			item := window[i]
			j := i
			for j > 0 {
				if uintptr(window[j - 1].texture) <= uintptr(item.texture) {
					break
				}
				window[j] = window[j - 1]
				j -= 1
			}
			window[j] = item
		}
		return
	}

	lo, hi := first, second
	if lo > hi do lo, hi = hi, lo

	tmp: [MAX_SPRITES]Queued_Sprite
	w := 0
	for q in window {
		if uintptr(q.texture) == lo {
			tmp[w] = q
			w += 1
		}
	}
	for q in window {
		if uintptr(q.texture) == hi {
			tmp[w] = q
			w += 1
		}
	}
	copy(window, tmp[:w])
}
