package declarations

import "core:mem"
import raw "gododin:bindings/raw"
import godot "gododin:bindings/typed/godot"
import gd "gododin:bindings/typed/call"
import render "gododin:bindings/typed/render"
import bunny "../subsystem"

// Bunny_Field owns one retained float32 staging owner and one pure population.
// The scene owns the child MultiMeshInstance2D, MultiMesh, canvas ArrayMesh and texture.
// The scene driver calls submit_frame once per fixed simulation step, with no
// autonomous callback hidden outside the eval bracket. Packing reads live SoA
// positions; verification independently replays state and reads engine transforms.
// Explicit shutdown runs on every scene exit before authority disappears.
@(gododin_class="Node2D")
Bunny_Field :: struct {
	capacity:         i64,
	population_seed:  u32,
	frames_submitted: i64,
	speed_scale:      f32,
	gravity:          f32,
	allocator:        mem.Allocator,
	// One stream owns the whole submission pipeline for the scene's RID:
	// the staged store, the pending retirement, and the write/publish/
	// submit sequence. The RID itself stays scene-owned inside it.
	stream:        render.MultiMesh_Stream,
	field:         ^bunny.Field,
	retired_field: ^bunny.Field,
}

// Bunny_Field_Init records the Host-provided owning allocator once so pure
// population allocation and deallocation stay symmetric across callbacks.
// Every engine callback still establishes its valid Odin context and role.
bunny_field_init :: proc(self: ^Bunny_Field, allocator: mem.Allocator) -> bool {
	self.allocator = allocator
	return true
}

// Bunny_Field_Deinit is the allocator-symmetric counterpart of init. The pure
// population, if it is still live here, is retired under its own recorded
// allocator. The staged owner is deliberately not touched: retiring it needs
// facade call authority the teardown hook does not have, which is what the
// explicit shutdown operation is for — the same cooperative pattern the
// multimesh_stream fixture froze.
bunny_field_deinit :: proc(self: ^Bunny_Field) {
	if self.field != nil {
		_ = bunny.field_destroy(&self.field)
	}
	_ = bunny.field_destroy(&self.retired_field)
}

// Bunny_Field_Setup binds the scene's MultiMesh resource, allocates the
// requested bunny capacity through the engine's own server API, and creates
// the retained staging owner plus the pure population for that capacity. The
// scene's MultiMeshInstance2D is this node's own child 0; its MultiMesh
// resource's server RID is the workload's multimesh, resolved through the
// engine's validated casts and released immediately (the owned reference
// from get_multimesh is unreference()'d — the scene holds the owning one).
// The scene owns the server RID; this class changes its data extent.
//
// Error codes: -1 invalid call token, -2 capacity out of range, -3 staging
// already live, -4 authored object unavailable, -5 not a Node2D, -6 no
// MultiMeshInstance2D child at the scene's declared MMI path, -7 unused
// (merged into -6), -8 not a MultiMeshInstance2D,
// -9 get_multimesh failed or empty, -10 not a Resource, -11 get_rid failed or
// not live, -12 reference release failed, -13 rendering server unavailable,
// -14 multimesh_allocate_data failed, -15 facade unavailable, -16 staging
// owner creation failed, -17 pure population creation failed.
@(gododin)
bunny_field_setup :: proc(self: ^Bunny_Field, call: gd.Main_Call, count: i64, population_seed: i64) -> (code: i64) {
	if gd.validate_main_call(call) != .None do return -1
	if count <= 0 || count > bunny.MAX_COUNT do return -2
	if render.multimesh_stream_owned(&self.stream) || self.field != nil || self.retired_field != nil do return -3
	object, ok := gd.self(call)
	if !ok do return -4
	n2d, cast_ok := godot.cast_Node2D(gd.godot_main_call(call), &object)
	if !cast_ok do return -5
	// The scene owns its MultiMesh: this node's child named "MMI" (see
	// project/main.tscn) is the workload's MultiMeshInstance2D, and its
	// MultiMesh resource's RID is what the server API allocates against. The
	// name is the scene's explicit binding, so adding or reordering siblings
	// cannot silently rebind the workload.
	child, child_ok := child_by_name(gd.godot_main_call(call), cast(^godot.Node)n2d, "MMI")
	if !child_ok do return -6
	scene_object := godot.Object{ptr = child.ptr}
	mmi, mmi_ok := godot.cast_MultiMeshInstance2D(gd.godot_main_call(call), &scene_object)
	if !mmi_ok do return -8
	multimesh, multimesh_error := godot.multi_mesh_instance2d_get_multimesh(gd.godot_main_call(call), mmi)
	if multimesh_error != .None do return -9
	if !multimesh.live do return -9
	// get_multimesh returns an owned reference, not a borrowed one. A typed
	// MultiMesh is a RefCounted Resource; release on every subsequent return.
	refc := godot.RefCounted{ptr=multimesh.handle.ptr}
	defer {
		if _, err := godot.ref_counted_unreference(gd.godot_main_call(call), &refc); err != .None {code = -12}
	}
	resource := godot.Resource{ptr=multimesh.handle.ptr}
	handle, rid_error := godot.resource_get_rid(gd.godot_main_call(call), &resource)
	if rid_error != .None || !handle.live do return -11
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -15
	stream, open_error := render.multimesh_stream_open(
		render.main_call(api),
		handle.value,
		render.MultiMesh_Layout{},
		int(count),
	)
	if open_error != .None do return -14
	self.stream = stream // Reachable by shutdown even if population allocation fails.
	field := bunny.field_create_with_allocator(count, u32(population_seed & 0xFFFFFFFF), self.allocator)
	if field == nil do return -17
	self.capacity         = count
	self.population_seed  = u32(population_seed & 0xFFFFFFFF)
	self.frames_submitted = 0
	self.speed_scale      = bunny.CANONICAL_SPEED_SCALE
	self.gravity          = bunny.CANONICAL_GRAVITY
	self.field            = field
	return 0
}

// Bunny_Field_Submit_Frame is one workload frame: the pure subsystem integrates
// every bunny in one call, the producer writes the staged store in the earned
// 2D layout, the publisher releases it, and one coarse server call submits
// it. Nothing per-instance crosses the boundary, nothing allocates, and the
// configured Safe rendering model consumes on the main/server thread, which
// the scene checks at startup, so the next frame may rewrite the same store. The step always runs at the fixed canonical 1/60 dt so
// canonical-profile runs are replay-verifiable. Custom history is explicit.
//
// Error codes: -1 invalid call token, -2 staging or population not live,
// -3 facade unavailable, -4 staging write view failed, -5 pure step or layout
// write failed, -6 publish failed, -7 unused (owned by submit), -8 submit
// refused (no new publication, zero RID handle, or refused engine call).
@(gododin)
bunny_field_submit_frame :: proc(self: ^Bunny_Field, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if !self.stream.staging.live || self.field == nil || self.capacity <= 0 do return -2
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -3
	step_ok := bunny.field_step(self.field, bunny.CANONICAL_DT, bunny.CANONICAL_BOUNDS_X, bunny.CANONICAL_BOUNDS_Y,
		self.speed_scale, self.gravity)
	if !step_ok do return -5
	view, write_error := render.multimesh_stream_write(render.main_call(api), &self.stream)
	if write_error != .None do return -4
	if !bunny.field_write_layout(self.field, view) do return -5
	if _, publish_error := render.multimesh_stream_publish(&self.stream, .Main); publish_error != .None do return -6
	if submit_error := render.multimesh_stream_submit(render.main_call(api), &self.stream); submit_error != .None do return -8
	self.frames_submitted += 1
	return 0
}

// Bunny_Field_Set_Count exercises the explicit capacity change: the same
// scene-owned RID is re-allocated through the server at the new count and
// both the staging owner and the pure population are replaced. The new owners
// are created first and the old ones retire only after the whole change
// succeeded. Once the server resize succeeds the new state is authoritative;
// a cleanup error is fatal and does not claim to undo engine storage.
//
// Error codes: -1 invalid call token, -2 count out of range, -3 current state
// not live, -4 facade unavailable, -5 stream resize failed (staged store
// creation, server re-allocation, or a pending retirement that refused to
// die), -6 new population failed, -7/-8 unused (folded into -5), -9 pending
// staging or population could not retire. Once the server resize succeeds,
// replacement state remains adopted even if old cleanup fails.
@(gododin)
bunny_field_set_count :: proc(self: ^Bunny_Field, call: gd.Main_Call, count: i64) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if count <= 0 || count > bunny.MAX_COUNT do return -2
	if !self.stream.staging.live || self.field == nil do return -3
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -4
	if !retire_previous(self, call) do return -9
	self.retired_field = bunny.field_create_with_allocator(count, self.population_seed, self.allocator)
	if self.retired_field == nil do return -6
	if resize_error := render.multimesh_stream_resize(render.main_call(api), &self.stream, int(count)); resize_error != .None {
		_ = retire_previous(self, call)
		return -5
	}
	// Server extent has changed and the stream adopted its new store; adopt
	// the matching pending population. The former active owners remain in
	// the retirement slots for retry if cleanup fails.
	self.field, self.retired_field = self.retired_field, self.field
	self.capacity = count
	self.frames_submitted = 0
	if !retire_previous(self, call) do return -9
	return 0
}

// Bunny_Field_Set_Profile retunes the ordinary live speed and gravity controls. The bounds and dt stay canonical. The
// staged oracle only applies at the canonical profile, so after a non-canonical
// retune `verify` reports that verification does not apply until the profile
// returns to canonical and a fresh population is installed.
//
// Returns -1 for an invalid call token and -2 for non-finite values.
@(gododin)
bunny_field_set_profile :: proc(self: ^Bunny_Field, call: gd.Main_Call, speed_scale: f64, gravity: f64) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if !bunny.valid_profile({bunny.CANONICAL_DT,1280,720,f32(speed_scale),f32(gravity)}) do return -2
	self.speed_scale = f32(speed_scale)
	self.gravity     = f32(gravity)
	return 0
}

// Bunny_Field_Verify reads the complete packed transform buffer and checks every
// bunny bitwise against independent canonical replay and the submitted staging.
// The bulk getter avoids dev5 Compatibility's individual-getter CPU cache, which
// would retain subsequent transform-only submissions and invalidate staging reuse.
// The engine readback allocates/stalls outside timing; its owner is released before
// returning, so verification can be followed by more submitted frames.
//
// Requires transform-only MultiMesh data, physics interpolation off, and calls
// on the render thread. See dev5 drivers/gles3/storage/mesh_storage.cpp:1965,
// 2092, 2133: bulk readback does not create the per-instance data cache.
// Verification applies only to the canonical profile; a non-canonical retune
// reports -30, meaning healthy but unverified, rather than a state mismatch.
// Error codes: -1 invalid call token, -2 no capacity, -3 nothing submitted,
// -4 rendering server unavailable, -10 bulk getter, -11 readback extent/view,
// -12 readback release, -20 engine transform mismatch, -21 staged payload/state
// mismatch, -22 staging unavailable, -30 non-canonical physics.
@(gododin)
bunny_field_verify :: proc(self: ^Bunny_Field, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if self.capacity <= 0 || self.field == nil do return -2
	if self.frames_submitted == 0 do return -3
	if !self.field.profile_uniform || !bunny.profile_equal(self.field.profile, bunny.canonical_profile()) do return -30
	engine_call := gd.godot_main_call(call)
	server, server_error := godot.singleton_rendering_server(engine_call)
	if server_error != .None do return -4
	frames := self.field.frame
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -22
	view, view_error := render.multimesh_stream_write(render.main_call(api), &self.stream)
	if view_error != .None do return -22
	owned, get_error := godot.rendering_server_multimesh_get_buffer(engine_call, &server, self.stream.rid)
	if get_error != .None do return -10
	// This facade value only borrows `owned`; never destroy both representations.
	readback := render.Packed_Float32_Array{value=godot.owned_value_borrow(&owned), live=true}
	base, length, read_error := render.packed_float32_array_read_base(api, &readback)
	result := i64(0)
	if read_error != .None || base == nil || length != int(self.capacity)*bunny.STRIDE {
		result = -11
	} else {
		stored := mem.slice_ptr(base, length)
		for representative in 0..<self.capacity {
			start := int(representative)*bunny.STRIDE
			if !bunny.oracle_instance(view[start:start+bunny.STRIDE], representative, frames, self.population_seed) {
				result = -21
				break
			}
			expected := bunny.reference_state(representative, frames, self.population_seed)
			actual := [4]f32{self.field.x[representative], self.field.y[representative], self.field.vx[representative], self.field.vy[representative]}
			want := [4]f32{expected.x0, expected.y0, expected.vx0, expected.vy0}
			for value, i in actual {
				if transmute(u32)value != transmute(u32)want[i] {
					result = -21
					break
				}
			}
			if result != 0 do break
			if !bunny.oracle_instance(stored[start:start+bunny.STRIDE], representative, frames, self.population_seed) {
				result = -20
				break
			}
		}
	}
	if godot.owned_value_destroy(engine_call.api, &owned) != .None do return -12
	return result
}

// Bunny_Field_Shutdown retires the retained owners explicitly: the staging
// owner and the pure population. The teardown hook has no authority, so
// cooperative shutdown — the compositor's pattern — is the symmetric cleanup
// this owner pair's lifetime contract requires.
//
// Error codes: -1 invalid call token, -2 facade unavailable, -3 staging
// could not retire, -4 population could not retire.
@(gododin)
bunny_field_shutdown :: proc(self: ^Bunny_Field, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if !retire_previous(self, call) do return -2
	if !render.multimesh_stream_owned(&self.stream) && self.field == nil do return 0
	code := i64(0)
	if render.multimesh_stream_owned(&self.stream) {
		api, facade_ok := gd.main_facade(call)
		if !facade_ok do return -2
		if close_error := render.multimesh_stream_close(render.main_call(api), &self.stream); close_error != .None {
			code = -3
		}
	}
	if self.field != nil {
		if !bunny.field_destroy(&self.field) && code == 0 do code = -4
	}
	if !bunny.field_destroy(&self.retired_field) && code == 0 do code = -4
	if code == 0 {self.capacity = 0; self.frames_submitted = 0}
	return code
}

@(gododin)
bunny_field_frames_submitted :: proc(self: ^Bunny_Field) -> i64 {
	return self.frames_submitted
}

@(gododin)
bunny_field_count :: proc(self: ^Bunny_Field) -> i64 {
	return self.capacity
}

@(gododin)
bunny_field_staging_live :: proc(self: ^Bunny_Field) -> bool {
	return render.multimesh_stream_owned(&self.stream)
}

// Bunny_Field_Verify_Buffer checks the GDScript arm's actual x/y/vx/vy/scale
// rows against independent replay. It borrows the packed input only for this
// call. Returns -1 for invalid extent/frame count, -2 for a bitwise mismatch.
@(gododin)
bunny_field_verify_buffer :: proc(self: ^Bunny_Field, values: []f32, frames, seed: i64) -> i64 {
	_ = self
	if len(values)==0 || len(values)%5!=0 || len(values)/5>bunny.MAX_COUNT || frames<0 || frames>100000 do return -1
	for i in 0..<len(values)/5 {
		p := bunny.reference_state(i64(i),frames,u32(seed & 0xffffffff))
		expected := [5]f32{p.x0,p.y0,p.vx0,p.vy0,p.scale}
		for v,j in expected {if transmute(u32)v != transmute(u32)values[i*5+j] do return -2}
	}
	return 0
}

// Bunny_Field_Verifiable reports whether the current history matches the
// canonical seed/frame replay; returning sliders to canonical does not erase
// earlier custom steps. A resize starts a fresh history.
@(gododin)
bunny_field_verifiable :: proc(self: ^Bunny_Field) -> bool {
	return self.field != nil && self.field.profile_uniform && bunny.profile_equal(self.field.profile,bunny.canonical_profile())
}

// A failed staging write can invalidate its view while its packed array remains
// owned. Cleanup follows ownership of either state, not only view validity.

// retire_previous keeps pre-adoption and old active populations in instance
// storage while trying their cleanup, so every later shutdown can reach them.
// The stream's pending retirement lives inside the stream owner now, so this
// handles only the pure population.
retire_previous :: proc(self: ^Bunny_Field, call: gd.Main_Call) -> bool {
	_ = call
	return bunny.field_destroy(&self.retired_field)
}

// child_by_name resolves one direct child of `node` by the scene's declared
// name through the generated surface alone: the interface's own Latin-1
// StringName constructor, the generated String and NodePath constructors, and
// get_node_or_null. Every intermediate engine value is released on every
// path, including the constructor-slot binds when they fail after an
// allocation. This is a bind-time helper, never a hot path. ok is false when
// the call is stale, any engine step refuses, or the path resolves to
// nothing; the caller's class validation distinguishes a wrong-kind target.
child_by_name :: proc(call: godot.Call, node: ^godot.Node, name: cstring) -> (godot.Node, bool) {
	if error := godot.validate_call(call); error != .None do return {}, false
	api := call.api
	name_sn: raw.StringName
	api.string_name_new_latin1(
		raw.GDExtensionUninitializedStringNamePtr(rawptr(&name_sn)),
		transmute(^i8)name,
		0,
	)
	defer api.string_name_destructor(godot.type_pointer(rawptr(&name_sn)))
	if godot.bind_construct_string_from_string_name(api) != .None do return {}, false
	text, text_error := godot.construct_string_from_string_name(call, name_sn)
	if text_error != .None do return {}, false
	if godot.bind_construct_node_path_from_string(api) != .None {
		_ = godot.owned_string_destroy(api, &text)
		return {}, false
	}
	path, path_error := godot.construct_node_path_from_string(call, text.value)
	text_destroy_error := godot.owned_string_destroy(api, &text)
	if path_error != .None || text_destroy_error != .None do return {}, false
	defer godot.owned_value_destroy(api, &path)
	found, found_error := godot.node_get_node_or_null(call, node, path.value)
	if found_error != .None do return {}, false
	return found, found.ptr != nil
}
