// Clay UI integration for the `draw` package.
//
// All code in this file is dedicated to bridging Clay's render command stream into `draw`'s
// primitive/sub-batch pipeline. Nothing outside this file should reference the `clay` package
// directly; everything Clay-related (types, lifecycle helpers, render-command dispatch, the
// border-merge stack, the Clay backdrop bracket walker, the text measure/error callbacks,
// and the `Clay_Image_Data` user-facing helper) lives here. `draw.odin`'s lifecycle procs
// call `init_clay`, `destroy_clay`, and `clear_clay_per_frame` to drive the bits of state
// that necessarily live on the shared `Global` struct.
package draw

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

import clay "../vendor/clay"

// ---------------------------------------------------------------------------------------------------------------------
// ----- Lifecycle ------------
// ---------------------------------------------------------------------------------------------------------------------

// Allocate the Clay arena, build the merge-candidate stack, hand the arena to Clay, and
// register the text-measurement and error callbacks. Called by `init` once `GLOB` has been
// populated with the device/window state Clay's callbacks read from.
//INTERNAL
init_clay :: proc(window: ^sdl.Window, allocator: runtime.Allocator) {
	min_memory_size: c.size_t = cast(c.size_t)clay.MinMemorySize()
	GLOB.clay_merge_open_stack = make([dynamic]Clay_Merge_Candidate, 0, 16, allocator = allocator)
	GLOB.clay_memory = make([^]u8, min_memory_size, allocator = allocator)
	arena := clay.CreateArenaWithCapacityAndMemory(min_memory_size, GLOB.clay_memory)
	window_width, window_height: c.int
	sdl.GetWindowSize(window, &window_width, &window_height)
	clay.Initialize(arena, {f32(window_width), f32(window_height)}, {handler = clay_error_handler})
	clay.SetMeasureTextFunction(measure_text_clay, nil)
}

// Free the Clay arena memory allocated in `init_clay`. Called by `destroy`. The merge stack
// is left to the package allocator's normal teardown to preserve historical behavior.
//INTERNAL
destroy_clay :: proc(allocator: runtime.Allocator) {
	free(GLOB.clay_memory, allocator)
}

// Reset Clay per-frame state: the z-index high-water mark and the border-merge stack.
// Called by `clear_global` at the start of every frame.
//INTERNAL
clear_clay_per_frame :: proc() {
	GLOB.clay_z_index = 0
	clear(&GLOB.clay_merge_open_stack)
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Image data (Clay RenderCommandType.Image payload) ------------
// ---------------------------------------------------------------------------------------------------------------------

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}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Callbacks (clay -> draw) ------------
// ---------------------------------------------------------------------------------------------------------------------

@(private = "file")
clay_error_handler :: proc "c" (errorData: clay.ErrorData) {
	context = GLOB.odin_context
	log.error("Clay error:", errorData.errorType, errorData.errorText)
}

@(private = "file")
measure_text_clay :: proc "c" (
	text: clay.StringSlice,
	config: ^clay.TextElementConfig,
	user_data: rawptr,
) -> clay.Dimensions {
	context = GLOB.odin_context
	text := string(text.chars[:text.length])
	c_text := strings.clone_to_cstring(text, context.temp_allocator)
	defer delete(c_text, context.temp_allocator)
	width, height: c.int
	if !sdl_ttf.GetStringSize(get_font(config.fontId, config.fontSize), c_text, 0, &width, &height) {
		log.panicf("Failed to measure text: %s", sdl.GetError())
	}

	return clay.Dimensions{width = f32(width) / GLOB.dpi_scaling, height = f32(height) / GLOB.dpi_scaling}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Custom draw + customData envelope ------------
// ---------------------------------------------------------------------------------------------------------------------

// Called for each Clay `RenderCommandType.Custom` render command that
// `prepare_clay_batch` encounters and which is NOT a levlib-managed variant
// (e.g. `Backdrop_Marker`).
//
// - `layer` is the layer the command belongs to (post-z-index promotion).
// - `bounds` is already translated into the active layer's coordinate system
//   and pre-DPI, matching what the built-in shape procs expect.
// - `render_data` is Clay's `CustomRenderData` for the element, exposing
//   `backgroundColor` and `cornerRadius`. Its `customData` field has been
//   unwrapped from the `Clay_Custom` envelope: it points at the user's own
//   data (the value the user wrote into the `rawptr` variant), not at the
//   `Clay_Custom` itself. If the union was zero-init (no variant set) or
//   `customData` was originally nil, the callback receives nil.
//
// The callback must not call `new_layer` or `prepare_clay_batch`.
Custom_Draw :: #type proc(layer: ^Layer, bounds: Rectangle, render_data: clay.CustomRenderData)

ClayBatch :: struct {
	bounds: Rectangle,
	cmds:   clay.ClayArray(clay.RenderCommand),
}

// Discriminated sum of everything `clay.CustomElementConfig.customData` is allowed to point
// at. levlib-defined variants (currently just `Backdrop_Marker`) are recognized by
// `prepare_clay_batch` and routed to the appropriate internal path; the `rawptr` variant is
// the escape hatch for user-defined custom drawing — `prepare_clay_batch` unwraps it before
// invoking `custom_draw` so the callback sees the user's pointer in `render_data.customData`
// exactly as if no wrapper were involved.
//
// Contract: `customData`, when non-nil, MUST point at storage holding a `Clay_Custom`
// value. The user owns that storage; its lifetime must span the Clay layout call and the
// matching `prepare_clay_batch` call. Pointing `customData` at a bare user struct violates
// the contract — the dispatcher will read its first bytes as a union tag and either route
// the draw incorrectly or panic on type assertion. There is no recovery path; this is a
// strict-discipline API by design.
//
// Construction notes (Odin implicit-conversion rules):
//   - Backdrop variant: `bd: Clay_Custom = Backdrop_Marker{...}` works directly.
//     Variant-to-union conversion is implicit.
//   - User pointer: `up: Clay_Custom = rawptr(&my_struct)` — the explicit `rawptr(...)` is
//     required because Odin does not chain `^T -> rawptr -> Clay_Custom` implicitly. A bare
//     `up: Clay_Custom = &my_struct` is a compile error.
Clay_Custom :: union {
	Backdrop_Marker,
	rawptr,
}

// Per-primitive parameters for a backdrop blur dispatched through the Clay integration.
// Embedded as a `Clay_Custom` variant; `prepare_clay_batch` walks the command stream,
// opens/closes a backdrop scope around contiguous backdrop runs, and feeds these to
// `backdrop_blur` via `dispatch_clay_backdrop`. The discriminant is the union tag — no
// in-band magic field needed (compiler-enforced).
Backdrop_Marker :: struct {
	sigma:       f32,
	tint:        Color,
	radii:       Rectangle_Radii,
	feather_ppx: f32,
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Border-merge stack ------------
// ---------------------------------------------------------------------------------------------------------------------

// One entry on the Clay merge stack. Pushed by `dispatch_clay_command` when emitting a
// Rectangle or an Image primitive, then popped by a matching Border to retroactively add
// the outline. See `try_dispatch_clay_border_merge` for the matching semantics.
//INTERNAL
Clay_Merge_Candidate :: struct {
	primitive_index: u32, // Index into `GLOB.tmp_primitives` of the candidate primitive.
	outer_bounds:    Rectangle, // Clay's bounding box — keyed on for the bounds match check.
	corner_radii:    clay.CornerRadius, // Clay's corner radii — also keyed on for the match check.
	image_data:      Clay_Image_Data, // Only read when kind == .Fill_Texture (needed to refit UVs to inner_bounds).
	kind:            Clay_Merge_Candidate_Kind,
}

//INTERNAL
Clay_Merge_Candidate_Kind :: enum u8 {
	// Solid Color brush. Used for Rectangle commands and for the bg primitive of an Image
	// command that has `backgroundColor.a > 0`. Merge mutation: shrink shape + add outline.
	Fill_Color,
	// Texture_Fill brush. Used for the image primitive of an Image command with no bg, where
	// `fit_params` returned `fit_rect == outer_bounds` (the image fully covers Clay's bounds).
	// Merge mutation: shrink shape + add outline + refit UV against inner_bounds.
	Fill_Texture,
}

// Returns true if this Clay render command represents a backdrop primitive — i.e. its
// `customData` points at a `Clay_Custom` whose active variant is `Backdrop_Marker`.
is_clay_backdrop :: proc(cmd: ^clay.RenderCommand) -> bool {
	if cmd.commandType != .Custom do return false
	p := cmd.renderData.custom.customData
	if p == nil do return false
	_, ok := (^Clay_Custom)(p).(Backdrop_Marker)
	return ok
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Border emission ------------
// ---------------------------------------------------------------------------------------------------------------------

// Emit a Clay border drawn INSIDE `bounds` — the outer edge of each side aligns with
// `bounds`, the inner edge is `border_width.*` pixels inset. Matches Clay's layout model
// (CSS border-box) so the visible element occupies exactly Clay's allocated space.
//
// The fast path (uniform widths) uses `rectangle()` with the built-in SDF outline, which
// always extends outward from the shape it's given — we pre-shrink the shape by
// `border_width` so the outline lands precisely at Clay's bounds. The slow path (non-uniform
// widths) emits per-side rectangles and per-corner arcs directly, all positioned inside
// `bounds`. All-zero widths is a no-op.
//
// A corner is rounded iff its radius is positive AND both adjacent sides have positive
// width. Top corners take their thickness from `border_width.top`, bottom corners from
// `border_width.bottom`. When the two widths meeting at a corner differ there is a step at
// the side/corner junction (acceptable for the rare mixed-width case).
//
// When `border_width > corner_radius`, the inner corner clamps to zero (sharp inside, still
// rounded outside) — matches CSS-standard behavior.
//INTERNAL
clay_emit_partial_border :: proc(
	layer: ^Layer,
	bounds: Rectangle,
	border_color: Color,
	border_width: clay.BorderWidth,
	corner_radii: clay.CornerRadius,
) {
	// All-zero: nothing to draw.
	if border_width.top == 0 && border_width.right == 0 && border_width.bottom == 0 && border_width.left == 0 {
		return
	}

	// Convert side widths once (u16 -> f32) and cache for reuse.
	width_top := f32(border_width.top)
	width_right := f32(border_width.right)
	width_bottom := f32(border_width.bottom)
	width_left := f32(border_width.left)

	// Fast path: all four sides have the same nonzero width. Pre-shrink the shape by the
	// uniform width so the SDF outline (which always extends outward from the shape) lands
	// exactly at Clay's `bounds` — the visible border ends up INSIDE Clay's allocation while
	// the SDF mechanism keeps doing outward outlining. Single SDF primitive, exact curves,
	// analytical AA.
	if border_width.left == border_width.top &&
	   border_width.top == border_width.right &&
	   border_width.right == border_width.bottom {
		uniform_width := width_top
		inner_bounds := Rectangle {
			x      = bounds.x + uniform_width,
			y      = bounds.y + uniform_width,
			width  = bounds.width - 2 * uniform_width,
			height = bounds.height - 2 * uniform_width,
		}
		inner_radii := Rectangle_Radii {
			top_left     = max(0, corner_radii.topLeft - uniform_width),
			top_right    = max(0, corner_radii.topRight - uniform_width),
			bottom_right = max(0, corner_radii.bottomRight - uniform_width),
			bottom_left  = max(0, corner_radii.bottomLeft - uniform_width),
		}
		rectangle(
			layer,
			inner_bounds,
			BLANK,
			outline_color = border_color,
			outline_width = uniform_width,
			radii = inner_radii,
		)
		return
	}

	// A corner is drawn rounded only if its radius is positive AND both adjacent sides are present.
	top_left_rounded := corner_radii.topLeft > 0 && border_width.top > 0 && border_width.left > 0
	top_right_rounded := corner_radii.topRight > 0 && border_width.top > 0 && border_width.right > 0
	bottom_left_rounded := corner_radii.bottomLeft > 0 && border_width.bottom > 0 && border_width.left > 0
	bottom_right_rounded := corner_radii.bottomRight > 0 && border_width.bottom > 0 && border_width.right > 0

	// Horizontal x-coordinates where the top/bottom side rectangles start/end. When the
	// adjacent corner is rounded, the side stops at `bounds.x + radius` (where the corner
	// arc takes over). When not rounded, the side runs to the bounds edge; the perpendicular
	// side handles the inset to avoid overlap.
	top_left_x: f32 = top_left_rounded ? bounds.x + corner_radii.topLeft : bounds.x
	top_right_x: f32 =
		top_right_rounded ? bounds.x + bounds.width - corner_radii.topRight : bounds.x + bounds.width
	bottom_left_x: f32 = bottom_left_rounded ? bounds.x + corner_radii.bottomLeft : bounds.x
	bottom_right_x: f32 =
		bottom_right_rounded ? bounds.x + bounds.width - corner_radii.bottomRight : bounds.x + bounds.width

	// Vertical y-coordinates where the left/right side rectangles start/end. When the
	// adjacent corner is rounded, inset by the corner radius. When not rounded, inset by the
	// adjacent horizontal width — the horizontal side owns the corner area (extending through
	// it to the bounds edge), so the vertical side starts below it to avoid overdraw of
	// translucent colors.
	top_left_y: f32 = top_left_rounded ? bounds.y + corner_radii.topLeft : bounds.y + width_top
	top_right_y: f32 = top_right_rounded ? bounds.y + corner_radii.topRight : bounds.y + width_top
	bottom_left_y: f32 =
		bottom_left_rounded ? bounds.y + bounds.height - corner_radii.bottomLeft : bounds.y + bounds.height - width_bottom
	bottom_right_y: f32 =
		bottom_right_rounded ? bounds.y + bounds.height - corner_radii.bottomRight : bounds.y + bounds.height - width_bottom

	// Side rectangles drawn INSIDE `bounds`. Sharp corners, solid fill, no outline. Each
	// gated on its own width — skipping zero-width sides saves the primitive upload.
	if border_width.top > 0 {
		top_side := Rectangle {
			x      = top_left_x,
			y      = bounds.y,
			width  = top_right_x - top_left_x,
			height = width_top,
		}
		rectangle(layer, top_side, border_color)
	}
	if border_width.bottom > 0 {
		bottom_side := Rectangle {
			x      = bottom_left_x,
			y      = bounds.y + bounds.height - width_bottom,
			width  = bottom_right_x - bottom_left_x,
			height = width_bottom,
		}
		rectangle(layer, bottom_side, border_color)
	}
	if border_width.left > 0 {
		left_side := Rectangle {
			x      = bounds.x,
			y      = top_left_y,
			width  = width_left,
			height = bottom_left_y - top_left_y,
		}
		rectangle(layer, left_side, border_color)
	}
	if border_width.right > 0 {
		right_side := Rectangle {
			x      = bounds.x + bounds.width - width_right,
			y      = top_right_y,
			width  = width_right,
			height = bottom_right_y - top_right_y,
		}
		rectangle(layer, right_side, border_color)
	}

	// Corner arcs (90° quadrants) drawn INSIDE bounds: outer radius matches Clay's
	// `corner_radii`, inner radius is the outer radius minus the relevant border thickness
	// (clamped to 0 for thick borders — produces a filled pie slice when border > radius,
	// matching CSS). Angle convention matches ring(): 0° = +x (right), 90° = +y (down),
	// 180° = -x (left), 270° = -y (up).
	if top_left_rounded {
		radius := corner_radii.topLeft
		inner_radius := max(0, radius - width_top)
		center := Vec2{bounds.x + radius, bounds.y + radius}
		ring(layer, center, inner_radius, radius, border_color, start_angle = 180, end_angle = 270)
	}
	if top_right_rounded {
		radius := corner_radii.topRight
		inner_radius := max(0, radius - width_top)
		center := Vec2{bounds.x + bounds.width - radius, bounds.y + radius}
		ring(layer, center, inner_radius, radius, border_color, start_angle = 270, end_angle = 360)
	}
	if bottom_right_rounded {
		radius := corner_radii.bottomRight
		inner_radius := max(0, radius - width_bottom)
		center := Vec2{bounds.x + bounds.width - radius, bounds.y + bounds.height - radius}
		ring(layer, center, inner_radius, radius, border_color, start_angle = 0, end_angle = 90)
	}
	if bottom_left_rounded {
		radius := corner_radii.bottomLeft
		inner_radius := max(0, radius - width_bottom)
		center := Vec2{bounds.x + radius, bounds.y + bounds.height - radius}
		ring(layer, center, inner_radius, radius, border_color, start_angle = 90, end_angle = 180)
	}
}

// Try to retroactively merge this Border into a pending Rectangle/Image candidate on the
// merge stack. Returns true on success so the caller can skip the standalone Border emission.
//
// Clay emits a parent element's bg and border bracketing all the children's commands, so a
// simple "is the next command a Border?" check (the previous approach) only catches leaf
// elements. The stack approach lets us pair them across arbitrary nesting: every Rectangle/
// Image push registers itself; every Border pops down until it finds a geometric match.
//
// Pop semantics: non-matching candidates above the match are discarded — their elements had
// no border anyway, so their primitives stay in `tmp_primitives` as plain Rectangles. A
// Border that finds no match at all falls back to standalone `clay_emit_partial_border`.
//
// Predicates that decline a candidate:
//   - non-uniform or zero border widths (can't be a single uniform outline)
//   - translucent border (the unmerged path's bg-under-border blending differs)
//   - mismatched bounds or cornerRadius (the candidate isn't from the same element)
//
// False-match risk: two unrelated elements with bit-identical bounds and corner radii.
// Requires geometric coincidence (rare in practice), and even when it fires, the misattributed
// outline still lands at the correct screen position with the correct color — the pixels
// match the unmerged ground truth for opaque borders (the only kind we merge).
//INTERNAL
try_dispatch_clay_border_merge :: proc(bounds: Rectangle, border_data: clay.BorderRenderData) -> bool {
	border_width := border_data.width
	uniform_nonzero :=
		border_width.left == border_width.top &&
		border_width.top == border_width.right &&
		border_width.right == border_width.bottom &&
		border_width.top > 0
	if !uniform_nonzero do return false
	if border_data.color[3] < 255 do return false

	for len(GLOB.clay_merge_open_stack) > 0 {
		candidate := pop(&GLOB.clay_merge_open_stack)
		if candidate.outer_bounds != bounds do continue
		if candidate.corner_radii != border_data.cornerRadius do continue
		apply_clay_border_merge_to_primitive(candidate, border_data)
		return true
	}
	return false
}

// Mutates `tmp_primitives[candidate.primitive_index]` in place: shrinks the SDF shape by
// the uniform border width so the (outward) outline lands at the outer bounds, sets the
// outline flag and params, and — for `Fill_Texture` candidates — refits the texture's UV
// against `inner_bounds` so the image doesn't overflow into the border strip.
//
// The primitive's `bounds` field stays at the outer bounds: the rasterized quad already
// covers the area the outline now occupies. Skipping the bounds expansion that
// `apply_brush_and_outline` would normally do is intentional — expanding here would push the
// rasterized quad past Clay's outer edge.
//INTERNAL
apply_clay_border_merge_to_primitive :: proc(
	candidate: Clay_Merge_Candidate,
	border_data: clay.BorderRenderData,
) {
	prim := &GLOB.tmp_primitives[candidate.primitive_index]
	uniform_width := f32(border_data.width.top)
	dpi_scale := GLOB.dpi_scaling

	inner_half_width := candidate.outer_bounds.width * 0.5 - uniform_width
	inner_half_height := candidate.outer_bounds.height * 0.5 - uniform_width
	prim.params.rrect.half_size_ppx = {inner_half_width * dpi_scale, inner_half_height * dpi_scale}
	prim.params.rrect.radii_ppx = {
		max(0, candidate.corner_radii.topLeft - uniform_width) * dpi_scale,
		max(0, candidate.corner_radii.topRight - uniform_width) * dpi_scale,
		max(0, candidate.corner_radii.bottomRight - uniform_width) * dpi_scale,
		max(0, candidate.corner_radii.bottomLeft - uniform_width) * dpi_scale,
	}

	// Set the outline bit in the packed flags field (low byte = Shape_Kind, bits 8+ = Shape_Flags).
	prim.flags |= u32(transmute(u8)Shape_Flags{.Outline}) << 8
	prim.effects.outline_color = Color(border_data.color)
	prim.effects.outline_packed = pack_f16_pair(f16(uniform_width * dpi_scale), 0)

	if candidate.kind == .Fill_Texture {
		// The candidate was only pushed if its `fit_rect == outer_bounds` at emission time, so the
		// image fills the rasterized quad. Refit UVs against `inner_bounds` so the image is scoped
		// to the area inside the new outline rather than overflowing into the border strip.
		inner_bounds := Rectangle {
			x      = candidate.outer_bounds.x + uniform_width,
			y      = candidate.outer_bounds.y + uniform_width,
			width  = candidate.outer_bounds.width - 2 * uniform_width,
			height = candidate.outer_bounds.height - 2 * uniform_width,
		}
		uv_rect, _, _ := fit_params(candidate.image_data.fit, inner_bounds, candidate.image_data.texture_id)
		prim.uv_rect = {uv_rect.x, uv_rect.y, uv_rect.width, uv_rect.height}
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Command dispatch ------------
// ---------------------------------------------------------------------------------------------------------------------

// Dispatch a single non-backdrop Clay render command to the appropriate `draw` primitive.
// Extracted from the main `prepare_clay_batch` walk so that the deferred-buffer flush path
// can replay commands accumulated during an open backdrop scope without duplicating the
// per-command lowering code.
//INTERNAL
dispatch_clay_command :: proc(
	layer: ^Layer,
	render_command: ^clay.RenderCommand,
	custom_draw: Custom_Draw,
	temp_allocator: runtime.Allocator,
) {
	// Translate bounding box of the primitive by the layer position
	bounds := Rectangle {
		x      = render_command.boundingBox.x + layer.bounds.x,
		y      = render_command.boundingBox.y + layer.bounds.y,
		width  = render_command.boundingBox.width,
		height = render_command.boundingBox.height,
	}

	switch render_command.commandType {
	case clay.RenderCommandType.None:
		log.errorf(
				"Received render command with type None. This generally means we're in some kind of fucked up state.",
			)
	case clay.RenderCommandType.Text:
		render_data := render_command.renderData.text
		txt := string(render_data.stringContents.chars[:render_data.stringContents.length])
		c_text := strings.clone_to_cstring(txt, temp_allocator)
		defer delete(c_text, temp_allocator)
		// Clay render-command IDs are derived via Clay's internal HashNumber (Jenkins-family)
		// and namespaced with .Clay so they can never collide with user-provided custom text IDs.
		sdl_text := cache_get_or_update(
			Cache_Key{render_command.id, .Clay},
			c_text,
			get_font(render_data.fontId, render_data.fontSize),
		)
		prepare_text(layer, Text{sdl_text, {bounds.x, bounds.y}, Color(render_data.textColor)})
	case clay.RenderCommandType.Image:
		// Any texture
		render_data := render_command.renderData.image
		if render_data.imageData == nil do return
		img_data := (^Clay_Image_Data)(render_data.imageData)^
		corner_radii_clay := render_data.cornerRadius
		radii := Rectangle_Radii {
			top_left     = corner_radii_clay.topLeft,
			top_right    = corner_radii_clay.topRight,
			bottom_right = corner_radii_clay.bottomRight,
			bottom_left  = corner_radii_clay.bottomLeft,
		}

		background_color := Color(render_data.backgroundColor)
		uv_rect, sampler, fit_rect := fit_params(img_data.fit, bounds, img_data.texture_id)

		if background_color.a > 0 {
			// Bg behind image. Push the bg primitive as the merge candidate so a matching Border
			// turns into a bg+border-merged primitive plus a separate image draw on top.
			rectangle(layer, bounds, background_color, radii = radii)
			bg_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
			rectangle(
				layer,
				fit_rect,
				Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
				radii = radii,
			)
			append(
				&GLOB.clay_merge_open_stack,
				Clay_Merge_Candidate {
					primitive_index = bg_primitive_index,
					outer_bounds = bounds,
					corner_radii = corner_radii_clay,
					kind = .Fill_Color,
				},
			)
		} else {
			// No bg: the image itself can host the outline if its fit fully covers Clay's bounds.
			// `Fit_Mode.Fit` with aspect mismatch returns a sub-rect, which can't host an outline
			// (the rasterized quad wouldn't reach Clay's outer edge), so we skip pushing.
			rectangle(
				layer,
				fit_rect,
				Texture_Fill{id = img_data.texture_id, tint = img_data.tint, uv_rect = uv_rect, sampler = sampler},
				radii = radii,
			)
			if fit_rect == bounds {
				img_primitive_index := u32(len(GLOB.tmp_primitives) - 1)
				append(
					&GLOB.clay_merge_open_stack,
					Clay_Merge_Candidate {
						primitive_index = img_primitive_index,
						outer_bounds = bounds,
						corner_radii = corner_radii_clay,
						image_data = img_data,
						kind = .Fill_Texture,
					},
				)
			}
		}
	case clay.RenderCommandType.ScissorStart:
		if bounds.width == 0 || bounds.height == 0 do return

		curr_scissor := &GLOB.scissors[layer.scissor_start + layer.scissor_len - 1]

		if curr_scissor.sub_batch_len != 0 {
			// Scissor has some content, need to make a new scissor
			new := Scissor {
				sub_batch_start = curr_scissor.sub_batch_start + curr_scissor.sub_batch_len,
				bounds          = sdl.Rect {
					c.int(bounds.x * GLOB.dpi_scaling),
					c.int(bounds.y * GLOB.dpi_scaling),
					c.int(bounds.width * GLOB.dpi_scaling),
					c.int(bounds.height * GLOB.dpi_scaling),
				},
			}
			append(&GLOB.scissors, new)
			layer.scissor_len += 1
		} else {
			curr_scissor.bounds = sdl.Rect {
				c.int(bounds.x * GLOB.dpi_scaling),
				c.int(bounds.y * GLOB.dpi_scaling),
				c.int(bounds.width * GLOB.dpi_scaling),
				c.int(bounds.height * GLOB.dpi_scaling),
			}
		}
	case clay.RenderCommandType.ScissorEnd:
	case clay.RenderCommandType.OverlayColorStart, clay.RenderCommandType.OverlayColorEnd:
		unimplemented("Clay overlays not supported yet...")
	case clay.RenderCommandType.Rectangle:
		render_data := render_command.renderData.rectangle
		corner_radii_clay := render_data.cornerRadius
		background_color := Color(render_data.backgroundColor)
		radii := Rectangle_Radii {
			top_left     = corner_radii_clay.topLeft,
			top_right    = corner_radii_clay.topRight,
			bottom_right = corner_radii_clay.bottomRight,
			bottom_left  = corner_radii_clay.bottomLeft,
		}
		rectangle(layer, bounds, background_color, radii = radii)
		// Register this primitive as a merge candidate. If the element has a matching Border
		// later in the stream (after its children's commands), `try_dispatch_clay_border_merge`
		// will pop this candidate and mutate the primitive in-place to add the outline.
		primitive_index := u32(len(GLOB.tmp_primitives) - 1)
		append(
			&GLOB.clay_merge_open_stack,
			Clay_Merge_Candidate {
				primitive_index = primitive_index,
				outer_bounds = bounds,
				corner_radii = corner_radii_clay,
				kind = .Fill_Color,
			},
		)
	case clay.RenderCommandType.Border:
		render_data := render_command.renderData.border
		if try_dispatch_clay_border_merge(bounds, render_data) do return
		clay_emit_partial_border(
			layer,
			bounds,
			Color(render_data.color),
			render_data.width,
			render_data.cornerRadius,
		)
	case clay.RenderCommandType.Custom:
		// Copy the CustomRenderData by value so we can patch its `customData` field for the
		// user callback without mutating Clay-owned memory. After unwrapping, the callback
		// sees its own pointer in `render_data.customData`, identical to what it would see
		// if `Clay_Custom` did not exist as an intermediary.
		patched := render_command.renderData.custom
		// Default to nil so a zero-init `Clay_Custom` (no variant set) and an originally-nil
		// `customData` both surface to the callback as `customData = nil`.
		patched.customData = nil
		if custom_data_pointer := render_command.renderData.custom.customData; custom_data_pointer != nil {
			switch custom_value in (^Clay_Custom)(custom_data_pointer)^ {
			case Backdrop_Marker: // The walker pre-filters backdrops into `dispatch_clay_backdrop` and never feeds
					// them here; reaching this branch means either the walker logic is broken or the
					// `Clay_Custom` variant tag mutated between the walker's `is_clay_backdrop` check
					// and this re-check (heap corruption / lifetime bug in user-managed customData
					// memory). Both are renderer-level bugs that warrant a hard failure rather than a
					// silently-dropped panel.
					log.panicf(
						"backdrop marker reached dispatch_clay_command; either the prepare_clay_batch walker is misrouting commands or the customData pointee at %p was mutated mid-frame",
						render_command.renderData.custom.customData,
					)
			case rawptr: patched.customData = custom_value
			}
		}
		if custom_draw != nil {
			custom_draw(layer, bounds, patched)
		} else if patched.customData != nil {
			log.panicf(
				"Received clay render command of type custom with non-nil user data but no custom_draw proc provided.",
			)
		}
	}
}

// Dispatch a single backdrop Clay render command to `backdrop_blur` on the active layer.
// Caller guarantees:
//   - a backdrop scope is open on `layer` so the underlying `append_or_extend_sub_batch`
//     contract assertion is satisfied;
//   - the command's `customData` points at a `Clay_Custom` whose active variant is
//     `Backdrop_Marker` (the walker has already verified this via `is_clay_backdrop`).
//INTERNAL
dispatch_clay_backdrop :: proc(layer: ^Layer, cmd: ^clay.RenderCommand) {
	bounds := Rectangle {
		x      = cmd.boundingBox.x + layer.bounds.x,
		y      = cmd.boundingBox.y + layer.bounds.y,
		width  = cmd.boundingBox.width,
		height = cmd.boundingBox.height,
	}
	// Type-asserting form (no `, ok`): panics loudly if the variant tag changed since
	// `is_clay_backdrop`, which is the desired tripwire for a heap-corruption bug in
	// user-managed customData.
	marker := (^Clay_Custom)(cmd.renderData.custom.customData).(Backdrop_Marker)
	backdrop_blur(
		layer,
		bounds,
		gaussian_sigma = marker.sigma,
		tint = marker.tint,
		radii = marker.radii,
		feather_ppx = marker.feather_ppx,
	)
}

// Close the in-flight backdrop scope (if open) and replay every command accumulated in the
// deferred index buffer. Ordering: end_backdrop first so deferred non-backdrop draws land
// at submission position relative to the bracket they followed (the bracket is now closed,
// so these draws render after it). Used at every zIndex transition and at end of stream.
//INTERNAL
flush_deferred_and_close_backdrop_scope :: proc(
	layer: ^Layer,
	batch: ^ClayBatch,
	deferred_indices: ^[dynamic]i32,
	backdrop_scope_open: ^bool,
	custom_draw: Custom_Draw,
	temp_allocator: runtime.Allocator,
) {
	if backdrop_scope_open^ {
		end_backdrop(layer)
		backdrop_scope_open^ = false
	}
	// Clear the merge stack at scope/stratum boundaries: any pending candidates from the
	// pre-scope (or pre-transition) commands stay as plain primitives — they can't merge
	// with Borders on the far side of the boundary because that would change draw order.
	clear(&GLOB.clay_merge_open_stack)
	for index in deferred_indices^ {
		cmd := clay.RenderCommandArray_Get(&batch.cmds, index)
		dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
	}
	clear(deferred_indices)
}

// ---------------------------------------------------------------------------------------------------------------------
// ----- Main entry point ------------
// ---------------------------------------------------------------------------------------------------------------------

// Process Clay render commands into shape, text, and backdrop primitives.
//
// Single-walk dispatcher with a deferred buffer. The walk does three things per command:
//   1. zIndex transitions: close the in-flight scope, flush any deferred non-backdrop
//      commands into the current layer, then open a new layer seeded with `base_layer.bounds`
//      (NOT the bumping element's bounds — Clay's floating elements with `clipTo = .None`
//      should not be over-clipped, and `clipTo = .AttachedParent` floating elements get a
//      Clay-emitted ScissorStart immediately afterward that narrows correctly).
//   2. Backdrop commands: open a scope on first encounter (extending it on subsequent ones),
//      then dispatch the backdrop_blur call.
//   3. Non-backdrop commands during an open scope: append to the deferred buffer for replay
//      after the scope closes. The buffer holds command indices, not pointers, so it stays
//      valid even if the underlying ClayArray reallocates.
// At end of stream, flush whatever remains.
prepare_clay_batch :: proc(
	base_layer: ^Layer,
	batch: ^ClayBatch,
	custom_draw: Custom_Draw = nil,
	temp_allocator := context.temp_allocator,
) {
	layer := base_layer
	command_count := int(batch.cmds.length)
	deferred_indices := make([dynamic]i32, 0, 16, temp_allocator)
	backdrop_scope_open := false
	// Seed from GLOB.clay_z_index so multi-batch frames preserve the original semantics: a
	// later call to `prepare_clay_batch` doesn't re-trigger layer splits for zIndex values
	// the previous batch already saw.
	previous_z_index := GLOB.clay_z_index

	// Start with a clean merge stack. The stack is also cleared by
	// `flush_deferred_and_close_backdrop_scope` at every stratum boundary; both clears together
	// ensure merge candidates never pair across a boundary that would shift draw order.
	clear(&GLOB.clay_merge_open_stack)
	for i in 0 ..< command_count {
		cmd := clay.RenderCommandArray_Get(&batch.cmds, i32(i))

		// zIndex transition: close out current stratum, create new layer, continue.
		if cmd.zIndex > previous_z_index {
			log.debug("Higher zIndex found, creating new layer & setting z_index to", cmd.zIndex)
			flush_deferred_and_close_backdrop_scope(
				layer,
				batch,
				&deferred_indices,
				&backdrop_scope_open,
				custom_draw,
				temp_allocator,
			)
			layer = new_layer(layer, base_layer.bounds)
			previous_z_index = cmd.zIndex
			// Keep GLOB.clay_z_index in sync for any external readers (debug tooling, etc.).
			GLOB.clay_z_index = cmd.zIndex
		}

		if is_clay_backdrop(cmd) {
			if !backdrop_scope_open {
				begin_backdrop(layer)
				backdrop_scope_open = true
			}
			dispatch_clay_backdrop(layer, cmd)
		} else if backdrop_scope_open {
			append(&deferred_indices, i32(i))
		} else {
			// Rectangle/Image dispatches push merge candidates; Border dispatches pop the stack
			// to retroactively add an outline to a matching candidate. See
			// `try_dispatch_clay_border_merge` for the matching semantics.
			dispatch_clay_command(layer, cmd, custom_draw, temp_allocator)
		}
	}

	// End-of-stream: flush whatever remains.
	flush_deferred_and_close_backdrop_scope(
		layer,
		batch,
		&deferred_indices,
		&backdrop_scope_open,
		custom_draw,
		temp_allocator,
	)
}
