--!strict
--# selene: allow(bad_string_escape)
local tests = require("./tests")
local kitchen_sink = require("./samples/kitchen_sink")

local assertEquals = tests.assertEquals
local describe = tests.describe
local it = tests.it

local UNICODE_REPLACEMENT_CHARACTER = "\xEF\xBF\xBD"

-- Helper to build a raw protobuf binary with a single string field.
-- Uses field 33 (text) of KitchenSink, which is a `string` field.
-- Tag: (33 << 3) | 2 = 266 -> varint bytes 0x8A, 0x02
local function makeRawStringMessage(bytes: { number }): buffer
	local raw = buffer.create(2 + 1 + #bytes)
	buffer.writeu8(raw, 0, 0x8A) -- tag byte 1
	buffer.writeu8(raw, 1, 0x02) -- tag byte 2
	buffer.writeu8(raw, 2, #bytes) -- length (assumes < 128)
	for i, byte in bytes do
		buffer.writeu8(raw, 2 + i, byte)
	end
	return raw
end

describe("invalid UTF-8 handling", function()
	describe("decoding binary with invalid UTF-8 in string fields", function()
		it("should replace a lone continuation byte (0x80) with U+FFFD", function()
			local raw = makeRawStringMessage({ 0x80 })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace byte 0xFF with U+FFFD", function()
			local raw = makeRawStringMessage({ 0xFF })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace byte 0xFE with U+FFFD", function()
			local raw = makeRawStringMessage({ 0xFE })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace a truncated 2-byte sequence with U+FFFD", function()
			-- 0xC3 is the start of a 2-byte sequence but has no continuation byte
			local raw = makeRawStringMessage({ 0xC3 })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace only the invalid byte in otherwise valid ASCII", function()
			-- "A" 0x80 "B" - continuation byte embedded in otherwise valid ASCII
			local raw = makeRawStringMessage({ 0x41, 0x80, 0x42 })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "A" .. UNICODE_REPLACEMENT_CHARACTER .. "B")
		end)
	end)

	describe("valid UTF-8 should still decode correctly", function()
		it("should decode plain ASCII", function()
			local raw = makeRawStringMessage({ 0x68, 0x65, 0x6C, 0x6C, 0x6F }) -- "hello"
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "hello")
		end)

		it("should decode multi-byte characters (e-acute)", function()
			-- "héllo" = 0x68, 0xC3, 0xA9, 0x6C, 0x6C, 0x6F
			local raw = makeRawStringMessage({ 0x68, 0xC3, 0xA9, 0x6C, 0x6C, 0x6F })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "h\xC3\xA9llo")
		end)

		it("should decode 3-byte characters (CJK)", function()
			-- U+65E5 (日) = 0xE6 0x97 0xA5
			local raw = makeRawStringMessage({ 0xE6, 0x97, 0xA5 })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "\xE6\x97\xA5")
		end)

		it("should decode 4-byte characters (emoji)", function()
			-- U+1F600 (😀) = 0xF0 0x9F 0x98 0x80
			local raw = makeRawStringMessage({ 0xF0, 0x9F, 0x98, 0x80 })
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "\xF0\x9F\x98\x80")
		end)

		it("should round-trip valid multi-byte strings through encode/decode", function()
			local original = kitchen_sink.KitchenSink.new({
				text = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E", -- 日本語
			})
			local encoded = original:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, original.text)
		end)

		it("should handle the empty string", function()
			local raw = makeRawStringMessage({})
			local decoded = kitchen_sink.KitchenSink.decode(raw)
			assertEquals(decoded.text, "")
		end)
	end)

	describe("encoding should replace invalid UTF-8 with U+FFFD", function()
		it("should replace a lone continuation byte during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "\x80",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace 0xFF during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "\xFF",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace a truncated multi-byte sequence during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "\xC3", -- start of 2-byte sequence, missing continuation
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace invalid bytes embedded within valid UTF-8 during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "hello\x80world",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, "hello" .. UNICODE_REPLACEMENT_CHARACTER .. "world")
		end)

		it("should replace invalid bytes at the start of a string during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "\xFFhello",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, UNICODE_REPLACEMENT_CHARACTER .. "hello")
		end)

		it("should replace invalid bytes at the end of a string during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "hello\xFE",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.text, "hello" .. UNICODE_REPLACEMENT_CHARACTER)
		end)

		it("should replace multiple invalid bytes scattered through a string during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "one\x80two\xFFthree",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(
				decoded.text,
				"one" .. UNICODE_REPLACEMENT_CHARACTER .. "two" .. UNICODE_REPLACEMENT_CHARACTER .. "three"
			)
		end)

		it("should replace invalid bytes at start, middle, and end during encode", function()
			local msg = kitchen_sink.KitchenSink.new({
				text = "\xFEhello\x80world\xFF",
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(
				decoded.text,
				UNICODE_REPLACEMENT_CHARACTER
					.. "hello"
					.. UNICODE_REPLACEMENT_CHARACTER
					.. "world"
					.. UNICODE_REPLACEMENT_CHARACTER
			)
		end)

		it("should replace invalid UTF-8 in a repeated string field", function()
			local msg = kitchen_sink.KitchenSink.new({
				strings = { "valid", "\x80\xFF", "also valid" },
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(
				decoded.strings,
				{ "valid", UNICODE_REPLACEMENT_CHARACTER .. UNICODE_REPLACEMENT_CHARACTER, "also valid" }
			)
		end)

		it("should replace invalid UTF-8 in a oneof string field", function()
			local msg = kitchen_sink.KitchenSink.new({
				oneofValue = { type = "y", value = "\xFE\xFF" },
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(
				decoded.oneofValue,
				{ type = "y", value = UNICODE_REPLACEMENT_CHARACTER .. UNICODE_REPLACEMENT_CHARACTER }
			)
		end)

		it("should replace invalid UTF-8 in a map string value", function()
			local msg = kitchen_sink.KitchenSink.new({
				map = { [true] = "\x80invalid" },
			})
			local encoded = msg:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(decoded.map, { [true] = UNICODE_REPLACEMENT_CHARACTER .. "invalid" })
		end)
	end)

	describe("bytes fields should accept arbitrary data", function()
		it("should round-trip invalid UTF-8 in a bytes field without error", function()
			local original = kitchen_sink.KitchenSink.new({
				bytes = buffer.fromstring("\x80\xFF\xFE\xC0"),
			})
			local encoded = original:encode()
			local decoded = kitchen_sink.KitchenSink.decode(encoded)
			assertEquals(buffer.tostring(decoded.bytes), "\x80\xFF\xFE\xC0")
		end)
	end)
end)

tests.finish()


-- Jest does not yet work in Lune.
-- Replace this when it can.
local tests = {}

local anyFailed = false
local indentCount = 0

local function indent()
	return string.rep("\t", indentCount)
end

function tests.describe(category: string, callback: () -> ())
	print(`{indent()}{category}`)
	indentCount += 1
	callback()
	indentCount -= 1
end

function tests.it(message: string, callback: () -> ())
	local success, problemWithStackTrace = xpcall(function()
		callback()
	end, function(problem)
		return debug.traceback(problem)
	end)

	print(`{indent()}{if success then "✅" else "❌"} {message}`)

	if not success then
		anyFailed = true
		print(problemWithStackTrace)
	end
end

local function deepEqual<T>(x: T, y: T): boolean
	if typeof(x) == "table" and typeof(y) == "table" then
		for key, value in x :: any do
			if not deepEqual(value, y[key]) then
				return false
			end
		end

		for key in y :: any do
			if x[key] == nil then
				return false
			end
		end

		return true
	elseif typeof(x) == "buffer" and typeof(y) == "buffer" then
		return buffer.tostring(x) == buffer.tostring(y)
	end

	return x == y
end

function tests.assertEquals<T>(x: T, y: T)
	if not deepEqual(x, y) then
		error(`Assertion failed. Expected \`{x}\` to equal \`{y}\``)
	end
end

function tests.assertNotEquals<T>(x: T, y: T)
	if deepEqual(x, y) then
		error(`Assertion failed. Expected \`{x}\` not to equal \`{y}\``)
	end
end

function tests.assertStringContains(haystack: string, needle: string)
	if not string.find(haystack, needle) then
		error(`Assertion failed. Expected \`{haystack}\` to contain \`{needle}\``)
	end
end

function tests.assertThrows(callback: () -> ()): any?
	local thrown = nil
	local success = xpcall(callback, function(problem)
		thrown = problem
	end)

	if success then
		error("Expected an error to be thrown, but it wasn't")
	end

	return thrown
end

function tests.finish()
	if anyFailed then
		error("Tests failed")
	end
end

return tests


local tests = require("./tests")

local proto = require("./samples/proto")

local any = require("./samples/google/protobuf/any")
local api = require("./samples/google/protobuf/api")
local duration = require("./samples/google/protobuf/duration")
local field_mask = require("./samples/google/protobuf/field_mask")
local struct = require("./samples/google/protobuf/struct")
local timestamp = require("./samples/google/protobuf/timestamp")
local wrappers = require("./samples/google/protobuf/wrappers")

local kitchen_sink = require("./samples/kitchen_sink")

local assertEquals = tests.assertEquals
local assertThrows = tests.assertThrows
local assertStringContains = tests.assertStringContains
local describe = tests.describe
local it = tests.it

describe("JSON round-trips should work for", function()
	describe("Api", function()
		-- Luau type regression test
		it("should allow encoding of inner methods", function()
			local apiObject = api.Api.new({
				methods = {
					api.Method.new(),
				},
			})

			local _method: api.Method = apiObject.methods[1]

			for _, method in apiObject.methods do
				method:encode()
			end
		end)
	end)

	describe("Value", function()
		it("with null value", function()
			local json = nil

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")

			assertEquals(deserializedValue.kind.type, "null_value")
			assertEquals(deserializedValue.kind.value, "NULL_VALUE")

			assertEquals(deserializedValue:jsonEncode(), json)
		end)

		it("with number value", function()
			local json = 3.14

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")

			assertEquals(deserializedValue.kind.type, "number_value")
			assertEquals(deserializedValue.kind.value, 3.14)

			assertEquals(deserializedValue:jsonEncode(), json)
		end)

		it("with string value", function()
			local json = "Hello, World!"

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")

			assertEquals(deserializedValue.kind.type, "string_value")
			assertEquals(deserializedValue.kind.value, "Hello, World!")

			assertEquals(deserializedValue:jsonEncode(), json)
		end)

		it("with boolean value", function()
			local json = true

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")

			assertEquals(deserializedValue.kind.type, "bool_value")
			assertEquals(deserializedValue.kind.value, true)

			assertEquals(deserializedValue:jsonEncode(), json)
		end)

		it("with struct value", function()
			local json = {
				name = {
					family = "Doe",
					given = "John",
				},
				age = 42,
			}

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")
			assert(deserializedValue.kind.type == "struct_value", "Deserialized value is not a Struct")

			local nameKind = deserializedValue.kind.value.fields.name.kind
			assert(nameKind ~= nil, "Couldn't parse name field")
			assert(nameKind.type == "struct_value", "Name wasn't a struct")

			local familyKind = nameKind.value.fields.family.kind
			assert(familyKind ~= nil, "Couldn't parse family field")

			assertEquals(familyKind.value, "Doe")

			assertEquals(deserializedValue:jsonEncode(), json)
		end)

		it("with list value", function()
			local json: { any } = {
				3.14,
				"Hello, World!",
				true,
			}

			local deserializedValue = struct.Value.jsonDecode(json)
			assert(deserializedValue.kind ~= nil, "Couldn't parse Value")
			assert(deserializedValue.kind.type == "list_value", "Deserialized value is not a ListValue")

			local firstValue = deserializedValue.kind.value.values[1]
			assert(firstValue.kind ~= nil, "Couldn't parse Value")

			local secondValue = deserializedValue.kind.value.values[2]
			assert(secondValue.kind ~= nil, "Couldn't parse Value")

			local thirdValue = deserializedValue.kind.value.values[3]
			assert(thirdValue.kind ~= nil, "Couldn't parse Value")

			assertEquals(firstValue.kind.type, "number_value")
			assertEquals(firstValue.kind.value, 3.14)

			assertEquals(secondValue.kind.type, "string_value")
			assertEquals(secondValue.kind.value, "Hello, World!")

			assertEquals(thirdValue.kind.type, "bool_value")
			assertEquals(thirdValue.kind.value, true)

			assertEquals(deserializedValue:jsonEncode(), json)
		end)
	end)

	describe("Timestamp", function()
		it("with seconds", function()
			local json = "1970-01-01T00:00:00Z"

			local deserialized_timestamp = timestamp.Timestamp.jsonDecode(json)

			assertEquals(deserialized_timestamp.seconds, 0)
			assertEquals(deserialized_timestamp.nanos, 0)

			assertEquals(deserialized_timestamp:jsonEncode(), json)
		end)

		it("with seconds and nanos", function()
			local json = "1970-01-01T00:00:00.000000001Z"

			local deserialized_timestamp = timestamp.Timestamp.jsonDecode(json)

			assertEquals(deserialized_timestamp.seconds, 0)
			assertEquals(deserialized_timestamp.nanos, 1)

			assertEquals(deserialized_timestamp:jsonEncode(), json)
		end)

		it("with large seconds", function()
			-- The maximum timestamp supported by os.date on Windows 11 seems to be 32536849999 (1/19/3001 21:53:19).
			-- However, Timestamp is supposed to support up to 253402300799 (12/31/9999 23:59:59).
			-- The protobuf conformance test uses 253402300800.
			local large_timestamp = timestamp.Timestamp.new({ seconds = 253402300800 })

			local error = assertThrows(function()
				large_timestamp:jsonEncode()
			end)

			assertStringContains(error :: string, "Invalid timestamp provided")
		end)

		it("with small seconds", function()
			-- This comes from the TimestampProtoInputTooSmall conformance test, which currently fails,
			-- even though this test passes...?
			local small_timestamp = timestamp.Timestamp.new({ seconds = -62135596801 })

			local error = assertThrows(function()
				small_timestamp:jsonEncode()
			end)

			assertStringContains(error :: string, "Invalid timestamp provided")
		end)
	end)

	describe("Duration", function()
		it("with seconds", function()
			local json = "1s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 1)
			assertEquals(deserialized_duration.nanos, 0)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with negative seconds", function()
			local json = "-1s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, -1)
			assertEquals(deserialized_duration.nanos, 0)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with nanos", function()
			local json = "0.000000001s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 0)
			assertEquals(deserialized_duration.nanos, 1)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with seconds and many nanos", function()
			local json = "1.5s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 1)
			assertEquals(deserialized_duration.nanos, 500000000)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with seconds and one nano", function()
			local json = "1.000000001s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 1)
			assertEquals(deserialized_duration.nanos, 1)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with many negative nanos", function()
			local json = "-0.5s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 0)
			assertEquals(deserialized_duration.nanos, -500000000)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with one negative nano", function()
			local json = "-0.000000001s"

			local deserialized_duration = duration.Duration.jsonDecode(json)

			assertEquals(deserialized_duration.seconds, 0)
			assertEquals(deserialized_duration.nanos, -1)

			assertEquals(deserialized_duration:jsonEncode(), json)
		end)

		it("with too many seconds", function()
			local json = "315576000001s"

			local error = assertThrows(function()
				duration.Duration.jsonDecode(json)
			end)

			assertStringContains(error :: string, "Duration seconds cannot exceed 315576000000")
		end)

		it("with too few seconds", function()
			local json = "-315576000001s"

			local error = assertThrows(function()
				duration.Duration.jsonDecode(json)
			end)

			assertStringContains(error :: string, "Duration seconds cannot be less than %-315576000000")
		end)
	end)

	describe("FieldMask", function()
		it("with single path", function()
			local json = "foo"

			local deserialized_field_mask = field_mask.FieldMask.jsonDecode(json)

			assertEquals(#deserialized_field_mask.paths, 1)
			assertEquals(deserialized_field_mask.paths[1], "foo")

			assertEquals(deserialized_field_mask:jsonEncode(), json)
		end)

		it("with multiple paths", function()
			local json = "foo,bar.baz"

			local deserialized_field_mask = field_mask.FieldMask.jsonDecode(json)

			assertEquals(#deserialized_field_mask.paths, 2)
			assertEquals(deserialized_field_mask.paths[1], "foo")
			assertEquals(deserialized_field_mask.paths[2], "bar.baz")

			assertEquals(deserialized_field_mask:jsonEncode(), json)
		end)
	end)

	describe("Any", function()
		it("with normal message", function()
			local json = {
				["@type"] = "type.googleapis.com/my.package.KitchenSink",
				int32 = 42,

				-- These need to be included because JSON output currently includes unset enum values.
				aliasedEnum = 0,
				enum = 0,
				nestedEnum = 0,
				enum2 = 0,
			}

			local deserialized_any = any.Any.jsonDecode(json)

			assertEquals(deserialized_any.type_url, "type.googleapis.com/my.package.KitchenSink")

			local unpacked = deserialized_any:unpack() :: proto.Message<kitchen_sink.KitchenSink, unknown>
			assertEquals(unpacked.int32, 42)

			assertEquals(deserialized_any:jsonEncode(), json)
			assertEquals(any.Any.pack(unpacked, "type.googleapis.com"):jsonEncode(), json)
		end)

		it("with WKT", function()
			local json = {
				["@type"] = "type.googleapis.com/google.protobuf.Timestamp",
				value = "1970-01-01T00:00:01.000000001Z",
			}

			local deserialized_any = any.Any.jsonDecode(json)

			assertEquals(deserialized_any.type_url, "type.googleapis.com/google.protobuf.Timestamp")
			local unpacked = deserialized_any:unpack() :: proto.Message<timestamp.Timestamp, unknown>
			assertEquals(unpacked.seconds, 1)
			assertEquals(unpacked.nanos, 1)

			assertEquals(deserialized_any:jsonEncode(), json)
			assertEquals(any.Any.pack(unpacked, "type.googleapis.com"):jsonEncode(), json)
		end)

		it("should be able to use traditional message methods on unpacked results", function()
			local anySink = any.Any
				.jsonDecode({
					["@type"] = "type.googleapis.com/my.package.KitchenSink",
					int32 = 42,
				})
				:unpack()
			assert(anySink ~= nil, "Couldn't unpack")

			local equivalent = kitchen_sink.KitchenSink
				.new({
					int32 = 42,
				})
				:encode()

			assertEquals(anySink:encode(), equivalent)
		end)
	end)

	describe("wrapper for", function()
		it("bool", function()
			local bool_value = wrappers.BoolValue.new({ value = true })

			local json = bool_value:jsonEncode()
			assertEquals(json, true)

			local deserialized_bool_value = wrappers.BoolValue.jsonDecode(json)
			assertEquals(bool_value, deserialized_bool_value)
		end)

		it("int32", function()
			local int32_value = wrappers.Int32Value.new({ value = 42 })

			local json = int32_value:jsonEncode()
			assertEquals(json, 42)

			local deserialized_int32_value = wrappers.Int32Value.jsonDecode(json)
			assertEquals(int32_value, deserialized_int32_value)
		end)

		it("int64", function()
			local int64_value = wrappers.Int64Value.new({ value = 42 })

			local json = int64_value:jsonEncode()
			assertEquals(json, 42)

			local deserialized_int64_value = wrappers.Int64Value.jsonDecode(json)
			assertEquals(int64_value, deserialized_int64_value)
		end)

		it("uint32", function()
			local uint32_value = wrappers.UInt32Value.new({ value = 42 })

			local json = uint32_value:jsonEncode()
			assertEquals(json, 42)

			local deserialized_uint32_value = wrappers.UInt32Value.jsonDecode(json)
			assertEquals(uint32_value, deserialized_uint32_value)
		end)

		it("uint64", function()
			local uint64_value = wrappers.UInt64Value.new({ value = 42 })

			local json = uint64_value:jsonEncode()
			assertEquals(json, 42)

			local deserialized_uint64_value = wrappers.UInt64Value.jsonDecode(json)
			assertEquals(uint64_value, deserialized_uint64_value)
		end)

		it("float", function()
			local float_value = wrappers.FloatValue.new({ value = 3.14 })

			local json = float_value:jsonEncode()
			assertEquals(json, 3.14)

			local deserialized_float_value = wrappers.FloatValue.jsonDecode(json)
			assertEquals(float_value, deserialized_float_value)
		end)

		it("double", function()
			local double_value = wrappers.DoubleValue.new({ value = 3.14 })

			local json = double_value:jsonEncode()
			assertEquals(json, 3.14)

			local deserialized_double_value = wrappers.DoubleValue.jsonDecode(json)
			assertEquals(double_value, deserialized_double_value)
		end)

		it("string", function()
			local string_value = wrappers.StringValue.new({ value = "Hello, World!" })

			local json = string_value:jsonEncode()
			assertEquals(json, "Hello, World!")

			local deserialized_string_value = wrappers.StringValue.jsonDecode(json)
			assertEquals(string_value, deserialized_string_value)
		end)

		it("bytes", function()
			local bytes_value = wrappers.BytesValue.new({
				value = buffer.fromstring("Hello, World!"),
			})

			local json = bytes_value:jsonEncode()

			-- TODO: should a BytesValue be base64-encoded?
			assertEquals(json, "Hello, World!")

			local deserialized_bytes_value = wrappers.BytesValue.jsonDecode(json)
			assertEquals(bytes_value, deserialized_bytes_value)
		end)
	end)
end)

tests.finish()


local cf = require("./cloudflare")

local cname = require("../cname")
local success, oldCname = pcall(function()
	return require("../cname-old")
end)

if not success then
	print("No old cname file found, creating a new one.")
	oldCname = {}
end

local delete = table.clone(oldCname)
local add = {}
local update = {}

for name, content in cname do
	if oldCname[name] == nil then
		add[name] = content -- net new
	else
		delete[name] = nil -- exists in new, not deleted
		if oldCname[name] ~= content then
			update[name] = content -- content changed
		end
	end
end

for name, content in add do
	print("add", name)
	cf.addRecord(name, content)
end

for name, content in update do
	print("update", name)
	cf.updateRecord(name, content)
end

for name, _ in delete do
	print("delete", name)
	cf.deleteRecord(name)
end

cf.sendBatch()


local fs = require("@lute/fs")
local site = require("./")

pcall(fs.mkdir, "dist")

fs.writestringtofile("dist/index.html", site())


local net = require("@lute/net")
local fs = require("@lute/fs")
local process = require("@lute/process")

local server = net.serve(function()
	return fs.readfiletostring("dist/index.html")
end)

print(`Serving on http://{server.hostname}:{server.port}`)

process.run({ "echo site/init.luau | entr lute site/build.luau &" }, { shell = true })


--!nocheck
--selene: allow(incorrect_standard_library_use)
local build_single_example = require("../../build-examples/build_single_example")

build_single_example("01-hello-autput", "bin")


--!nocheck
-- See src/main.rs for an introduction to this example.

local autput = require(script.Parent.autput)
local example_wasm_loader = require(script.Parent.target_luau.example_wasm)

local wasm_env = { func_list = {} }
local finish_autput_init = autput.init(wasm_env)
local example_wasm_module = example_wasm_loader({env = wasm_env})
finish_autput_init(example_wasm_module)

example_wasm_module.func_list.main()


--!strict
--Licensed under BSD3 from Autput, (c) Daniel P H Fox 2025

--------------------------------------------------------------------------------
-- TYPE DEFINITIONS ------------------------------------------------------------
--------------------------------------------------------------------------------

-- Type aliases for convenience and documentation
type ptr_const_u8 = number
type u32 = number
type u8 = number

-- Passed to Wasynth when initialising a WebAssembly module.
-- Autput injects some of its own members into this environment, but otherwise
-- does not care about the rest of the contents.
type WasmEnvironment = {
	func_list: {
		[string]: (...any) -> (...any)
	}
}

-- Returned by Wasynth when initialising a WebAssembly module.
-- Autput expects certain members to be present.
type WasmModule = {
	rt: {
		load: {
			string: (
				memory: WasmMemory,
				ptr: ptr_const_u8,
				len: u32
			) -> string
		}
	},
	memory_list: {
		memory: WasmMemory
	}
}

-- An entry in the `memory_list` dictionary returned by Wasynth.
-- Autput doesn't care about its specific type - it's treated opaquely.
type WasmMemory = unknown

-- The internal state used by Autput's Luau-side API.
-- This is not shared between WASM modules.
type ApiState = {
	-- This module provides the low level FFI that the API functions use.
	module: WasmModule,
	log_fn: LogFn
}

-- The public API surface exposed by Autput after initialisation.
-- The API state is expected to be encapsulated as upvalues in closures.
export type Api = {
	-- Change where Rust logs are sent to, returning the old handler. This does
	-- not affect panic handling.
	replace_log_fn: (
		new_log_fn: LogFn
	) -> LogFn,
}

export type LogLevel = "error" | "warn" | "info" | "debug" | "trace"
export type LogFn = (
	log_level: LogLevel,
	message: string
) -> ()

--------------------------------------------------------------------------------
-- CONSTANTS -------------------------------------------------------------------
--------------------------------------------------------------------------------

local LOG_LEVELS: {LogLevel} = {
	"error",
	"warn",
	"info",
	"debug",
	"trace"
}

local STD_LOG: LogFn =
	if typeof(warn) == "function" then
		function(log_level, message)
			if log_level == "error" or log_level == "warn" then
				warn(message)
			else
				print(message)
			end
		end
	else
		function(_, message)
			print(message)
		end

--------------------------------------------------------------------------------
-- FOREIGN FUNCTION INTERFACE --------------------------------------------------
--------------------------------------------------------------------------------

local extern_fn = {}

function extern_fn.log(
	api_state: ApiState,
	level_ord: u8,
	ptr: ptr_const_u8,
	len: u32
): ()
	local message = api_state.module.rt.load.string(
		api_state.module.memory_list.memory,
		ptr,
		len
	)
	local log_level = LOG_LEVELS[level_ord] or error(
		`[autput] unknown log level{level_ord} for: {message}`, 0
	)
	api_state.log_fn(log_level :: LogLevel, message)
end

function extern_fn.panic(
	api_state: ApiState,
	ptr: ptr_const_u8,
	len: u32
): ()
	local message = api_state.module.rt.load.string(
		api_state.module.memory_list.memory,
		ptr,
		len
	)
	error(message, 0)
end

--------------------------------------------------------------------------------
-- LUAU SIDE API ---------------------------------------------------------------
--------------------------------------------------------------------------------

local api_fn = {}

-- See the `Api` type for user-facing documentation.
function api_fn.replace_log_fn(
	api_state: ApiState,
	new_log_fn: LogFn
): LogFn
	local old_log_fn = api_state.log_fn
	api_state.log_fn = new_log_fn
	return old_log_fn
end

--------------------------------------------------------------------------------
-- INITIALISATION --------------------------------------------------------------
--------------------------------------------------------------------------------

local autput = {}

-- Autput needs to register its own extern functions into the WASM environment,
-- and it needs to obtain a reference to the WASM module so that it can invoke
-- functions that exist in the WASM runtime. Once both have been received, you
-- will receive an `Api` that you can use with the provided module.
function autput.init(
	env: WasmEnvironment
)
	local api_state: ApiState? = nil
	for fn_name, fn in pairs(extern_fn) do
		env.func_list["autput_"..fn_name] = function(...)
			if api_state == nil then
				print("[autput] api not initialised yet on luau side - cannot handle incoming data")
				return
			end
			return fn(api_state, ...)
		end
	end
	return function(
		module: WasmModule
	): Api
		assert(api_state == nil, "[autput] only one module allowed per environment")
		api_state = {
			module = module,
			log_fn = STD_LOG
		}
		local api = {}
		for fn_name, fn in pairs(api_fn) do
			api[fn_name] = function(...)
				return fn(api_state, ...)
			end
		end
		return api :: any
	end
end

return autput


local ProcessService = game:GetService("ProcessService")
local Root = script.Parent.ZenObservableTestModel

local Packages = Root.Packages
-- Load JestGlobals source into Packages folder so it's next to Roact as expected
local JestGlobals = require(Root.Packages.Dev.JestGlobals)
local TestEZ = JestGlobals.TestEZ

-- Run all tests, collect results, and report to stdout.
local result = TestEZ.TestBootstrap:run(
	{ Packages.ZenObservable, Packages.Tests },
	TestEZ.Reporters.TextReporterQuiet
)

if result.failureCount == 0 and #result.errors == 0 then
	ProcessService:ExitAsync(0)
else
	ProcessService:ExitAsync(1)
end


--!strict
local exports = {}
local ObservableModule = require(script.Observable)
local Observable = ObservableModule.Observable
exports.Observable = Observable

export type SubscriptionObserver<T> = ObservableModule.SubscriptionObserver<T>
export type Observer<T> = ObservableModule.Observer<T>
export type Subscriber<T> = ObservableModule.Subscriber<T>
export type Subscription<T> = ObservableModule.Subscription<T>
export type Observable<T> = ObservableModule.Observable<T>

return exports


--!strict
--!optimize 2
--!native

local raylib = require("@raylib")

local lib = raylib.lib;
local const = raylib.const;
local structs = raylib.structs;

local screenWidth = 800;
local screenHeight = 450;

lib.InitWindow(screenWidth, screenHeight, "ZUNE + Raylib");

lib.SetTargetFPS(240);

local pos: raylib.Vector2 = vector.zero;

local bgColor = structs.Color:new({ r = 16, g = 16, b = 16, a = 255 });

while lib.WindowShouldClose() == 0 do
    lib.BeginDrawing();

    lib.ClearBackground(bgColor);

    pos = lib.GetMousePosition();

    lib.DrawRectangleRounded(structs.Rectangle:new{ x = 350, y = 100, width = 150, height = 100 }, 0.2, 10, const.RED);
    lib.DrawRectangleRoundedLinesEx(structs.Rectangle:new{ x = 350, y = 100, width = 150, height = 100 }, 0.2, 10, 4, const.BLACK);
    lib.DrawCircleV(pos, 40, const.BLUE);

    lib.DrawFPS(10, 10);

    lib.DrawText("ZUNE RAYLIB BINDINGS", 240, 400, 40, const.BLACK);

    lib.EndDrawing();
end

lib.CloseWindow();
zune.process.exit(0);


--!strict

local native = require("@self/native");
local raylib = require("@self/native/raylib");

export type Vector2 = raylib.Vector2
export type Vector3 = raylib.Vector3
export type Vector4 = raylib.Vector4
export type Quaternion = raylib.Quaternion
export type Matrix = raylib.Matrix
export type Color = raylib.Color
export type Rectangle = raylib.Rectangle
export type Image = raylib.Image
export type Texture = raylib.Texture
export type Texture2D = raylib.Texture2D
export type TextureCubemap = raylib.TextureCubemap
export type RenderTexture = raylib.RenderTexture
export type RenderTexture2D = raylib.RenderTexture2D
export type NPatchInfo = raylib.NPatchInfo
export type GlyphInfo = raylib.GlyphInfo
export type Font = raylib.Font
export type Camera3D = raylib.Camera3D
export type Camera2D = raylib.Camera2D
export type Mesh = raylib.Mesh
export type Shader = raylib.Shader
export type MaterialMap = raylib.MaterialMap
export type Material = raylib.Material
export type Transform = raylib.Transform
export type BoneInfo = raylib.BoneInfo
export type Model = raylib.Model
export type ModelAnimation = raylib.ModelAnimation
export type Ray = raylib.Ray
export type RayCollision = raylib.RayCollision
export type BoundingBox = raylib.BoundingBox
export type Wave = raylib.Wave
export type rAudioBuffer = raylib.rAudioBuffer
export type rAudioProcessor = raylib.rAudioProcessor
export type AudioStream = raylib.AudioStream
export type Sound = raylib.Sound
export type Music = raylib.Music
export type VrDeviceInfo = raylib.VrDeviceInfo
export type VrStereoConfig = raylib.VrStereoConfig
export type FilePathList = raylib.FilePathList
export type AutomationEvent = raylib.AutomationEvent
export type AutomationEventList = raylib.AutomationEventList

local lib = native.lib;
local const = native.const;
local structs = const.structs;

return {
    lib = lib,
    const = const,
    structs = structs,
}


--!strict
--!optimize 2
--!native

local tar = {};

local function spanstring(buf: buffer, offset: number, max: number): string
    return buffer.readstring(
        buf,
        offset,
        math.min(
            (zune.mem.findScalarPos(buf, offset, 0) or offset + max) - offset,
            max
        )
    );
end

function tar.read(buf: buffer): { [string]: buffer }
    local len: number = buffer.len(buf);
    local offset: number = 0;
    local files: { [string]: buffer } = {};
    local links: { [string]: string } = {};

    while (offset + 512 <= len) do
        local name = spanstring(buf, offset, 100);
        if (#name == 0) then
            break;
        end

        local flag = buffer.readu8(buf, offset + 156);
        local size_str = spanstring(buf, offset + 124, 12);
        local size = tonumber(size_str, 8) or 0;

        if (flag == 50) then -- symlink
            local linkname = spanstring(buf, offset + 157, 100);
            links[name] = linkname;
            offset += 512;
            continue;
        elseif (flag == 0 or flag == 48) then -- file
            local data_offset = offset + 512
            if (data_offset + size > len) then
                break;
            end
            local data = buffer.create(size);
            buffer.copy(data, 0, buf, data_offset, size);
            files[name] = data;
        end
        -- unknown
        local padded = math.ceil(size / 512) * 512;
        offset += 512 + padded;
    end

    for link, target in links do
        local parent = zune.fs.path.dirname(link) or "";
        local target_path = zune.fs.path.resolve(parent, target);
        if (files[target_path]) then
            files[link] = files[target_path];
        end
    end

    return files;
end

return tar;


--!strict
--!optimize 2
--!native

local mem = zune.mem;
local flate = zune.serde.flate;

local zip = {};

local EOCD_SIGNATURE = buffer.fromstring("\x50\x4b\x05\x06");

function zip.is(buf: buffer): boolean
    local len = buffer.len(buf);
    if len < 22 then
        return false;
    end
    return mem.findPos(buf, len - 22, EOCD_SIGNATURE) ~= nil
end

function zip.read(buf: buffer): { [string]: buffer }
    local len = buffer.len(buf);
    local files: { [string]: buffer } = {};

    local eocd_offset: number? = mem.findPos(buf, len - 22, EOCD_SIGNATURE);
    if (not eocd_offset) then
        error("Invalid ZIP file", 0);
    end

    local central_dir_offset = buffer.readu32(buf, eocd_offset + 16);
    local central_dir_size = buffer.readu32(buf, eocd_offset + 12);

    local offset = central_dir_offset;
    while (offset < central_dir_offset + central_dir_size) do
        if (buffer.readu32(buf, offset) ~= 0x02014b50) then
            break;
        end

        local compression = buffer.readu16(buf, offset + 10);
        local compressed_size = buffer.readu32(buf, offset + 20);
        local name_len = buffer.readu16(buf, offset + 28);
        local extra_len = buffer.readu16(buf, offset + 30);
        local comment_len = buffer.readu16(buf, offset + 32);
        local local_header_offset = buffer.readu32(buf, offset + 42);

        local name = buffer.readstring(buf, offset + 46, name_len);

        local local_extra_len = buffer.readu16(buf, local_header_offset + 28);
        local data_offset = local_header_offset + 30 + name_len + local_extra_len;

        if (data_offset + compressed_size > len) then
            break;
        end

        local compressed_data = buffer.create(compressed_size);
        buffer.copy(compressed_data, 0, buf, data_offset, compressed_size);

        local decompressed: buffer;
        if (compression == 8) then
            decompressed = flate.decompress(compressed_data);
        elseif (compression == 0) then
            decompressed = compressed_data;
        else
            decompressed = buffer.create(0);
        end

        files[name] = decompressed;

        offset = offset + 46 + name_len + extra_len + comment_len;
    end

    return files;
end

return zip;


--!strict
--!optimize 2
--!native

local ffi = zune.ffi;

local raylib = require("./raylib");

local structs = raylib.structs;

local void = ffi.types.void;
local int = ffi.types.i32;
local uint = ffi.types.u32;
local bool = ffi.types.u8;
local float = ffi.types.float;
local cstring = ffi.types.pointer;
type int = number;
type uint = number;
type bool = number;
type float = number;
type cstring = string | buffer | FFIPointer;

local Wave = structs.Wave;
local Sound = structs.Sound;
local Music = structs.Music;
local AudioStream = structs.AudioStream;
type Wave = raylib.Wave;
type Sound = raylib.Sound;
type Music = raylib.Music;
type AudioStream = raylib.AudioStream;

local function fn(returns: any, args: { any })
    return {
        returns = returns,
        args = args,
    };
end

export type Fns = {
    InitAudioDevice: () -> (),
    CloseAudioDevice: () -> (),
    IsAudioDeviceReady: () -> bool,
    SetMasterVolume: (volume: float) -> (),
    GetMasterVolume: () -> float,
    LoadWave: (fileName: cstring) -> Wave,
    LoadWaveFromMemory: (fileType: cstring, fileData: FFIPointer, dataSize: int) -> Wave,
    IsWaveValid: (wave: Wave) -> bool,
    LoadSound: (fileName: cstring) -> Sound,
    LoadSoundFromWave: (wave: Wave) -> Sound,
    LoadSoundAlias: (source: Sound) -> Sound,
    IsSoundValid: (sound: Sound) -> bool,
    UpdateSound: (sound: Sound, data: FFIPointer, sampleCount: int) -> (),
    UnloadWave: (wave: Wave) -> (),
    UnloadSound: (sound: Sound) -> (),
    UnloadSoundAlias: (alias: Sound) -> (),
    ExportWave: (wave: Wave, fileName: cstring) -> bool,
    ExportWaveAsCode: (wave: Wave, fileName: cstring) -> bool,
    PlaySound: (sound: Sound) -> (),
    StopSound: (sound: Sound) -> (),
    PauseSound: (sound: Sound) -> (),
    ResumeSound: (sound: Sound) -> (),
    IsSoundPlaying: (sound: Sound) -> bool,
    SetSoundVolume: (sound: Sound, volume: float) -> (),
    SetSoundPitch: (sound: Sound, pitch: float) -> (),
    SetSoundPan: (sound: Sound, pan: float) -> (),
    WaveCopy: (wave: Wave) -> Wave,
    WaveCrop: (wave: FFIPointer, initFrame: int, finalFrame: int) -> (),
    WaveFormat: (wave: FFIPointer, sampleRate: int, sampleSize: int, channels: int) -> (),
    LoadWaveSamples: (wave: Wave) -> FFIPointer,
    UnloadWaveSamples: (samples: FFIPointer) -> (),
    LoadMusicStream: (fileName: cstring) -> Music,
    LoadMusicStreamFromMemory: (fileType: cstring, data: FFIPointer, dataSize: int) -> Music,
    IsMusicValid: (music: Music) -> bool,
    UnloadMusicStream: (music: Music) -> (),
    PlayMusicStream: (music: Music) -> (),
    IsMusicStreamPlaying: (music: Music) -> bool,
    UpdateMusicStream: (music: Music) -> (),
    StopMusicStream: (music: Music) -> (),
    PauseMusicStream: (music: Music) -> (),
    ResumeMusicStream: (music: Music) -> (),
    SeekMusicStream: (music: Music, position: float) -> (),
    SetMusicVolume: (music: Music, volume: float) -> (),
    SetMusicPitch: (music: Music, pitch: float) -> (),
    SetMusicPan: (music: Music, pan: float) -> (),
    GetMusicTimeLength: (music: Music) -> float,
    GetMusicTimePlayed: (music: Music) -> float,
    LoadAudioStream: (sampleRate: uint, sampleSize: uint, channels: uint) -> AudioStream,
    IsAudioStreamValid: (stream: AudioStream) -> bool,
    UnloadAudioStream: (stream: AudioStream) -> (),
    UpdateAudioStream: (stream: AudioStream, data: FFIPointer, frameCount: int) -> (),
    IsAudioStreamProcessed: (stream: AudioStream) -> bool,
    PlayAudioStream: (stream: AudioStream) -> (),
    PauseAudioStream: (stream: AudioStream) -> (),
    ResumeAudioStream: (stream: AudioStream) -> (),
    IsAudioStreamPlaying: (stream: AudioStream) -> bool,
    StopAudioStream: (stream: AudioStream) -> (),
    SetAudioStreamVolume: (stream: AudioStream, volume: float) -> (),
    SetAudioStreamPitch: (stream: AudioStream, pitch: float) -> (),
    SetAudioStreamPan: (stream: AudioStream, pan: float) -> (),
    SetAudioStreamBufferSizeDefault: (size: int) -> (),
    SetAudioStreamCallback: (stream: AudioStream, callback: FFIPointer) -> (),
    AttachAudioStreamProcessor: (stream: AudioStream, processor: FFIPointer) -> (),
    DetachAudioStreamProcessor: (stream: AudioStream, processor: FFIPointer) -> (),
    AttachAudioMixedProcessor: (processor: FFIPointer) -> (),
    DetachAudioMixedProcessor: (processor: FFIPointer) -> (),
};

return {
    def = {
        -- void InitAudioDevice(void);
        InitAudioDevice = fn(void, {}),
        -- void CloseAudioDevice(void);
        CloseAudioDevice = fn(void, {}),
        -- bool IsAudioDeviceReady(void);
        IsAudioDeviceReady = fn(bool, {}),
        -- void SetMasterVolume(float volume);
        SetMasterVolume = fn(void, { float }),
        -- float GetMasterVolume(void);
        GetMasterVolume = fn(float, {}),
        -- Wave LoadWave(const char *fileName);
        LoadWave = fn(Wave, { cstring }),
        -- Wave LoadWaveFromMemory(const char *fileType, const unsigned char *fileData, int dataSize);
        LoadWaveFromMemory = fn(Wave, { cstring, ffi.types.pointer, int }),
        -- bool IsWaveValid(Wave wave);
        IsWaveValid = fn(bool, { Wave }),
        -- Sound LoadSound(const char *fileName);
        LoadSound = fn(Sound, { cstring }),
        -- Sound LoadSoundFromWave(Wave wave);
        LoadSoundFromWave = fn(Sound, { Wave }),
        -- Sound LoadSoundAlias(Sound source);
        LoadSoundAlias = fn(Sound, { Sound }),
        -- bool IsSoundValid(Sound sound);
        IsSoundValid = fn(bool, { Sound }),
        -- void UpdateSound(Sound sound, const void *data, int sampleCount);
        UpdateSound = fn(void, { Sound, ffi.types.pointer, int }),
        -- void UnloadWave(Wave wave);
        UnloadWave = fn(void, { Wave }),
        -- void UnloadSound(Sound sound);
        UnloadSound = fn(void, { Sound }),
        -- void UnloadSoundAlias(Sound alias);
        UnloadSoundAlias = fn(void, { Sound }),
        -- bool ExportWave(Wave wave, const char *fileName);
        ExportWave = fn(bool, { Wave, cstring }),
        -- bool ExportWaveAsCode(Wave wave, const char *fileName);
        ExportWaveAsCode = fn(bool, { Wave, cstring }),
        -- void PlaySound(Sound sound);
        PlaySound = fn(void, { Sound }),
        -- void StopSound(Sound sound);
        StopSound = fn(void, { Sound }),
        -- void PauseSound(Sound sound);
        PauseSound = fn(void, { Sound }),
        -- void ResumeSound(Sound sound);
        ResumeSound = fn(void, { Sound }),
        -- bool IsSoundPlaying(Sound sound);
        IsSoundPlaying = fn(bool, { Sound }),
        -- void SetSoundVolume(Sound sound, float volume);
        SetSoundVolume = fn(void, { Sound, float }),
        -- void SetSoundPitch(Sound sound, float pitch);
        SetSoundPitch = fn(void, { Sound, float }),
        -- void SetSoundPan(Sound sound, float pan);
        SetSoundPan = fn(void, { Sound, float }),
        -- Wave WaveCopy(Wave wave);
        WaveCopy = fn(Wave, { Wave }),
        -- void WaveCrop(Wave *wave, int initFrame, int finalFrame);
        WaveCrop = fn(void, { ffi.types.pointer, int, int }),
        -- void WaveFormat(Wave *wave, int sampleRate, int sampleSize, int channels);
        WaveFormat = fn(void, { ffi.types.pointer, int, int, int }),
        -- float *LoadWaveSamples(Wave wave);
        LoadWaveSamples = fn(ffi.types.pointer, { Wave }),
        -- void UnloadWaveSamples(float *samples);
        UnloadWaveSamples = fn(void, { ffi.types.pointer }),
        -- Music LoadMusicStream(const char *fileName);
        LoadMusicStream = fn(Music, { cstring }),
        -- Music LoadMusicStreamFromMemory(const char *fileType, const unsigned char *data, int dataSize);
        LoadMusicStreamFromMemory = fn(Music, { cstring, ffi.types.pointer, int }),
        -- bool IsMusicValid(Music music);
        IsMusicValid = fn(bool, { Music }),
        -- void UnloadMusicStream(Music music);
        UnloadMusicStream = fn(void, { Music }),
        -- void PlayMusicStream(Music music);
        PlayMusicStream = fn(void, { Music }),
        -- bool IsMusicStreamPlaying(Music music);
        IsMusicStreamPlaying = fn(bool, { Music }),
        -- void UpdateMusicStream(Music music);
        UpdateMusicStream = fn(void, { Music }),
        -- void StopMusicStream(Music music);
        StopMusicStream = fn(void, { Music }),
        -- void PauseMusicStream(Music music);
        PauseMusicStream = fn(void, { Music }),
        -- void ResumeMusicStream(Music music);
        ResumeMusicStream = fn(void, { Music }),
        -- void SeekMusicStream(Music music, float position);
        SeekMusicStream = fn(void, { Music, float }),
        -- void SetMusicVolume(Music music, float volume);
        SetMusicVolume = fn(void, { Music, float }),
        -- void SetMusicPitch(Music music, float pitch);
        SetMusicPitch = fn(void, { Music, float }),
        -- void SetMusicPan(Music music, float pan);
        SetMusicPan = fn(void, { Music, float }),
        -- float GetMusicTimeLength(Music music);
        GetMusicTimeLength = fn(float, { Music }),
        -- float GetMusicTimePlayed(Music music);
        GetMusicTimePlayed = fn(float, { Music }),
        -- AudioStream LoadAudioStream(unsigned int sampleRate, unsigned int sampleSize, unsigned int channels);
        LoadAudioStream = fn(AudioStream, { uint, uint, uint }),
        -- bool IsAudioStreamValid(AudioStream stream);
        IsAudioStreamValid = fn(bool, { AudioStream }),
        -- void UnloadAudioStream(AudioStream stream);
        UnloadAudioStream = fn(void, { AudioStream }),
        -- void UpdateAudioStream(AudioStream stream, const void *data, int frameCount);
        UpdateAudioStream = fn(void, { AudioStream, ffi.types.pointer, int }),
        -- bool IsAudioStreamProcessed(AudioStream stream);
        IsAudioStreamProcessed = fn(bool, { AudioStream }),
        -- void PlayAudioStream(AudioStream stream);
        PlayAudioStream = fn(void, { AudioStream }),
        -- void PauseAudioStream(AudioStream stream);
        PauseAudioStream = fn(void, { AudioStream }),
        -- void ResumeAudioStream(AudioStream stream);
        ResumeAudioStream = fn(void, { AudioStream }),
        -- bool IsAudioStreamPlaying(AudioStream stream);
        IsAudioStreamPlaying = fn(bool, { AudioStream }),
        -- void StopAudioStream(AudioStream stream);
        StopAudioStream = fn(void, { AudioStream }),
        -- void SetAudioStreamVolume(AudioStream stream, float volume);
        SetAudioStreamVolume = fn(void, { AudioStream, float }),
        -- void SetAudioStreamPitch(AudioStream stream, float pitch);
        SetAudioStreamPitch = fn(void, { AudioStream, float }),
        -- void SetAudioStreamPan(AudioStream stream, float pan);
        SetAudioStreamPan = fn(void, { AudioStream, float }),
        -- void SetAudioStreamBufferSizeDefault(int size);
        SetAudioStreamBufferSizeDefault = fn(void, { int }),
        -- void SetAudioStreamCallback(AudioStream stream, AudioCallback callback);
        SetAudioStreamCallback = fn(void, { AudioStream, ffi.types.pointer }),
        -- void AttachAudioStreamProcessor(AudioStream stream, AudioCallback processor);
        AttachAudioStreamProcessor = fn(void, { AudioStream, ffi.types.pointer }),
        -- void DetachAudioStreamProcessor(AudioStream stream, AudioCallback processor);
        DetachAudioStreamProcessor = fn(void, { AudioStream, ffi.types.pointer }),
        -- void AttachAudioMixedProcessor(AudioCallback processor);
        AttachAudioMixedProcessor = fn(void, { ffi.types.pointer }),
        -- void DetachAudioMixedProcessor(AudioCallback processor);
        DetachAudioMixedProcessor = fn(void, { ffi.types.pointer }),
    }
}


--!strict
--!optimize 2
--!native

local raylib = require("./raylib");

local structs = raylib.structs;

local ffi = zune.ffi;

local void = ffi.types.void;
local int = ffi.types.i32;
local uint = ffi.types.u32;
local bool = ffi.types.u8;
local float = ffi.types.float;
local cstring = ffi.types.pointer;
local long = ffi.types.i64;
type int = number;
type uint = number;
type bool = number;
type float = number;
type long = buffer;
type double = number;
type cstring = string | buffer | FFIPointer;

local Vector2 = structs.Vector2;
local Vector3 = structs.Vector3;
local Matrix = structs.Matrix;
local Color = structs.Color;
local Image = structs.Image;
local Texture = structs.Texture;
local RenderTexture2D = structs.RenderTexture2D;
local Shader = structs.Shader;
local Camera3D = structs.Camera3D;
local Camera2D = structs.Camera2D;
local Ray = structs.Ray;
local VrStereoConfig = structs.VrStereoConfig;
local VrDeviceInfo = structs.VrDeviceInfo;
local FilePathList = structs.FilePathList;
local AutomationEvent = structs.AutomationEvent;
local AutomationEventList = structs.AutomationEventList;
type Vector2 = raylib.Vector2;
type Vector3 = raylib.Vector3;
type Matrix = raylib.Matrix;
type Color = raylib.Color;
type Image = raylib.Image;
type Texture = raylib.Texture;
type RenderTexture2D = raylib.RenderTexture2D;
type Shader = raylib.Shader;
type Camera3D = raylib.Camera3D;
type Camera2D = raylib.Camera2D;
type Ray = raylib.Ray;
type VrStereoConfig = raylib.VrStereoConfig;
type VrDeviceInfo = raylib.VrDeviceInfo;
type FilePathList = raylib.FilePathList;
type AutomationEvent = raylib.AutomationEvent;
type AutomationEventList = raylib.AutomationEventList;

local function fn(returns: any, args: { any })
    return {
        returns = returns,
        args = args,
    };
end

export type Fns = {
    InitWindow: (width: int, height: int, title: cstring) -> (),
    CloseWindow: () -> (),
    WindowShouldClose: () -> bool,
    IsWindowReady: () -> bool,
    IsWindowFullscreen: () -> bool,
    IsWindowHidden: () -> bool,
    IsWindowMinimized: () -> bool,
    IsWindowMaximized: () -> bool,
    IsWindowFocused: () -> bool,
    IsWindowResized: () -> bool,
    IsWindowState: (flag: int) -> bool,
    SetWindowState: (flags: int) -> (),
    ClearWindowState: (flags: int) -> (),
    ToggleFullscreen: () -> (),
    ToggleBorderlessWindowed: () -> (),
    MaximizeWindow: () -> (),
    MinimizeWindow: () -> (),
    RestoreWindow: () -> (),
    SetWindowIcon: (image: Image) -> (),
    SetWindowIcons: (images: FFIPointer, count: int) -> (),
    SetWindowTitle: (title: cstring) -> (),
    SetWindowPosition: (x: int, y: int) -> (),
    SetWindowMonitor: (monitor: int) -> (),
    SetWindowMinSize: (width: int, height: int) -> (),
    SetWindowMaxSize: (width: int, height: int) -> (),
    SetWindowSize: (width: int, height: int) -> (),
    SetWindowOpacity: (opacity: float) -> (),
    SetWindowFocused: () -> (),
    GetWindowHandle: () -> FFIPointer,
    GetScreenWidth: () -> int,
    GetScreenHeight: () -> int,
    GetRenderWidth: () -> int,
    GetRenderHeight: () -> int,
    GetMonitorCount: () -> int,
    GetCurrentMonitor: () -> int,
    GetMonitorPosition: (monitor: int) -> Vector2,
    GetMonitorWidth: (monitor: int) -> int,
    GetMonitorHeight: (monitor: int) -> int,
    GetMonitorPhysicalWidth: (monitor: int) -> int,
    GetMonitorPhysicalHeight: (monitor: int) -> int,
    GetMonitorRefreshRate: (monitor: int) -> int,
    GetWindowPosition: () -> Vector2,
    GetWindowScaleDPI: () -> Vector2,
    GetMonitorName: (monitor: int) -> FFIPointer,
    SetClipboardText: (text: cstring) -> (),
    GetClipboardText: () -> FFIPointer,
    GetClipboardImage: () -> Image,
    EnableEventWaiting: () -> (),
    DisableEventWaiting: () -> (),
    ShowCursor: () -> (),
    HideCursor: () -> (),
    EnableCursor: () -> (),
    DisableCursor: () -> (),
    IsCursorHidden: () -> bool,
    IsCursorOnScreen: () -> bool,
    ClearBackground: (color: Color) -> (),
    BeginDrawing: () -> (),
    EndDrawing: () -> (),
    BeginMode2D: (camera: Camera2D) -> (),
    EndMode2D: () -> (),
    BeginMode3D: (camera: Camera3D) -> (),
    EndMode3D: () -> (),
    BeginTextureMode: (target: RenderTexture2D) -> (),
    EndTextureMode: () -> (),
    BeginShaderMode: (shader: Shader) -> (),
    EndShaderMode: () -> (),
    BeginBlendMode: (mode: int) -> (),
    EndBlendMode: () -> (),
    BeginScissorMode: (x: int, y: int, width: int, height: int) -> (),
    EndScissorMode: () -> (),
    BeginVrStereoMode: (config: VrStereoConfig) -> (),
    EndVrStereoMode: () -> (),
    LoadVrStereoConfig: (device: VrDeviceInfo) -> VrStereoConfig,
    UnloadVrStereoConfig: (config: VrStereoConfig) -> (),
    LoadShader: (vsFileName: cstring, fsFileName: cstring) -> Shader,
    LoadShaderFromMemory: (vsCode: cstring, fsCode: cstring) -> Shader,
    IsShaderValid: (shader: Shader) -> bool,
    GetShaderLocation: (shader: Shader, uniformName: cstring) -> int,
    GetShaderLocationAttrib: (shader: Shader, attribName: cstring) -> int,
    SetShaderValue: (shader: Shader, locIndex: int, value: FFIPointer, uniformType: int) -> (),
    SetShaderValueV: (shader: Shader, locIndex: int, value: FFIPointer, uniformType: int, count: int) -> (),
    SetShaderValueMatrix: (shader: Shader, locIndex: int, mat: Matrix) -> (),
    SetShaderValueTexture: (shader: Shader, locIndex: int, texture: Texture) -> (),
    UnloadShader: (shader: Shader) -> (),
    GetScreenToWorldRay: (position: Vector2, camera: Camera3D) -> Ray,
    GetScreenToWorldRayEx: (position: Vector2, camera: Camera3D, width: int, height: int) -> Ray,
    GetWorldToScreen: (position: Vector3, camera: Camera3D) -> Vector2,
    GetWorldToScreenEx: (position: Vector3, camera: Camera3D, width: int, height: int) -> Vector2,
    GetWorldToScreen2D: (position: Vector2, camera: Camera2D) -> Vector2,
    GetScreenToWorld2D: (position: Vector2, camera: Camera2D) -> Vector2,
    GetCameraMatrix: (camera: Camera3D) -> Matrix,
    GetCameraMatrix2D: (camera: Camera2D) -> Matrix,
    SetTargetFPS: (fps: int) -> (),
    GetFrameTime: () -> float,
    GetTime: () -> double,
    GetFPS: () -> int,
    SwapScreenBuffer: () -> (),
    PollInputEvents: () -> (),
    WaitTime: (seconds: double) -> (),
    SetRandomSeed: (seed: uint) -> (),
    GetRandomValue: (min: int, max: int) -> int,
    LoadRandomSequence: (count: uint, min: int, max: int) -> FFIPointer,
    UnloadRandomSequence: (sequence: FFIPointer) -> (),
    TakeScreenshot: (fileName: cstring) -> (),
    SetConfigFlags: (flags: uint) -> (),
    OpenURL: (url: cstring) -> (),
    TraceLog: (logLevel: int, text: cstring) -> (),
    SetTraceLogLevel: (logLevel: int) -> (),
    MemAlloc: (size: uint) -> FFIPointer,
    MemRealloc: (ptr: FFIPointer, size: uint) -> FFIPointer,
    MemFree: (ptr: FFIPointer) -> (),
    SetTraceLogCallback: (callback: FFIPointer) -> (),
    SetLoadFileDataCallback: (callback: FFIPointer) -> (),
    SetSaveFileDataCallback: (callback: FFIPointer) -> (),
    SetLoadFileTextCallback: (callback: FFIPointer) -> (),
    SetSaveFileTextCallback: (callback: FFIPointer) -> (),
    LoadFileData: (fileName: cstring, dataSize: FFIPointer) -> FFIPointer,
    UnloadFileData: (data: FFIPointer) -> (),
    SaveFileData: (fileName: cstring, data: FFIPointer, dataSize: int) -> bool,
    ExportDataAsCode: (data: FFIPointer, dataSize: int, fileName: cstring) -> bool,
    LoadFileText: (fileName: cstring) -> FFIPointer,
    UnloadFileText: (text: cstring) -> (),
    SaveFileText: (fileName: cstring, text: cstring) -> bool,
    FileExists: (fileName: cstring) -> bool,
    DirectoryExists: (dirPath: cstring) -> bool,
    IsFileExtension: (fileName: cstring, ext: cstring) -> bool,
    GetFileLength: (fileName: cstring) -> int,
    GetFileExtension: (fileName: cstring) -> FFIPointer,
    GetFileName: (fileName: cstring) -> FFIPointer,
    GetFileNameWithoutExt: (fileName: cstring) -> FFIPointer,
    GetDirectoryPath: (fileName: cstring) -> FFIPointer,
    GetWorkingDirectory: () -> FFIPointer,
    GetApplicationDirectory: () -> FFIPointer,
    ChangeDirectory: (dir: cstring) -> bool,
    IsPathFile: (path: cstring) -> bool,
    LoadDirectoryFiles: (dirPath: cstring) -> FilePathList,
    UnloadDirectoryFiles: (filePathList: FilePathList) -> (),
    GetFileModTime: (fileName: cstring) -> long,
    CompressData: (data: FFIPointer, dataSize: int, compDataSize: FFIPointer) -> FFIPointer,
    DecompressData: (compData: FFIPointer, compDataSize: int, dataSize: FFIPointer) -> FFIPointer,
    EncodeDataBase64: (data: FFIPointer, dataSize: int, outputSize: FFIPointer) -> FFIPointer,
    DecodeDataBase64: (data: cstring, outputSize: FFIPointer) -> FFIPointer,
    IsKeyPressed: (key: int) -> bool,
    IsKeyDown: (key: int) -> bool,
    IsKeyReleased: (key: int) -> bool,
    IsKeyUp: (key: int) -> bool,
    GetKeyPressed: () -> int,
    GetCharPressed: () -> int,
    SetExitKey: (key: int) -> (),
    IsGamepadAvailable: (gamepad: int) -> bool,
    GetGamepadName: (gamepad: int) -> FFIPointer,
    IsGamepadButtonPressed: (gamepad: int, button: int) -> bool,
    IsGamepadButtonDown: (gamepad: int, button: int) -> bool,
    IsGamepadButtonReleased: (gamepad: int, button: int) -> bool,
    IsGamepadButtonUp: (gamepad: int, button: int) -> bool,
    GetGamepadButtonPressed: () -> int,
    GetGamepadAxisCount: (gamepad: int) -> int,
    GetGamepadAxisMovement: (gamepad: int, axis: int) -> float,
    SetGamepadMappings: (mappings: cstring) -> int,
    SetGamepadVibration: (gamepad: int, leftMotor: float, rightMotor: float, duration: float) -> (),
    IsMouseButtonPressed: (button: int) -> bool,
    IsMouseButtonDown: (button: int) -> bool,
    IsMouseButtonReleased: (button: int) -> bool,
    IsMouseButtonUp: (button: int) -> bool,
    GetMouseX: () -> int,
    GetMouseY: () -> int,
    GetMousePosition: () -> Vector2,
    GetMouseDelta: () -> Vector2,
    SetMousePosition: (x: int, y: int) -> (),
    SetMouseOffset: (offsetX: int, offsetY: int) -> (),
    SetMouseScale: (scaleX: float, scaleY: float) -> (),
    GetMouseWheelMove: () -> float,
    GetMouseWheelMoveV: () -> Vector2,
    SetMouseCursor: (cursor: int) -> (),
    GetTouchX: () -> int,
    GetTouchY: () -> int,
    GetTouchPosition: (index: int) -> Vector2,
    GetTouchPointCount: () -> int,
    SetGesturesEnabled: (flags: uint) -> (),
    IsGestureDetected: (gesture: uint) -> bool,
    GetGestureDetected: () -> int,
    GetGestureHoldDuration: () -> float,
    GetGestureDragVector: () -> Vector2,
    GetGestureDragAngle: () -> float,
    GetGesturePinchVector: () -> Vector2,
    GetGesturePinchAngle: () -> float,
    UpdateCamera: (camera: FFIPointer, mode: int) -> (),
    UpdateCameraPro: (camera: FFIPointer, movement: Vector3, rotation: Vector3, zoom: float) -> (),
};

return {
    def = {
        -- void InitWindow(int width, int height, const char *title);
        InitWindow = fn(void, { int, int, cstring }),
        -- void CloseWindow(void);
        CloseWindow = fn(void, {}),
        -- bool WindowShouldClose(void);
        WindowShouldClose = fn(bool, {}),
        -- bool IsWindowReady(void);
        IsWindowReady = fn(bool, {}),
        -- bool IsWindowFullscreen(void);
        IsWindowFullscreen = fn(bool, {}),
        -- bool IsWindowHidden(void);
        IsWindowHidden = fn(bool, {}),
        -- bool IsWindowMinimized(void);
        IsWindowMinimized = fn(bool, {}),
        -- bool IsWindowMaximized(void);
        IsWindowMaximized = fn(bool, {}),
        -- bool IsWindowFocused(void);
        IsWindowFocused = fn(bool, {}),
        -- bool IsWindowResized(void);
        IsWindowResized = fn(bool, {}),
        -- bool IsWindowState(unsigned int flag);
        IsWindowState = fn(bool, { uint }),
        -- void SetWindowState(unsigned int flags);
        SetWindowState = fn(void, { uint }),
        -- void ClearWindowState(unsigned int flags);
        ClearWindowState = fn(void, { uint }),
        -- void ToggleFullscreen(void);
        ToggleFullscreen = fn(void, {}),
        -- void ToggleBorderlessWindowed(void);
        ToggleBorderlessWindowed = fn(void, {}),
        -- void MaximizeWindow(void);
        MaximizeWindow = fn(void, {}),
        -- void MinimizeWindow(void);
        MinimizeWindow = fn(void, {}),
        -- void RestoreWindow(void);
        RestoreWindow = fn(void, {}),
        -- void SetWindowIcon(Image image);
        SetWindowIcon = fn(void, { Image }),
        -- void SetWindowIcons(Image *images, int count);
        SetWindowIcons = fn(void, { ffi.types.pointer, int }),
        -- void SetWindowTitle(const char *title);
        SetWindowTitle = fn(void, { cstring }),
        -- void SetWindowPosition(int x, int y);
        SetWindowPosition = fn(void, { int, int }),
        -- void SetWindowMonitor(int monitor);
        SetWindowMonitor = fn(void, { int }),
        -- void SetWindowMinSize(int width, int height);
        SetWindowMinSize = fn(void, { int, int }),
        -- void SetWindowMaxSize(int width, int height);
        SetWindowMaxSize = fn(void, { int, int }),
        -- void SetWindowSize(int width, int height);
        SetWindowSize = fn(void, { int, int }),
        -- void SetWindowOpacity(float opacity);
        SetWindowOpacity = fn(void, { float }),
        -- void SetWindowFocused(void);
        SetWindowFocused = fn(void, {}),
        -- void *GetWindowHandle(void);
        GetWindowHandle = fn(ffi.types.pointer, {}),
        -- int GetScreenWidth(void);
        GetScreenWidth = fn(int, {}),
        -- int GetScreenHeight(void);
        GetScreenHeight = fn(int, {}),
        -- int GetRenderWidth(void);
        GetRenderWidth = fn(int, {}),
        -- int GetRenderHeight(void);
        GetRenderHeight = fn(int, {}),
        -- int GetMonitorCount(void);
        GetMonitorCount = fn(int, {}),
        -- int GetCurrentMonitor(void);
        GetCurrentMonitor = fn(int, {}),
        -- Vector2 GetMonitorPosition(int monitor);
        GetMonitorPosition = fn(Vector2, { int }),
        -- int GetMonitorWidth(int monitor);
        GetMonitorWidth = fn(int, { int }),
        -- int GetMonitorHeight(int monitor);
        GetMonitorHeight = fn(int, { int }),
        -- int GetMonitorPhysicalWidth(int monitor);
        GetMonitorPhysicalWidth = fn(int, { int }),
        -- int GetMonitorPhysicalHeight(int monitor);
        GetMonitorPhysicalHeight = fn(int, { int }),
        -- int GetMonitorRefreshRate(int monitor);
        GetMonitorRefreshRate = fn(int, { int }),
        -- Vector2 GetWindowPosition(void);
        GetWindowPosition = fn(Vector2, {}),
        -- Vector2 GetWindowScaleDPI(void);
        GetWindowScaleDPI = fn(Vector2, {}),
        -- const char *GetMonitorName(int monitor);
        GetMonitorName = fn(cstring, { int }),
        -- void SetClipboardText(const char *text);
        SetClipboardText = fn(void, { cstring }),
        -- const char *GetClipboardText(void);
        GetClipboardText = fn(cstring, {}),
        -- Image GetClipboardImage(void);
        GetClipboardImage = fn(Image, {}),
        -- void EnableEventWaiting(void);
        EnableEventWaiting = fn(void, {}),
        -- void DisableEventWaiting(void);
        DisableEventWaiting = fn(void, {}),

        -- void ShowCursor(void);
        ShowCursor = fn(void, {}),
        -- void HideCursor(void);
        HideCursor = fn(void, {}),
        -- void EnableCursor(void);
        EnableCursor = fn(void, {}),
        -- void DisableCursor(void);
        DisableCursor = fn(void, {}),
        -- bool IsCursorHidden(void);
        IsCursorHidden = fn(bool, {}),
        -- bool IsCursorOnScreen(void);
        IsCursorOnScreen = fn(bool, {}),

        -- void ClearBackground(Color color);
        ClearBackground = fn(void, { Color }),
        -- void BeginDrawing(void);
        BeginDrawing = fn(void, {}),
        -- void EndDrawing(void);
        EndDrawing = fn(void, {}),
        -- void BeginMode2D(Camera2D camera);
        BeginMode2D = fn(void, { Camera2D }),
        -- void EndMode2D(void);
        EndMode2D = fn(void, {}),
        -- void BeginMode3D(Camera3D camera);
        BeginMode3D = fn(void, { Camera3D }),
        -- void EndMode3D(void);
        EndMode3D = fn(void, {}),
        -- void BeginTextureMode(RenderTexture2D target);
        BeginTextureMode = fn(void, { RenderTexture2D }),
        -- void EndTextureMode(void);
        EndTextureMode = fn(void, {}),
        -- void BeginShaderMode(Shader shader);
        BeginShaderMode = fn(void, { Shader }),
        -- void EndShaderMode(void);
        EndShaderMode = fn(void, {}),
        -- void BeginBlendMode(int mode);
        BeginBlendMode = fn(void, { int }),
        -- void EndBlendMode(void);
        EndBlendMode = fn(void, {}),
        -- void BeginScissorMode(int x, int y, int width, int height);
        BeginScissorMode = fn(void, { int, int, int, int }),
        -- void EndScissorMode(void);
        EndScissorMode = fn(void, {}),
        -- void BeginVrStereoMode(VrStereoConfig config);
        BeginVrStereoMode = fn(void, { VrStereoConfig }),
        -- void EndVrStereoMode(void);
        EndVrStereoMode = fn(void, {}),

        -- VrStereoConfig LoadVrStereoConfig(VrDeviceInfo device);
        LoadVrStereoConfig = fn(VrStereoConfig, { VrDeviceInfo }),
        -- void UnloadVrStereoConfig(VrStereoConfig config);
        UnloadVrStereoConfig = fn(void, { VrStereoConfig }),

        -- Shader LoadShader(const char *vsFileName, const char *fsFileName);
        LoadShader = fn(Shader, { cstring, cstring }),
        -- Shader LoadShaderFromMemory(const char *vsCode, const char *fsCode);
        LoadShaderFromMemory = fn(Shader, { cstring, cstring }),
        -- bool IsShaderValid(Shader shader);
        IsShaderValid = fn(bool, { Shader }),
        -- int GetShaderLocation(Shader shader, const char *uniformName);
        GetShaderLocation = fn(int, { Shader, cstring }),
        -- int GetShaderLocationAttrib(Shader shader, const char *attribName);
        GetShaderLocationAttrib = fn(int, { Shader, cstring }),
        -- void SetShaderValue(Shader shader, int locIndex, const void *value, int uniformType);
        SetShaderValue = fn(void, { Shader, int, ffi.types.pointer, int }),
        -- void SetShaderValueV(Shader shader, int locIndex, const void *value, int uniformType, int count);
        SetShaderValueV = fn(void, { Shader, int, ffi.types.pointer, int, int }),
        -- void SetShaderValueMatrix(Shader shader, int locIndex, Matrix mat);
        SetShaderValueMatrix = fn(void, { Shader, int, Matrix }),
        -- void SetShaderValueTexture(Shader shader, int locIndex, Texture2D texture);
        SetShaderValueTexture = fn(void, { Shader, int, Texture }),
        -- void UnloadShader(Shader shader);
        UnloadShader = fn(void, { Shader }),

        -- Ray GetScreenToWorldRay(Vector2 position, Camera camera);
        GetScreenToWorldRay = fn(Ray, { Vector2, Camera3D }),
        -- Ray GetScreenToWorldRayEx(Vector2 position, Camera camera, int width, int height);
        GetScreenToWorldRayEx = fn(Ray, { Vector2, Camera3D, int, int }),
        -- Vector2 GetWorldToScreen(Vector3 position, Camera camera);
        GetWorldToScreen = fn(Vector2, { Vector3, Camera3D }),
        -- Vector2 GetWorldToScreenEx(Vector3 position, Camera camera, int width, int height);
        GetWorldToScreenEx = fn(Vector2, { Vector3, Camera3D, int, int }),
        -- Vector2 GetWorldToScreen2D(Vector2 position, Camera2D camera);
        GetWorldToScreen2D = fn(Vector2, { Vector2, Camera2D }),
        -- Vector2 GetScreenToWorld2D(Vector2 position, Camera2D camera);
        GetScreenToWorld2D = fn(Vector2, { Vector2, Camera2D }),
        -- Matrix GetCameraMatrix(Camera camera);
        GetCameraMatrix = fn(Matrix, { Camera3D }),
        -- Matrix GetCameraMatrix2D(Camera2D camera);
        GetCameraMatrix2D = fn(Matrix, { Camera2D }),

        -- void SetTargetFPS(int fps);
        SetTargetFPS = fn(void, { int }),
        -- float GetFrameTime(void);
        GetFrameTime = fn(float, {}),
        -- double GetTime(void);
        GetTime = fn(ffi.types.double, {}),
        -- int GetFPS(void);
        GetFPS = fn(int, {}),

        -- void SwapScreenBuffer(void);
        SwapScreenBuffer = fn(void, {}),
        -- void PollInputEvents(void);
        PollInputEvents = fn(void, {}),
        -- void WaitTime(double seconds);
        WaitTime = fn(void, { ffi.types.double }),

        -- void SetRandomSeed(unsigned int seed);
        SetRandomSeed = fn(void, { uint }),
        -- int GetRandomValue(int min, int max);
        GetRandomValue = fn(int, { int, int }),
        -- int *LoadRandomSequence(unsigned int count, int min, int max);
        LoadRandomSequence = fn(ffi.types.pointer, { uint, int, int }),
        -- void UnloadRandomSequence(int *sequence);
        UnloadRandomSequence = fn(void, { ffi.types.pointer }),

        -- void TakeScreenshot(const char *fileName);
        TakeScreenshot = fn(void, { cstring }),
        -- void SetConfigFlags(unsigned int flags);
        SetConfigFlags = fn(void, { uint }),
        -- void OpenURL(const char *url);
        OpenURL = fn(void, { cstring }),

        -- void TraceLog(int logLevel, const char *text, ...);
        TraceLog = fn(void, { int, cstring }),
        -- void SetTraceLogLevel(int logLevel);
        SetTraceLogLevel = fn(void, { int }),
        -- void *MemAlloc(unsigned int size);
        MemAlloc = fn(ffi.types.pointer, { uint }),
        -- void *MemRealloc(void *ptr, unsigned int size);
        MemRealloc = fn(ffi.types.pointer, { ffi.types.pointer, uint }),
        -- void MemFree(void *ptr);
        MemFree = fn(void, { ffi.types.pointer }),

        -- void SetTraceLogCallback(TraceLogCallback callback);
        SetTraceLogCallback = fn(void, { ffi.types.pointer }),
        -- void SetLoadFileDataCallback(LoadFileDataCallback callback);
        SetLoadFileDataCallback = fn(void, { ffi.types.pointer }),
        -- void SetSaveFileDataCallback(SaveFileDataCallback callback);
        SetSaveFileDataCallback = fn(void, { ffi.types.pointer }),
        -- void SetLoadFileTextCallback(LoadFileTextCallback callback);
        SetLoadFileTextCallback = fn(void, { ffi.types.pointer }),
        -- void SetSaveFileTextCallback(SaveFileTextCallback callback);
        SetSaveFileTextCallback = fn(void, { ffi.types.pointer }),

        -- unsigned char *LoadFileData(const char *fileName, int *dataSize);
        LoadFileData = fn(ffi.types.pointer, { cstring, ffi.types.pointer }),
        -- void UnloadFileData(unsigned char *data);
        UnloadFileData = fn(void, { ffi.types.pointer }),
        -- bool SaveFileData(const char *fileName, void *data, int dataSize);
        SaveFileData = fn(bool, { cstring, ffi.types.pointer, int }),
        -- bool ExportDataAsCode(const unsigned char *data, int dataSize, const char *fileName);
        ExportDataAsCode = fn(bool, { ffi.types.pointer, int, cstring }),
        -- char *LoadFileText(const char *fileName);
        LoadFileText = fn(cstring, { cstring }),
        -- void UnloadFileText(char *text);
        UnloadFileText = fn(void, { cstring }),
        -- bool SaveFileText(const char *fileName, char *text);
        SaveFileText = fn(bool, { cstring, cstring }),

        -- bool FileExists(const char *fileName);
        FileExists = fn(bool, { cstring }),
        -- bool DirectoryExists(const char *dirPath);
        DirectoryExists = fn(bool, { cstring }),
        -- bool IsFileExtension(const char *fileName, const char *ext);
        IsFileExtension = fn(bool, { cstring, cstring }),
        -- int GetFileLength(const char *fileName);
        GetFileLength = fn(int, { cstring }),
        -- const char *GetFileExtension(const char *fileName);
        GetFileExtension = fn(cstring, { cstring }),
        -- const char *GetFileName(const char filePath);
        GetFileName = fn(cstring, { cstring }),
        -- const char *GetFileNameWithoutExt(const char *filePath);
        GetFileNameWithoutExt = fn(cstring, { cstring }),
        -- const char *GetDirectoryPath(const char *filePath);
        GetDirectoryPath = fn(cstring, { cstring }),
        -- const char *GetPrevDirectoryPath(const char *dirPath);
        GetPrevDirectoryPath = fn(cstring, { cstring }),
        -- const char *GetWorkingDirectory(void);
        GetWorkingDirectory = fn(cstring, {}),
        -- const char *GetApplicationDirectory(void);
        GetApplicationDirectory = fn(cstring, {}),
        -- int MakeDirectory(const char *dirPath);
        MakeDirectory = fn(int, { cstring }),
        -- bool ChangeDirectory(const char *dir);
        ChangeDirectory = fn(bool, { cstring }),
        -- bool IsPathFile(const char *path);
        IsPathFile = fn(bool, { cstring }),
        -- bool IsFileNameValid(const char *fileName);
        IsFileNameValid = fn(bool, { cstring }),
        -- FilePathList LoadDirectoryFiles(const char *dirPath);
        LoadDirectoryFiles = fn(FilePathList, { cstring }),
        -- FilePathList LoadDirectoryFilesEx(const char *basePath, const char *filter, bool scanSubdirs);
        LoadDirectoryFilesEx = fn(FilePathList, { cstring, cstring, bool }),
        -- void UnloadDirectoryFiles(FilePathList files);
        UnloadDirectoryFiles = fn(void, { FilePathList }),
        -- bool IsFileDropped(void);
        IsFileDropped = fn(bool, {}),
        -- FilePathList LoadDroppedFiles(void);
        LoadDroppedFiles = fn(FilePathList, {}),
        -- void UnloadDroppedFiles(FilePathList files);
        UnloadDroppedFiles = fn(void, { FilePathList }),
        -- long GetFileModTime(const char *fileName);
        GetFileModTime = fn(long, { cstring }),

        -- unsigned char *CompressData(const unsigned char *data, int dataSize, int *compDataSize);
        CompressData = fn(ffi.types.pointer, { ffi.types.pointer, int, ffi.types.pointer }),
        -- unsigned char *DecompressData(const unsigned char *compData, int compDataSize, int *dataSize);
        DecompressData = fn(ffi.types.pointer, { ffi.types.pointer, int, ffi.types.pointer }),
        -- char *EncodeDataBase64(const unsigned char *data, int dataSize, int *outputSize);
        EncodeDataBase64 = fn(cstring, { ffi.types.pointer, int, ffi.types.pointer }),
        -- unsigned char *DecodeDataBase64(const unsigned char *data, int *outputSize);
        DecodeDataBase64 = fn(ffi.types.pointer, { ffi.types.pointer, ffi.types.pointer }),
        -- unsigned int ComputeCRC32(unsigned char *data, int dataSize);
        ComputeCRC32 = fn(uint, { ffi.types.pointer, int }),
        -- unsigned int *ComputeMD5(unsigned char *data, int dataSize);
        ComputeMD5 = fn(ffi.types.pointer, { ffi.types.pointer, int }),
        -- unsigned int *ComputeSHA1(unsigned char *data, int dataSize);
        ComputeSHA1 = fn(ffi.types.pointer, { ffi.types.pointer, int }),

        -- AutomationEventList LoadAutomationEventList(const char *fileName);
        LoadAutomationEventList = fn(AutomationEventList, { cstring }),
        -- void UnloadAutomationEventList(AutomationEventList list);
        UnloadAutomationEventList = fn(void, { AutomationEventList }),
        -- bool ExportAutomationEventList(AutomationEventList list, const char *fileName);
        ExportAutomationEventList = fn(bool, { AutomationEventList, cstring }),
        -- void SetAutomationEventList(AutomationEventList *list);
        SetAutomationEventList = fn(void, { ffi.types.pointer }),
        -- void SetAutomationEventBaseFrame(int frame);
        SetAutomationEventBaseFrame = fn(void, { int }),
        -- void StartAutomationEventRecording(void);
        StartAutomationEventRecording = fn(void, {}),
        -- void StopAutomationEventRecording(void);
        StopAutomationEventRecording = fn(void, {}),
        -- void PlayAutomationEvent(AutomationEvent event);
        PlayAutomationEvent = fn(void, { AutomationEvent }),

        -- bool IsKeyPressed(int key);
        IsKeyPressed = fn(bool, { int }),
        -- bool IsKeyPressedRepeat(int key);
        IsKeyPressedRepeat = fn(bool, { int }),
        -- bool IsKeyDown(int key);
        IsKeyDown = fn(bool, { int }),
        -- bool IsKeyReleased(int key);
        IsKeyReleased = fn(bool, { int }),
        -- bool IsKeyUp(int key);
        IsKeyUp = fn(bool, { int }),
        -- int GetKeyPressed(void);
        GetKeyPressed = fn(int, {}),
        -- int GetCharPressed(void);
        GetCharPressed = fn(int, {}),
        -- void SetExitKey(int key);
        SetExitKey = fn(void, { int }),

        -- bool IsGamepadAvailable(int gamepad);
        IsGamepadAvailable = fn(bool, { int }),
        -- const char *GetGamepadName(int gamepad);
        GetGamepadName = fn(cstring, { int }),
        -- bool IsGamepadButtonPressed(int gamepad, int button);
        IsGamepadButtonPressed = fn(bool, { int, int }),
        -- bool IsGamepadButtonDown(int gamepad, int button);
        IsGamepadButtonDown = fn(bool, { int, int }),
        -- bool IsGamepadButtonReleased(int gamepad, int button);
        IsGamepadButtonReleased = fn(bool, { int, int }),
        -- bool IsGamepadButtonUp(int gamepad, int button);
        IsGamepadButtonUp = fn(bool, { int, int }),
        -- int GetGamepadButtonPressed(void);
        GetGamepadButtonPressed = fn(int, {}),
        -- int GetGamepadAxisCount(int gamepad);
        GetGamepadAxisCount = fn(int, { int }),
        -- float GetGamepadAxisMovement(int gamepad, int axis);
        GetGamepadAxisMovement = fn(float, { int, int }),
        -- int SetGamepadMappings(const char *mappings);
        SetGamepadMappings = fn(int, { cstring }),
        -- void SetGamepadVibration(int gamepad, float leftMotor, float rightMotor, float duration);
        SetGamepadVibration = fn(void, { int, float, float, float }),

        -- bool IsMouseButtonPressed(int button);
        IsMouseButtonPressed = fn(bool, { int }),
        -- bool IsMouseButtonDown(int button);
        IsMouseButtonDown = fn(bool, { int }),
        -- bool IsMouseButtonReleased(int button);
        IsMouseButtonReleased = fn(bool, { int }),
        -- bool IsMouseButtonUp(int button);
        IsMouseButtonUp = fn(bool, { int }),
        -- int GetMouseX(void);
        GetMouseX = fn(int, {}),
        -- int GetMouseY(void);
        GetMouseY = fn(int, {}),
        -- Vector2 GetMousePosition(void);
        GetMousePosition = fn(Vector2, {}),
        -- Vector2 GetMouseDelta(void);
        GetMouseDelta = fn(Vector2, {}),
        -- void SetMousePosition(int x, int y);
        SetMousePosition = fn(void, { int, int }),
        -- void SetMouseOffset(int offsetX, int offsetY);
        SetMouseOffset = fn(void, { int, int }),
        -- void SetMouseScale(float scaleX, float scaleY);
        SetMouseScale = fn(void, { float, float }),
        -- float GetMouseWheelMove(void);
        GetMouseWheelMove = fn(float, {}),
        -- Vector2 GetMouseWheelMoveV(void);
        GetMouseWheelMoveV = fn(Vector2, {}),
        -- void SetMouseCursor(int cursor);
        SetMouseCursor = fn(void, { int }),

        -- int GetTouchX(void);
        GetTouchX = fn(int, {}),
        -- int GetTouchY(void);
        GetTouchY = fn(int, {}),
        -- Vector2 GetTouchPosition(int index);
        GetTouchPosition = fn(Vector2, { int }),
        -- int GetTouchPointId(int index);
        GetTouchPointId = fn(int, { int }),
        -- int GetTouchPointCount(void);
        GetTouchPointCount = fn(int, {}),

        -- void SetGesturesEnabled(unsigned int flags);
        SetGesturesEnabled = fn(void, { uint }),
        -- bool IsGestureDetected(unsigned int gesture);
        IsGestureDetected = fn(bool, { uint }),
        -- int GetGestureDetected(void);
        GetGestureDetected = fn(int, {}),
        -- float GetGestureHoldDuration(void);
        GetGestureHoldDuration = fn(float, {}),
        -- Vector2 GetGestureDragVector(void);
        GetGestureDragVector = fn(Vector2, {}),
        -- float GetGestureDragAngle(void);
        GetGestureDragAngle = fn(float, {}),
        -- Vector2 GetGesturePinchVector(void);
        GetGesturePinchVector = fn(Vector2, {}),
        -- float GetGesturePinchAngle(void);
        GetGesturePinchAngle = fn(float, {}),

        -- void UpdateCamera(Camera *camera, int mode);
        UpdateCamera = fn(void, { raylib.ptrs.Camera, int }),
        -- void UpdateCameraPro(Camera *camera, Vector3 movement, Vector3 rotation, float zoom);
        UpdateCameraPro = fn(void, { raylib.ptrs.Camera, Vector3, Vector3, float }),
    }
}