--[=[
	ML-KEM Main Module (Key Encapsulation Mechanism)

	The complete ML-KEM post quantum key encapsulation.
	Key generation, encapsulation, and decapsulation operations with
	CCA security using the Fujisaki Okamoto transform.

	Example usage:
		local MlKem = require(script.MlKem)
		local CSPRNG = require(script.CSPRNG)

		local D = CSPRNG.RandomBytes(32)
		local Z = CSPRNG.RandomBytes(32)
		local PublicKey, SecretKey = MlKem.MLKEM_1024.KeyGen(D, Z)

		local Message = CSPRNG.RandomBytes(32)
		local Success, Ciphertext, SharedSecret = MlKem.MLKEM_1024.Encapsulate(PublicKey, Message)

		if Success then
			local RecoveredSecret = MlKem.MLKEM_1024.Decapsulate(SecretKey, Ciphertext)
			-- RecoveredSecret should equal SharedSecret
		end
--]=]

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

local Pke = require("@self/PKE")
local Polyvec = require("@self/PolyVec")
local CSPRNG = require("@self/CSPRNG")
local Params = require("@self/Params")
local Utils = require("@self/Utils")
local SHA3 = require("@self/SHA3")

local DECAP_G_INPUT = buffer.create(64)
local DECAP_K_PRIME = buffer.create(32)
local DECAP_R_PRIME = buffer.create(32)
local DECAP_J_OUTPUT = buffer.create(32)

local ENCAP_G_INPUT = buffer.create(64)
local ENCAP_K_BYTES = buffer.create(32)
local ENCAP_R = buffer.create(32)

local MlKem = {
	CSPRNG = CSPRNG,
}

function MlKem.KeyGen(K: number, Eta1: number, D: buffer, Z: buffer): (buffer, buffer)
	if not Params.CheckKeygenParams(K, Eta1) then
		error("Invalid keygen parameters")
	end

	if buffer.len(D) ~= 32 then
		error("D must be 32 bytes")
	end

	if buffer.len(Z) ~= 32 then
		error("Z must be 32 bytes")
	end

	local KpkePublicKey, KpkeSecretKey = Pke.KeyGen(K, Eta1, D)
	local PublicKeyHash = SHA3.SHA3_256(KpkePublicKey)

	local SecretKeyLen = Utils.GetKemSecretKeyLen(K)
	local SecretKey = buffer.create(SecretKeyLen)

	local Offset = 0

	local KpkeSecretKeyLen = buffer.len(KpkeSecretKey)
	buffer.copy(SecretKey, Offset, KpkeSecretKey, 0, KpkeSecretKeyLen)
	Offset = Offset + KpkeSecretKeyLen

	local KpkePublicKeyLen = buffer.len(KpkePublicKey)
	buffer.copy(SecretKey, Offset, KpkePublicKey, 0, KpkePublicKeyLen)
	Offset += KpkePublicKeyLen

	buffer.copy(SecretKey, Offset, PublicKeyHash, 0, 32)
	Offset += 32

	buffer.copy(SecretKey, Offset, Z, 0, 32)

	return KpkePublicKey, SecretKey
end

function MlKem.Encapsulate(
	K: number,
	Eta1: number,
	Eta2: number,
	Du: number,
	Dv: number,
	PublicKey: buffer,
	Message: buffer
): (buffer?, buffer?)
	if not Params.CheckEncapParams(K, Eta1, Eta2, Du, Dv) then
		error("Invalid encapsulation parameters")
	end

	if buffer.len(PublicKey) ~= Utils.GetKemPublicKeyLen(K) then
		error("Invalid public key length")
	end

	if buffer.len(Message) ~= 32 then
		error("Message must be 32 bytes")
	end

	local RhoOffset = K * 12 * 32
	local EncodedTPrime = buffer.create(RhoOffset)
	buffer.copy(EncodedTPrime, 0, PublicKey, 0, RhoOffset)

	local TPrime = Polyvec.VecDecode(EncodedTPrime, K, 12)
	local ReEncodedTPrime = Polyvec.VecEncode(TPrime, K, 12)

	local IsValid = Utils.CtMemcmp(EncodedTPrime, ReEncodedTPrime)
	if IsValid ~= 0xFFFFFFFF then
		error("malformed public key encoding")
	end

	local PublicKeyHash = SHA3.SHA3_256(PublicKey)

	local GInput = ENCAP_G_INPUT
	buffer.copy(GInput, 0, Message, 0, 32)
	buffer.copy(GInput, 32, PublicKeyHash, 0, 32)

	local GOutput = SHA3.SHA3_512(GInput)

	local K_bytes = buffer.create(32)
	local R = ENCAP_R
	buffer.copy(K_bytes, 0, GOutput, 0, 32)
	buffer.copy(R, 0, GOutput, 32, 32)

	local Ciphertext = Pke.Encrypt(K, Eta1, Eta2, Du, Dv, PublicKey, Message, R)

	return Ciphertext, K_bytes
end

function MlKem.Decapsulate(
	K: number,
	Eta1: number,
	Eta2: number,
	Du: number,
	Dv: number,
	SecretKey: buffer,
	Ciphertext: buffer
): buffer
	if not Params.CheckDecapParams(K, Eta1, Eta2, Du, Dv) then
		error("Invalid decapsulation parameters")
	end
	if buffer.len(SecretKey) ~= Utils.GetKemSecretKeyLen(K) then
		error("Invalid secret key length")
	end

	if buffer.len(Ciphertext) ~= Utils.GetKemCipherTextLen(K, Du, Dv) then
		error("Invalid ciphertext length")
	end

	local KpkeSecretKeyLen = Utils.GetPkeSecretKeyLen(K)
	local KpkePublicKeyLen = Utils.GetPkePublicKeyLen(K)

	local KpkeSecretKey = buffer.create(KpkeSecretKeyLen)
	local KpkePublicKey = buffer.create(KpkePublicKeyLen)
	local H = buffer.create(32)
	local Z = buffer.create(32)

	local Offset = 0
	buffer.copy(KpkeSecretKey, 0, SecretKey, Offset, KpkeSecretKeyLen)
	Offset += KpkeSecretKeyLen

	buffer.copy(KpkePublicKey, 0, SecretKey, Offset, KpkePublicKeyLen)
	Offset += KpkePublicKeyLen

	buffer.copy(H, 0, SecretKey, Offset, 32)
	Offset += 32

	buffer.copy(Z, 0, SecretKey, Offset, 32)

	local MPrime = Pke.Decrypt(K, Du, Dv, KpkeSecretKey, Ciphertext)

	local GInput = DECAP_G_INPUT
	buffer.copy(GInput, 0, MPrime, 0, 32)
	buffer.copy(GInput, 32, H, 0, 32)

	local GOutput = SHA3.SHA3_512(GInput)

	local KPrime = DECAP_K_PRIME
	local RPrime = DECAP_R_PRIME
	buffer.copy(KPrime, 0, GOutput, 0, 32)
	buffer.copy(RPrime, 0, GOutput, 32, 32)

	local JInput = buffer.create(32 + buffer.len(Ciphertext))
	buffer.copy(JInput, 0, Z, 0, 32)
	buffer.copy(JInput, 32, Ciphertext, 0, buffer.len(Ciphertext))

	local JOutputBuffer = SHA3.SHAKE256(JInput, 32)
	local JOutput = DECAP_J_OUTPUT
	buffer.copy(JOutput, 0, JOutputBuffer, 0, 32)

	local CiphertextPrime = Pke.Encrypt(K, Eta1, Eta2, Du, Dv, KpkePublicKey, MPrime, RPrime)
	local IsValid = Utils.CtMemcmp(Ciphertext, CiphertextPrime)

	local SharedSecret = buffer.create(32)
	Utils.CtCondMemcpy(IsValid, SharedSecret, KPrime, JOutput)

	return SharedSecret
end

function MlKem.SecretsEqual(Secret1: buffer, Secret2: buffer): boolean
	if buffer.len(Secret1) ~= 32 or buffer.len(Secret2) ~= 32 then
		return false
	end

	return Utils.CtMemcmp(Secret1, Secret2) == 0xFFFFFFFF
end

function MlKem.ValidateDecapsulationKey(K: number, SecretKey: buffer): boolean
	local KpkeSecretKeyLen = Utils.GetPkeSecretKeyLen(K)
	local KpkePublicKeyLen = Utils.GetPkePublicKeyLen(K)

	local EkOffset = KpkeSecretKeyLen
	local HOffset = KpkeSecretKeyLen + KpkePublicKeyLen

	local Ek = buffer.create(KpkePublicKeyLen)
	local StoredH = buffer.create(32)
	buffer.copy(Ek, 0, SecretKey, EkOffset, KpkePublicKeyLen)
	buffer.copy(StoredH, 0, SecretKey, HOffset, 32)

	local ComputedH = SHA3.SHA3_256(Ek)
	return Utils.CtMemcmp(StoredH, ComputedH) == 0xFFFFFFFF
end

MlKem.MLKEM_512 = {
	KeyGen = function(D: buffer, Z: buffer): (buffer, buffer)
		return MlKem.KeyGen(2, 3, D, Z)
	end,

	Encapsulate = function(PublicKey: buffer, Message: buffer): (buffer?, buffer?)
		return MlKem.Encapsulate(2, 3, 2, 10, 4, PublicKey, Message)
	end,

	Decapsulate = function(SecretKey: buffer, Ciphertext: buffer): buffer
		return MlKem.Decapsulate(2, 3, 2, 10, 4, SecretKey, Ciphertext)
	end,

	GenerateKeys = function(): (buffer, buffer)
		local D = CSPRNG.RandomBytes(32)
		local Z = CSPRNG.RandomBytes(32)

		return MlKem.MLKEM_512.KeyGen(D, Z)
	end,

	ValidateDecapsulationKey = function(SecretKey: buffer): boolean
		return MlKem.ValidateDecapsulationKey(2, SecretKey)
	end,
}

MlKem.MLKEM_768 = {
	KeyGen = function(D: buffer, Z: buffer): (buffer, buffer)
		return MlKem.KeyGen(3, 2, D, Z)
	end,

	Encapsulate = function(PublicKey: buffer, Message: buffer): (buffer?, buffer?)
		return MlKem.Encapsulate(3, 2, 2, 10, 4, PublicKey, Message)
	end,

	Decapsulate = function(SecretKey: buffer, Ciphertext: buffer): buffer
		return MlKem.Decapsulate(3, 2, 2, 10, 4, SecretKey, Ciphertext)
	end,

	GenerateKeys = function(): (buffer, buffer)
		local D = CSPRNG.RandomBytes(32)
		local Z = CSPRNG.RandomBytes(32)

		return MlKem.MLKEM_768.KeyGen(D, Z)
	end,

	ValidateDecapsulationKey = function(SecretKey: buffer): boolean
		return MlKem.ValidateDecapsulationKey(3, SecretKey)
	end,
}

MlKem.MLKEM_1024 = {
	KeyGen = function(D: buffer, Z: buffer): (buffer, buffer)
		return MlKem.KeyGen(4, 2, D, Z)
	end,

	Encapsulate = function(PublicKey: buffer, Message: buffer): (buffer?, buffer?)
		return MlKem.Encapsulate(4, 2, 2, 11, 5, PublicKey, Message)
	end,

	Decapsulate = function(SecretKey: buffer, Ciphertext: buffer): buffer
		return MlKem.Decapsulate(4, 2, 2, 11, 5, SecretKey, Ciphertext)
	end,

	GenerateKeys = function(): (buffer, buffer)
		local D = CSPRNG.RandomBytes(32)
		local Z = CSPRNG.RandomBytes(32)

		return MlKem.MLKEM_1024.KeyGen(D, Z)
	end,

	ValidateDecapsulationKey = function(SecretKey: buffer): boolean
		return MlKem.ValidateDecapsulationKey(4, SecretKey)
	end,
}

return MlKem


--[=[
	Cryptography library: XXHash32

	⚠ WARNING: XXHash32 wasn't designed with cryptographic security in mind!
	Only use for non-security purposes. For security, use SHA256 or higher. ⚠

	Return type: number
	Example usage:
		local Message = buffer.fromstring("Hello World")

		--------Usage Case 1--------
		local Result = XXHash32(Message)

		--------Usage Case 2--------
		local Seed = 0xDEADBEEF
		local Result = XXHash32(Message, Seed)
--]=]

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

local function XXH32(Message: buffer, Seed: number?): number
	local PRIME_1, PRIME_1_HIGH, PRIME_1_LOW = 0x9e3779B1, 40503, 31153
	local PRIME_2, PRIME_2_HIGH, PRIME_2_LOW = 0x85ebca77, 34283, 51831
	local _PRIME_3, PRIME_3_HIGH, PRIME_3_LOW = 0xc2b2ae3d, 49842, 44605
	local _PRIME_4, PRIME_4_HIGH, PRIME_4_LOW = 0x27d4eb2f, 10196, 60207
	local PRIME_5, PRIME_5_HIGH, PRIME_5_LOW = 0x165667b1, 5718, 26545

	local UsedSeed = Seed or 0
	local MessageLength = buffer.len(Message)
	local Digest: number
	local CurrentOffset = 0

	if MessageLength >= 16 then
		local Accumulator1 = UsedSeed + PRIME_1 + PRIME_2
		local Accumulator2 = UsedSeed + PRIME_2
		local Accumulator3 = UsedSeed
		local Accumulator4 = UsedSeed - PRIME_1

		while CurrentOffset <= MessageLength - 16 do
			local Word1 = buffer.readu32(Message, CurrentOffset)
			local Word2 = buffer.readu32(Message, CurrentOffset + 4)
			local Word3 = buffer.readu32(Message, CurrentOffset + 8)
			local Word4 = buffer.readu32(Message, CurrentOffset + 12)

			local AHigh1, ALow1 = bit32.rshift(Word1, 16), bit32.band(Word1, 65535)
			local Mult1 = bit32.lshift((AHigh1 * PRIME_2_LOW) + (ALow1 * PRIME_2_HIGH), 16) + (ALow1 * PRIME_2_LOW)

			local Temp1 = bit32.lrotate(Accumulator1 + Mult1, 13)
			local AHigh1_2, ALow1_2 = bit32.rshift(Temp1, 16), bit32.band(Temp1, 65535)
			Accumulator1 = bit32.lshift((AHigh1_2 * PRIME_1_LOW) + (ALow1_2 * PRIME_1_HIGH), 16)
				+ (ALow1_2 * PRIME_1_LOW)

			local AHigh2, ALow2 = bit32.rshift(Word2, 16), bit32.band(Word2, 65535)
			local Mult2 = bit32.lshift((AHigh2 * PRIME_2_LOW) + (ALow2 * PRIME_2_HIGH), 16) + (ALow2 * PRIME_2_LOW)

			local Temp2 = bit32.lrotate(Accumulator2 + Mult2, 13)
			local AHigh2_2, ALow2_2 = bit32.rshift(Temp2, 16), bit32.band(Temp2, 65535)
			Accumulator2 = bit32.lshift((AHigh2_2 * PRIME_1_LOW) + (ALow2_2 * PRIME_1_HIGH), 16)
				+ (ALow2_2 * PRIME_1_LOW)

			local AHigh3, ALow3 = bit32.rshift(Word3, 16), bit32.band(Word3, 65535)
			local Mult3 = bit32.lshift((AHigh3 * PRIME_2_LOW) + (ALow3 * PRIME_2_HIGH), 16) + (ALow3 * PRIME_2_LOW)

			local Temp3 = bit32.lrotate(Accumulator3 + Mult3, 13)
			local AHigh3_2, ALow3_2 = bit32.rshift(Temp3, 16), bit32.band(Temp3, 65535)
			Accumulator3 = bit32.lshift((AHigh3_2 * PRIME_1_LOW) + (ALow3_2 * PRIME_1_HIGH), 16)
				+ (ALow3_2 * PRIME_1_LOW)

			local AHigh4, ALow4 = bit32.rshift(Word4, 16), bit32.band(Word4, 65535)
			local Mult4 = bit32.lshift((AHigh4 * PRIME_2_LOW) + (ALow4 * PRIME_2_HIGH), 16) + (ALow4 * PRIME_2_LOW)

			local Temp4 = bit32.lrotate(Accumulator4 + Mult4, 13)
			local AHigh4_2, ALow4_2 = bit32.rshift(Temp4, 16), bit32.band(Temp4, 65535)
			Accumulator4 = bit32.lshift((AHigh4_2 * PRIME_1_LOW) + (ALow4_2 * PRIME_1_HIGH), 16)
				+ (ALow4_2 * PRIME_1_LOW)

			CurrentOffset += 16
		end

		Digest = bit32.lrotate(Accumulator1, 1)
			+ bit32.lrotate(Accumulator2, 7)
			+ bit32.lrotate(Accumulator3, 12)
			+ bit32.lrotate(Accumulator4, 18)
	else
		Digest = UsedSeed + PRIME_5
	end

	Digest += MessageLength

	while CurrentOffset <= MessageLength - 4 do
		if CurrentOffset + 4 <= buffer.len(Message) then
			local Word = buffer.readu32(Message, CurrentOffset)

			local AHigh_w, ALow_w = bit32.rshift(Word, 16), bit32.band(Word, 65535)
			local Mult_w = bit32.lshift((AHigh_w * PRIME_3_LOW) + (ALow_w * PRIME_3_HIGH), 16) + (ALow_w * PRIME_3_LOW)

			Digest += Mult_w

			local Rotated = bit32.lrotate(Digest, 17)
			local AHigh_r, ALow_r = bit32.rshift(Rotated, 16), bit32.band(Rotated, 65535)
			Digest = bit32.lshift((AHigh_r * PRIME_4_LOW) + (ALow_r * PRIME_4_HIGH), 16) + (ALow_r * PRIME_4_LOW)
		end
		CurrentOffset += 4
	end

	while CurrentOffset < MessageLength do
		if CurrentOffset < buffer.len(Message) then
			local ByteValue = buffer.readu8(Message, CurrentOffset)

			local AHigh_b, ALow_b = bit32.rshift(ByteValue, 16), bit32.band(ByteValue, 65535)
			local Mult_b = bit32.lshift((AHigh_b * PRIME_5_LOW) + (ALow_b * PRIME_5_HIGH), 16) + (ALow_b * PRIME_5_LOW)

			Digest += Mult_b

			local Rotated_b = bit32.lrotate(Digest, 11)
			local AHigh_rb, ALow_rb = bit32.rshift(Rotated_b, 16), bit32.band(Rotated_b, 65535)
			Digest = bit32.lshift((AHigh_rb * PRIME_1_LOW) + (ALow_rb * PRIME_1_HIGH), 16) + (ALow_rb * PRIME_1_LOW)
		end
		CurrentOffset += 1
	end

	local XorResult1 = bit32.bxor(Digest, bit32.rshift(Digest, 15))
	local AHigh_f1, ALow_f1 = bit32.rshift(XorResult1, 16), bit32.band(XorResult1, 65535)
	Digest = bit32.lshift((AHigh_f1 * PRIME_2_LOW) + (ALow_f1 * PRIME_2_HIGH), 16) + (ALow_f1 * PRIME_2_LOW)

	local XorResult2 = bit32.bxor(Digest, bit32.rshift(Digest, 13))
	local AHigh_f2, ALow_f2 = bit32.rshift(XorResult2, 16), bit32.band(XorResult2, 65535)
	Digest = bit32.lshift((AHigh_f2 * PRIME_3_LOW) + (ALow_f2 * PRIME_3_HIGH), 16) + (ALow_f2 * PRIME_3_LOW)

	return bit32.bxor(Digest, bit32.rshift(Digest, 16))
end

return XXH32


--[=[
	Cryptography library: Base64

	Return type: buffer
	Example usage:
		local Input = buffer.fromstring("Hello World")
		local Encoded = Base64.Encode(Input)
		local Decoded = Base64.Decode(Encoded)
--]=]

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

local PADDING_CHARACTER = 61
local ALPHABET_BYTES = buffer.create(64)
do
	local Characters = {
		65,
		66,
		67,
		68,
		69,
		70,
		71,
		72,
		73,
		74,
		75,
		76,
		77,
		78,
		79,
		80,
		81,
		82,
		83,
		84,
		85,
		86,
		87,
		88,
		89,
		90,
		97,
		98,
		99,
		100,
		101,
		102,
		103,
		104,
		105,
		106,
		107,
		108,
		109,
		110,
		111,
		112,
		113,
		114,
		115,
		116,
		117,
		118,
		119,
		120,
		121,
		122,
		48,
		49,
		50,
		51,
		52,
		53,
		54,
		55,
		56,
		57,
		43,
		47,
	}

	for Index = 1, 64 do
		buffer.writeu8(ALPHABET_BYTES, Index - 1, Characters[Index])
	end
end

local ENCODE_A, ENCODE_B = buffer.create(131072), buffer.create(131072)
do
	for Byte0 = 0, 255 do
		local Sextet0 = Byte0 // 4
		local Sextet0Char = buffer.readu8(ALPHABET_BYTES, Sextet0)
		local Byte0Low = Byte0 % 4
		for Byte1 = 0, 255 do
			local Sextet1 = Byte0Low * 16 + (Byte1 // 16)
			local Index = (Byte0 + Byte1 * 256) * 2
			buffer.writeu16(ENCODE_A, Index, Sextet0Char + buffer.readu8(ALPHABET_BYTES, Sextet1) * 256)
		end
	end

	for Byte1 = 0, 255 do
		local Nibble = (Byte1 % 16) * 4
		for Byte2 = 0, 255 do
			local Sextet2 = Nibble + (Byte2 // 64)
			local Sextet3 = Byte2 % 64
			local Index = (Byte1 + Byte2 * 256) * 2
			buffer.writeu16(
				ENCODE_B,
				Index,
				buffer.readu8(ALPHABET_BYTES, Sextet2) + buffer.readu8(ALPHABET_BYTES, Sextet3) * 256
			)
		end
	end
end

local DECODE_TET = buffer.create(256)
do
	for Index = 1, 64 do
		buffer.writeu8(DECODE_TET, buffer.readu8(ALPHABET_BYTES, Index - 1), Index - 1)
	end

	buffer.writeu8(DECODE_TET, PADDING_CHARACTER, 254)
end

local DECODE_PAIR = buffer.create(131072)
do
	for Char0 = 0, 255 do
		local Decoded0 = buffer.readu8(DECODE_TET, Char0)
		for Char1 = 0, 255 do
			local Decoded1 = buffer.readu8(DECODE_TET, Char1)
			local Key = (Char0 + Char1 * 256) * 2
			if Decoded0 < 64 and Decoded1 < 64 then
				buffer.writeu16(DECODE_PAIR, Key, Decoded0 * 64 + Decoded1)
			elseif Decoded0 == 254 or Decoded1 == 254 then
				buffer.writeu16(DECODE_PAIR, Key, 254)
			end
		end
	end
end

local PAIR_A_BYTE1_HIGH = buffer.create(4096)
local PAIR_B_BYTE1_LOW = buffer.create(4096)

local PAIR_A_BYTE0, PAIR_B_BYTE2 = buffer.create(4096), buffer.create(4096)
do
	for PairA = 0, 4095 do
		local Sextet0 = PairA // 64
		local Sextet1 = PairA % 64
		buffer.writeu8(PAIR_A_BYTE0, PairA, Sextet0 * 4 + (Sextet1 // 16))
		buffer.writeu8(PAIR_A_BYTE1_HIGH, PairA, (Sextet1 % 16) * 16)
	end

	for PairB = 0, 4095 do
		local Sextet2 = PairB // 64
		local Sextet3 = PairB % 64
		buffer.writeu8(PAIR_B_BYTE1_LOW, PairB, Sextet2 // 4)
		buffer.writeu8(PAIR_B_BYTE2, PairB, (Sextet2 % 4) * 64 + Sextet3)
	end
end

local function Encode(Input: buffer): buffer
	local InputLength = buffer.len(Input)
	if InputLength == 0 then
		return buffer.create(0)
	end

	local Output = buffer.create(((InputLength + 2) // 3) * 4)
	local InputIndex = 0
	local OutputIndex = 0
	local FullTripletEnd = InputLength - (InputLength % 3)
	local UnrollStop = InputLength - 13

	local EncodeA = ENCODE_A
	local EncodeB = ENCODE_B
	local AlphabetBytes = ALPHABET_BYTES

	while InputIndex <= UnrollStop do
		local Word0 = buffer.readu32(Input, InputIndex)
		local Word1 = buffer.readu32(Input, InputIndex + 3)
		local Word2 = buffer.readu32(Input, InputIndex + 6)
		local Word3 = buffer.readu32(Input, InputIndex + 9)

		local KeyA0 = Word0 % 65536
		local KeyB0 = (Word0 // 256) % 65536
		buffer.writeu32(
			Output,
			OutputIndex,
			buffer.readu16(EncodeA, KeyA0 * 2) + buffer.readu16(EncodeB, KeyB0 * 2) * 0x10000
		)

		local KeyA1 = Word1 % 65536
		local KeyB1 = (Word1 // 256) % 65536
		buffer.writeu32(
			Output,
			OutputIndex + 4,
			buffer.readu16(EncodeA, KeyA1 * 2) + buffer.readu16(EncodeB, KeyB1 * 2) * 0x10000
		)

		local KeyA2 = Word2 % 65536
		local KeyB2 = (Word2 // 256) % 65536
		buffer.writeu32(
			Output,
			OutputIndex + 8,
			buffer.readu16(EncodeA, KeyA2 * 2) + buffer.readu16(EncodeB, KeyB2 * 2) * 0x10000
		)

		local KeyA3 = Word3 % 65536
		local KeyB3 = (Word3 // 256) % 65536
		buffer.writeu32(
			Output,
			OutputIndex + 12,
			buffer.readu16(EncodeA, KeyA3 * 2) + buffer.readu16(EncodeB, KeyB3 * 2) * 0x10000
		)

		InputIndex += 12
		OutputIndex += 16
	end

	while InputIndex < FullTripletEnd do
		local KeyA = buffer.readu16(Input, InputIndex)
		local Byte2 = buffer.readu8(Input, InputIndex + 2)
		local KeyB = (KeyA // 256) + Byte2 * 256
		buffer.writeu32(
			Output,
			OutputIndex,
			buffer.readu16(EncodeA, KeyA * 2) + buffer.readu16(EncodeB, KeyB * 2) * 0x10000
		)
		InputIndex += 3
		OutputIndex += 4
	end

	local Remainder = InputLength - InputIndex
	if Remainder == 1 then
		local Byte0 = buffer.readu8(Input, InputIndex)
		local Sextet0 = Byte0 // 4
		local Sextet1 = (Byte0 % 4) * 16
		local Pair = buffer.readu8(AlphabetBytes, Sextet0) + buffer.readu8(AlphabetBytes, Sextet1) * 256
		buffer.writeu16(Output, OutputIndex, Pair)
		buffer.writeu8(Output, OutputIndex + 2, PADDING_CHARACTER)
		buffer.writeu8(Output, OutputIndex + 3, PADDING_CHARACTER)
	elseif Remainder == 2 then
		local Byte0 = buffer.readu8(Input, InputIndex)
		local Byte1 = buffer.readu8(Input, InputIndex + 1)
		local KeyA = Byte0 + Byte1 * 256
		buffer.writeu16(Output, OutputIndex, buffer.readu16(EncodeA, KeyA * 2))
		local Sextet2 = (Byte1 % 16) * 4
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(AlphabetBytes, Sextet2))
		buffer.writeu8(Output, OutputIndex + 3, PADDING_CHARACTER)
	end

	return Output
end

local function Decode(Input: buffer): buffer
	local InputLength = buffer.len(Input)
	if InputLength == 0 then
		return buffer.create(0)
	end

	local Padding = 0
	if buffer.readu8(Input, InputLength - 1) == PADDING_CHARACTER then
		Padding = 1
		if buffer.readu8(Input, InputLength - 2) == PADDING_CHARACTER then
			Padding = 2
		end
	end

	local Output = buffer.create((InputLength // 4) * 3 - Padding)
	local InputIndex = 0
	local OutputIndex = 0
	local BodyEnd = InputLength - 4
	local UnrollStop = BodyEnd - 8

	local DecodePair = DECODE_PAIR
	local Pair0 = PAIR_A_BYTE0
	local PairAHigh = PAIR_A_BYTE1_HIGH
	local PairBLow = PAIR_B_BYTE1_LOW
	local Pair2 = PAIR_B_BYTE2
	local Dtet = DECODE_TET

	while InputIndex <= UnrollStop do
		local Word0 = buffer.readu32(Input, InputIndex)
		local Word1 = buffer.readu32(Input, InputIndex + 4)

		local PairA0 = buffer.readu16(DecodePair, (Word0 % 65536) * 2)
		local PairB0 = buffer.readu16(DecodePair, (Word0 // 65536) * 2)
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA0))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA0) + buffer.readu8(PairBLow, PairB0))
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB0))

		local PairA1 = buffer.readu16(DecodePair, (Word1 % 65536) * 2)
		local PairB1 = buffer.readu16(DecodePair, (Word1 // 65536) * 2)
		buffer.writeu8(Output, OutputIndex + 3, buffer.readu8(Pair0, PairA1))
		buffer.writeu8(Output, OutputIndex + 4, buffer.readu8(PairAHigh, PairA1) + buffer.readu8(PairBLow, PairB1))
		buffer.writeu8(Output, OutputIndex + 5, buffer.readu8(Pair2, PairB1))

		InputIndex += 8
		OutputIndex += 6
	end

	while InputIndex <= BodyEnd - 4 do
		local Word = buffer.readu32(Input, InputIndex)
		local PairA = buffer.readu16(DecodePair, (Word % 65536) * 2)
		local PairB = buffer.readu16(DecodePair, (Word // 65536) * 2)
		buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
		buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + buffer.readu8(PairBLow, PairB))
		buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB))
		InputIndex += 4
		OutputIndex += 3
	end

	do
		local Char0 = buffer.readu8(Input, InputIndex)
		local Char1 = buffer.readu8(Input, InputIndex + 1)
		local Char2 = buffer.readu8(Input, InputIndex + 2)
		local Char3 = buffer.readu8(Input, InputIndex + 3)
		local Decoded0 = buffer.readu8(Dtet, Char0)
		local Decoded1 = buffer.readu8(Dtet, Char1)

		if Char2 == PADDING_CHARACTER then
			local PairA = Decoded0 * 64 + Decoded1
			buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
		elseif Char3 == PADDING_CHARACTER then
			local Decoded2 = buffer.readu8(Dtet, Char2)
			local PairA = Decoded0 * 64 + Decoded1
			local PairB = Decoded2 * 64
			buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
			buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + (PairB // 256))
		else
			local Decoded2 = buffer.readu8(Dtet, Char2)
			local Decoded3 = buffer.readu8(Dtet, Char3)
			local PairA = Decoded0 * 64 + Decoded1
			local PairB = Decoded2 * 64 + Decoded3
			buffer.writeu8(Output, OutputIndex, buffer.readu8(Pair0, PairA))
			buffer.writeu8(Output, OutputIndex + 1, buffer.readu8(PairAHigh, PairA) + buffer.readu8(PairBLow, PairB))
			buffer.writeu8(Output, OutputIndex + 2, buffer.readu8(Pair2, PairB))
		end
	end

	return Output
end

return {
	Encode = Encode,
	Decode = Decode,
}


--[=[
	Cryptography library: Blake3

	Sizes:
		Key: 32 bytes
		Output: variable

	Return type: string (hex)
	Example usage:
		local Message = buffer.fromstring("Hello World")
		local Key = buffer.fromstring(string.rep("k", 32))

		--------Standard Hash--------
		local Hash = Blake3.Digest(Message, 32)

		--------Keyed Hash--------
		local KeyedHash = Blake3.DigestKeyed(Key, Message, 32)

		--------Key Derivation--------
		local Context = buffer.fromstring("my context")
		local KeyDeriver = Blake3.DeriveKey(Context)
		local DerivedKey = KeyDeriver(Message, 32)
--]=]

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

local Blake3 = {}

local BLOCK_SIZE = 64
local CV_SIZE = 32
local EXTENDED_CV_SIZE = 64
local MAX_STACK_DEPTH = 64
local STACK_BUFFER_SIZE = MAX_STACK_DEPTH * CV_SIZE

local BLAKE3_KEY_SIZE = 32
local BLAKE3_MIN_OUTPUT_BYTES = 1
local BLAKE3_MAX_OUTPUT_BYTES = 2 ^ 32 - 1

local CHUNK_START = 0x01
local CHUNK_END = 0x02
local PARENT_FLAG = 0x04
local ROOT_FLAG = 0x08
local HASH_FLAG = 0x10
local DERIVE_CONTEXT_FLAG = 0x20
local DERIVE_MATERIAL_FLAG = 0x40

local INITIAL_VECTORS = buffer.create(CV_SIZE)
do
	local IV = {
		0x6a09e667,
		0xbb67ae85,
		0x3c6ef372,
		0xa54ff53a,
		0x510e527f,
		0x9b05688c,
		0x1f83d9ab,
		0x5be0cd19,
	}
	for Index, Value in ipairs(IV) do
		buffer.writeu32(INITIAL_VECTORS, (Index - 1) * 4, Value)
	end
end

local ENCODE_LOOKUP = buffer.create(256 * 2)
do
	local HexChars = "0123456789abcdef"
	for Byte = 0, 255 do
		local HighNibble = bit32.rshift(Byte, 4)
		local LowNibble = Byte % 16

		local HighChar = string.byte(HexChars, HighNibble + 1)
		local LowChar = string.byte(HexChars, LowNibble + 1)

		local Combined = HighChar + bit32.lshift(LowChar, 8)
		buffer.writeu16(ENCODE_LOOKUP, Byte * 2, Combined)
	end
end

local function Compress(
	Hash: buffer,
	MessageBlock: buffer,
	Counter: number,
	V14: number,
	V15: number,
	IsFull: boolean?
): buffer
	local Hash00 = buffer.readu32(Hash, 0)
	local Hash01 = buffer.readu32(Hash, 4)
	local Hash02 = buffer.readu32(Hash, 8)
	local Hash03 = buffer.readu32(Hash, 12)
	local Hash04 = buffer.readu32(Hash, 16)
	local Hash05 = buffer.readu32(Hash, 20)
	local Hash06 = buffer.readu32(Hash, 24)
	local Hash07 = buffer.readu32(Hash, 28)

	local V00, V01, V02, V03 = Hash00, Hash01, Hash02, Hash03
	local V04, V05, V06, V07 = Hash04, Hash05, Hash06, Hash07
	local V08, V09, V10, V11 = 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a

	local V12 = Counter % (2 ^ 32)
	local V13 = (Counter - V12) * (2 ^ -32)

	local M00 = buffer.readu32(MessageBlock, 0)
	local M01 = buffer.readu32(MessageBlock, 4)
	local M02 = buffer.readu32(MessageBlock, 8)
	local M03 = buffer.readu32(MessageBlock, 12)
	local M04 = buffer.readu32(MessageBlock, 16)
	local M05 = buffer.readu32(MessageBlock, 20)
	local M06 = buffer.readu32(MessageBlock, 24)
	local M07 = buffer.readu32(MessageBlock, 28)
	local M08 = buffer.readu32(MessageBlock, 32)
	local M09 = buffer.readu32(MessageBlock, 36)
	local M10 = buffer.readu32(MessageBlock, 40)
	local M11 = buffer.readu32(MessageBlock, 44)
	local M12 = buffer.readu32(MessageBlock, 48)
	local M13 = buffer.readu32(MessageBlock, 52)
	local M14 = buffer.readu32(MessageBlock, 56)
	local M15 = buffer.readu32(MessageBlock, 60)

	local XOR = bit32.bxor
	local LEFTROTATE = bit32.lrotate

	local Temp
	for Index = 1, 7 do
		V00 += V04 + M00
		V12 = LEFTROTATE(XOR(V12, V00), 16)
		V08 += V12
		V04 = LEFTROTATE(XOR(V04, V08), 20)
		V00 += V04 + M01
		V12 = LEFTROTATE(XOR(V12, V00), 24)
		V08 += V12
		V04 = LEFTROTATE(XOR(V04, V08), 25)

		V01 += V05 + M02
		V13 = LEFTROTATE(XOR(V13, V01), 16)
		V09 += V13
		V05 = LEFTROTATE(XOR(V05, V09), 20)
		V01 += V05 + M03
		V13 = LEFTROTATE(XOR(V13, V01), 24)
		V09 += V13
		V05 = LEFTROTATE(XOR(V05, V09), 25)

		V02 += V06 + M04
		V14 = LEFTROTATE(XOR(V14, V02), 16)
		V10 += V14
		V06 = LEFTROTATE(XOR(V06, V10), 20)
		V02 += V06 + M05
		V14 = LEFTROTATE(XOR(V14, V02), 24)
		V10 += V14
		V06 = LEFTROTATE(XOR(V06, V10), 25)

		V03 += V07 + M06
		V15 = LEFTROTATE(XOR(V15, V03), 16)
		V11 += V15
		V07 = LEFTROTATE(XOR(V07, V11), 20)
		V03 += V07 + M07
		V15 = LEFTROTATE(XOR(V15, V03), 24)
		V11 += V15
		V07 = LEFTROTATE(XOR(V07, V11), 25)

		V00 += V05 + M08
		V15 = LEFTROTATE(XOR(V15, V00), 16)
		V10 += V15
		V05 = LEFTROTATE(XOR(V05, V10), 20)
		V00 += V05 + M09
		V15 = LEFTROTATE(XOR(V15, V00), 24)
		V10 += V15
		V05 = LEFTROTATE(XOR(V05, V10), 25)

		V01 += V06 + M10
		V12 = LEFTROTATE(XOR(V12, V01), 16)
		V11 += V12
		V06 = LEFTROTATE(XOR(V06, V11), 20)
		V01 += V06 + M11
		V12 = LEFTROTATE(XOR(V12, V01), 24)
		V11 += V12
		V06 = LEFTROTATE(XOR(V06, V11), 25)

		V02 += V07 + M12
		V13 = LEFTROTATE(XOR(V13, V02), 16)
		V08 += V13
		V07 = LEFTROTATE(XOR(V07, V08), 20)
		V02 += V07 + M13
		V13 = LEFTROTATE(XOR(V13, V02), 24)
		V08 += V13
		V07 = LEFTROTATE(XOR(V07, V08), 25)

		V03 += V04 + M14
		V14 = LEFTROTATE(XOR(V14, V03), 16)
		V09 += V14
		V04 = LEFTROTATE(XOR(V04, V09), 20)
		V03 += V04 + M15
		V14 = LEFTROTATE(XOR(V14, V03), 24)
		V09 += V14
		V04 = LEFTROTATE(XOR(V04, V09), 25)

		if Index ~= 7 then
			Temp = M02
			M02 = M03
			M03 = M10
			M10 = M12
			M12 = M09
			M09 = M11
			M11 = M05
			M05 = M00
			M00 = Temp

			Temp = M06
			M06 = M04
			M04 = M07
			M07 = M13
			M13 = M14
			M14 = M15
			M15 = M08
			M08 = M01
			M01 = Temp
		end
	end

	if IsFull then
		local Result = buffer.create(EXTENDED_CV_SIZE)
		buffer.writeu32(Result, 0, XOR(V00, V08))
		buffer.writeu32(Result, 4, XOR(V01, V09))
		buffer.writeu32(Result, 8, XOR(V02, V10))
		buffer.writeu32(Result, 12, XOR(V03, V11))
		buffer.writeu32(Result, 16, XOR(V04, V12))
		buffer.writeu32(Result, 20, XOR(V05, V13))
		buffer.writeu32(Result, 24, XOR(V06, V14))
		buffer.writeu32(Result, 28, XOR(V07, V15))

		buffer.writeu32(Result, 32, XOR(V08, Hash00))
		buffer.writeu32(Result, 36, XOR(V09, Hash01))
		buffer.writeu32(Result, 40, XOR(V10, Hash02))
		buffer.writeu32(Result, 44, XOR(V11, Hash03))
		buffer.writeu32(Result, 48, XOR(V12, Hash04))
		buffer.writeu32(Result, 52, XOR(V13, Hash05))
		buffer.writeu32(Result, 56, XOR(V14, Hash06))
		buffer.writeu32(Result, 60, XOR(V15, Hash07))

		return Result
	else
		local Result = buffer.create(CV_SIZE)
		buffer.writeu32(Result, 0, XOR(V00, V08))
		buffer.writeu32(Result, 4, XOR(V01, V09))
		buffer.writeu32(Result, 8, XOR(V02, V10))
		buffer.writeu32(Result, 12, XOR(V03, V11))
		buffer.writeu32(Result, 16, XOR(V04, V12))
		buffer.writeu32(Result, 20, XOR(V05, V13))
		buffer.writeu32(Result, 24, XOR(V06, V14))
		buffer.writeu32(Result, 28, XOR(V07, V15))

		return Result
	end
end

local function ProcessMessage(InitialHashVector: buffer, Flags: number, Message: buffer, Length: number): buffer
	local MessageLength = buffer.len(Message)
	local StateCvs = buffer.create(STACK_BUFFER_SIZE)
	local StateCv = buffer.create(CV_SIZE)

	local StackSize = 0
	local StateCounter = 0

	local StateChunkNumber = 0
	local StateEndFlag = 0

	local BlockSize = BLOCK_SIZE
	local CvSize = CV_SIZE
	local ExtendedCvSize = EXTENDED_CV_SIZE

	local StateStartFlag = CHUNK_START
	local ChunkStart = CHUNK_START
	local ChunkEnd = CHUNK_END

	local ParentFlag = PARENT_FLAG
	local RootFlag = ROOT_FLAG
	local FlagsParent = Flags + ParentFlag

	local BlockBuffer = buffer.create(BlockSize)
	local PopCv = buffer.create(CvSize)
	local MergeBlock = buffer.create(ExtendedCvSize)
	local StackCv = buffer.create(CvSize)
	local Block = buffer.create(ExtendedCvSize)

	buffer.copy(StateCv, 0, InitialHashVector, 0, CvSize)

	for BlockOffset = 0, MessageLength - BlockSize - 1, BlockSize do
		buffer.copy(BlockBuffer, 0, Message, BlockOffset, BlockSize)
		local StateFlags = Flags + StateStartFlag + StateEndFlag
		StateCv = Compress(StateCv, BlockBuffer, StateCounter, BlockSize, StateFlags)
		StateStartFlag = 0
		StateChunkNumber += 1

		if StateChunkNumber == 15 then
			StateEndFlag = ChunkEnd
		elseif StateChunkNumber == 16 then
			local MergeCv = StateCv
			local MergeAmount = StateCounter + 1

			while MergeAmount % 2 == 0 do
				StackSize -= 1
				buffer.copy(PopCv, 0, StateCvs, StackSize * CvSize, CvSize)
				buffer.copy(MergeBlock, 0, PopCv, 0, CvSize)
				buffer.copy(MergeBlock, CvSize, MergeCv, 0, CvSize)
				MergeCv = Compress(InitialHashVector, MergeBlock, 0, BlockSize, FlagsParent)
				MergeAmount /= 2
			end

			buffer.copy(StateCvs, StackSize * CvSize, MergeCv, 0, CvSize)
			StackSize += 1

			buffer.copy(StateCv, 0, InitialHashVector, 0, CvSize)
			StateStartFlag = ChunkStart
			StateCounter += 1
			StateChunkNumber = 0
			StateEndFlag = 0
		end
	end

	local LastLength = MessageLength == 0 and 0 or ((MessageLength - 1) % BlockSize + 1)
	local PaddedMessage = buffer.create(BlockSize)
	if LastLength > 0 then
		buffer.copy(PaddedMessage, 0, Message, MessageLength - LastLength, LastLength)
	end

	local OutputCv: buffer
	local OutputBlock: buffer
	local OutputLength: number
	local OutputFlags: number

	if StateCounter > 0 then
		local StateFlags = Flags + StateStartFlag + ChunkEnd
		local MergeCv = Compress(StateCv, PaddedMessage, StateCounter, LastLength, StateFlags)
		for Index = StackSize, 2, -1 do
			buffer.copy(StackCv, 0, StateCvs, (Index - 1) * CvSize, CvSize)
			buffer.copy(Block, 0, StackCv, 0, CvSize)
			buffer.copy(Block, CvSize, MergeCv, 0, CvSize)
			MergeCv = Compress(InitialHashVector, Block, 0, BlockSize, FlagsParent)
		end

		OutputCv = InitialHashVector
		local FirstStackCv = buffer.create(CvSize)
		buffer.copy(FirstStackCv, 0, StateCvs, 0, CvSize)
		OutputBlock = buffer.create(ExtendedCvSize)

		buffer.copy(OutputBlock, 0, FirstStackCv, 0, CvSize)
		buffer.copy(OutputBlock, CvSize, MergeCv, 0, CvSize)

		OutputLength = BlockSize
		OutputFlags = Flags + RootFlag + ParentFlag
	else
		OutputCv = StateCv
		OutputBlock = PaddedMessage
		OutputLength = LastLength
		OutputFlags = Flags + StateStartFlag + ChunkEnd + RootFlag
	end

	local Output = buffer.create(Length)
	local OutputOffset = 0
	for Index = 0, Length // BlockSize do
		local MessageDigest = Compress(OutputCv, OutputBlock, Index, OutputLength, OutputFlags, true)
		local BytesToCopy = math.min(BlockSize, Length - OutputOffset)
		buffer.copy(Output, OutputOffset, MessageDigest, 0, BytesToCopy)
		OutputOffset += BytesToCopy
		if OutputOffset >= Length then
			break
		end
	end

	return Output
end

local function ToHex(Buffer: buffer): string
	local Length = buffer.len(Buffer)
	local Hex = buffer.create(Length * 2)

	local Lookup = ENCODE_LOOKUP

	local Leftover = Length % 8
	local HexCursor = 0

	for Index = 0, Length - Leftover - 1, 8 do
		local Hex1 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index) * 2)
		local Hex2 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 1) * 2)
		local Hex3 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 2) * 2)
		local Hex4 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 3) * 2)
		local Hex5 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 4) * 2)
		local Hex6 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 5) * 2)
		local Hex7 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 6) * 2)
		local Hex8 = buffer.readu16(Lookup, buffer.readu8(Buffer, Index + 7) * 2)

		buffer.writeu16(Hex, HexCursor, Hex1)
		buffer.writeu16(Hex, HexCursor + 2, Hex2)
		buffer.writeu16(Hex, HexCursor + 4, Hex3)
		buffer.writeu16(Hex, HexCursor + 6, Hex4)
		buffer.writeu16(Hex, HexCursor + 8, Hex5)
		buffer.writeu16(Hex, HexCursor + 10, Hex6)
		buffer.writeu16(Hex, HexCursor + 12, Hex7)
		buffer.writeu16(Hex, HexCursor + 14, Hex8)

		HexCursor += 16
	end

	for Index = Length - Leftover, Length - 1 do
		local HexPair = buffer.readu16(Lookup, buffer.readu8(Buffer, Index) * 2)
		buffer.writeu16(Hex, HexCursor, HexPair)
		HexCursor += 2
	end

	return buffer.tostring(Hex)
end

function Blake3.Digest(Message: buffer, Length: number?): buffer
	if Message == nil then
		error("Message cannot be nil", 2)
	end

	if typeof(Message) ~= "buffer" then
		error(`Message must be a buffer, got {typeof(Message)}`, 2)
	end

	if Length then
		if typeof(Length) ~= "number" then
			error(`Length must be a number, got {typeof(Length)}`, 2)
		end

		if Length ~= math.floor(Length) then
			error(`Length must be an integer, got {Length}`, 2)
		end

		if Length < BLAKE3_MIN_OUTPUT_BYTES then
			error(`Length must be at least {BLAKE3_MIN_OUTPUT_BYTES} byte, got {Length} bytes`, 2)
		end

		if Length > BLAKE3_MAX_OUTPUT_BYTES then
			error(`Length must be at most {BLAKE3_MAX_OUTPUT_BYTES} bytes, got {Length} bytes`, 2)
		end
	end

	local Result = ProcessMessage(INITIAL_VECTORS, 0, Message, Length or 32)

	return Result
end

function Blake3.DigestKeyed(Key: buffer, Message: buffer, Length: number?): buffer
	if Key == nil then
		error("Key cannot be nil", 2)
	end

	if typeof(Key) ~= "buffer" then
		error(`Key must be a buffer, got {typeof(Key)}`, 2)
	end

	local KeyLength = buffer.len(Key)
	if KeyLength ~= BLAKE3_KEY_SIZE then
		error(`Key must be exactly {BLAKE3_KEY_SIZE} bytes long, got {KeyLength} bytes`, 2)
	end

	if Message == nil then
		error("Message cannot be nil", 2)
	end
	if typeof(Message) ~= "buffer" then
		error(`Message must be a buffer, got {typeof(Message)}`, 2)
	end

	if Length then
		if typeof(Length) ~= "number" then
			error(`Length must be a number, got {typeof(Length)}`, 2)
		end
		if Length ~= math.floor(Length) then
			error(`Length must be an integer, got {Length}`, 2)
		end
		if Length < BLAKE3_MIN_OUTPUT_BYTES then
			error(`Length must be at least {BLAKE3_MIN_OUTPUT_BYTES} byte, got {Length} bytes`, 2)
		end
		if Length > BLAKE3_MAX_OUTPUT_BYTES then
			error(`Length must be at most {BLAKE3_MAX_OUTPUT_BYTES} bytes, got {Length} bytes`, 2)
		end
	end

	local Result = ProcessMessage(Key, HASH_FLAG, Message, Length or 32)
	return Result
end

function Blake3.DeriveKey(Context: buffer): (buffer, number?) -> (string, buffer)
	if Context == nil then
		error("Context cannot be nil", 2)
	end

	if typeof(Context) ~= "buffer" then
		error(`Context must be a buffer, got {typeof(Context)}`, 2)
	end

	local ContextHash = ProcessMessage(INITIAL_VECTORS, DERIVE_CONTEXT_FLAG, Context, 32)

	return function(Material: buffer, Length: number?): (string, buffer)
		if Material == nil then
			error("Material cannot be nil", 2)
		end

		if typeof(Material) ~= "buffer" then
			error(`Material must be a buffer, got {typeof(Material)}`, 2)
		end

		if Length then
			if typeof(Length) ~= "number" then
				error(`Length must be a number, got {typeof(Length)}`, 2)
			end

			if Length ~= math.floor(Length) then
				error(`Length must be an integer, got {Length}`, 2)
			end

			if Length < BLAKE3_MIN_OUTPUT_BYTES then
				error(`Length must be at least {BLAKE3_MIN_OUTPUT_BYTES} byte, got {Length} bytes`, 2)
			end

			if Length > BLAKE3_MAX_OUTPUT_BYTES then
				error(`Length must be at most {BLAKE3_MAX_OUTPUT_BYTES} bytes, got {Length} bytes`, 2)
			end
		end

		local Result = ProcessMessage(ContextHash, DERIVE_MATERIAL_FLAG, Material, Length or 32)

		return ToHex(Result), Result
	end
end

return Blake3


local ipairs = ipairs
local table = table

return function(enumItems)
	local enum = {}
	for index, name in ipairs(enumItems) do
		enum[name] = index - 1
		enum[index - 1] = name
	end

	if _G.dev then
		setmetatable(enum, {
			__index = function(_, index)
				return error(`"{index}" is not a valid enum item.`, 2)
			end,
		})
	end

	return table.freeze(enum)
end


local Functions = require("@shared/functions")

local getInstanceName = Functions.getInstanceName
local getFunctionName = Functions.getFunctionName

local Errors = {}

function Errors.cannotAccess(instance: Instance, capability: string?): string
	if capability then
		return `The current thread cannot access '{getInstanceName(instance)}' (lacking capability {capability})`
	else
		return `The current thread cannot access '{getInstanceName(instance)}'`
	end
end

function Errors.cannotCall(func: () -> () | string, capability: string?): string
	local name = if type(func) == "string" then func else (getFunctionName(func) or "")
	if capability then
		return `The current thread cannot call '{name}' (lacking capability {capability})`
	else
		return `The current thread cannot call '{name}'`
	end
end

function Errors.cannotAssetRequire(): string
	return `The current thread cannot require using asset ID (lacking capability AssetRequire)`
end

function Errors.lockedParent(instance: Instance): string
	return `The Parent property of {getInstanceName(instance)} is locked`
end

function Errors.lockedParentSet(instance: Instance, newParent: Instance?): string
	return `The Parent property of {getInstanceName(instance)} is locked, current parent: {getInstanceName(
		instance.Parent
	)}, new parent: {getInstanceName(newParent)}`
end

function Errors.parentToWriteProtected(parent: Instance): string
	return `Cannot set Parent property to {getInstanceName(parent)}`
end

function Errors.createBlockedClass(class: string): string
	return `Unable to create an Instance of type {class}`
end

function Errors.connectNonFunction(): string
	return `Attempt to connect failed: Passed value is not a function`
end

function Errors.scriptTimeout(allowedExecutionTime: number): string
	return `Script timeout: exhausted allowed execution time ({allowedExecutionTime})`
end

return table.freeze(Errors)


-- Maybe this should be rewritten to have each function in it's own file?
local string = string
local game = game
local coroutine = coroutine
local debug = debug
local table = table

local CSPRNG = require("@shared/crypto/CSPRNG")

local Functions = {}

function Functions.empty() end

function Functions.randomString(length: number?): string
	return CSPRNG.RandomString(length or 32, false)
end

Functions.randomInstanceName = CSPRNG.Random

-- This function makes the string "null terminated" (stops before a null character)
function Functions.nullTerminate(str: string): string
	local nullIndex = string.find(str, "\0", 1, true)
	if nullIndex then
		return string.sub(str, 1, nullIndex - 1)
	end

	return str
end

function Functions.toCString(value: any): string
	return Functions.nullTerminate(tostring(value))
end

function Functions.toCDouble(value: any): number?
	local datatype = type(value)
	if datatype == "number" then
		return value
	elseif datatype == "boolean" then
		return if value then 1 else 0
	end

	return tonumber(value)
end

function Functions.getInstanceName(instance: Instance?): string
	return if instance ~= nil then instance.Name else "NULL"
end

function Functions.getFunctionName(func: () -> ()): string?
	local name = debug.info(func, "n")
	return if name and #name == 0 then nil else name
end

do
	local indexThread = coroutine.create(function()
		return game[true]
	end)
	local newindexThread = coroutine.create(function()
		game[true] = true
	end)

	-- Coroutine.resume doesn't omit errors unlike task.spawn
	coroutine.resume(indexThread)
	coroutine.resume(newindexThread)

	Functions.indexInstance = debug.info(indexThread, 0, "f")
	Functions.newindexInstance = debug.info(newindexThread, 0, "f")
end

function Functions.formatPlayerName(player: Player): string
	if player.DisplayName == player.Name then
		return player.DisplayName
	end

	return `{player.DisplayName} (@{player.Name})`
end

function Functions.getServerType(): "StandardServer" | "ReservedServer" | "VIPServer"
	if game.PrivateServerId ~= "" then
		if game.PrivateServerOwnerId ~= 0 then
			return "VIPServer"
		else
			return "ReservedServer"
		end
	else
		return "StandardServer"
	end
end

return table.freeze(Functions)


local table = table
local print = print
local warn = warn
local error = error

local TestService = game:GetService("TestService")
local prefix

local Log = {}

function Log:SetPrefix(_prefix)
	if prefix then
		return error("Prefix is already set.", 2)
	end

	prefix = _prefix
end

function Log.debug(...: any)
	if _G.debugging then
		TestService:Message(prefix .. " " .. table.concat({ ... }, " "))
	end
end

function Log.print(...: any)
	print(prefix, ...)
end

function Log.warn(...: any)
	warn(prefix, ...)
end

return Log


--!native
--!strict
local msgpack = {}

local band = bit32.band
local bor = bit32.bor
local bufferCreate = buffer.create
local bufferLen = buffer.len
local bufferCopy = buffer.copy
local readstring = buffer.readstring
local writestring = buffer.writestring
local readu8 = buffer.readu8
local readi8 = buffer.readi8
local writeu8 = buffer.writeu8
local writei8 = buffer.writei8
local lshift = bit32.lshift
local extract = bit32.extract
local ldexp = math.ldexp
local frexp = math.frexp
local floor = math.floor
local modf = math.modf
local sign = math.sign
local ssub = string.sub
local char = string.char
local sbyte = string.byte
local concat = table.concat
local tableCreate = table.create

-- MsgPack numbers are big-endian, buffer methods are little-endian
-- Will need to reverse 16 and 32 bit ints, floats and doubles
local function reverse(b: buffer, offset: number, count: number): ()
	for i = 1, count // 2 do
		local byte = readu8(b, offset + i - 1)
		bufferCopy(b, offset + i - 1, b, offset + count - i, 1)
		writeu8(b, offset + count - i, byte)
	end
end

local function writeu16(b: buffer, offset: number, value: number): ()
	buffer.writeu16(b, offset, value)
	reverse(b, offset, 2)
end

local function writei16(b: buffer, offset: number, value: number): ()
	buffer.writei16(b, offset, value)
	reverse(b, offset, 2)
end

local function writeu32(b: buffer, offset: number, value: number): ()
	buffer.writeu32(b, offset, value)
	reverse(b, offset, 4)
end

local function writei32(b: buffer, offset: number, value: number): ()
	buffer.writei32(b, offset, value)
	reverse(b, offset, 4)
end

local function writef32(b: buffer, offset: number, value: number): ()
	buffer.writef32(b, offset, value)
	reverse(b, offset, 4)
end

local function writef64(b: buffer, offset: number, value: number): ()
	buffer.writef64(b, offset, value)
	reverse(b, offset, 8)
end

local function readu16(b: buffer, offset: number): number
	reverse(b, offset, 2)
	return buffer.readu16(b, offset)
end

local function readi16(b: buffer, offset: number): number
	reverse(b, offset, 2)
	return buffer.readi16(b, offset)
end

local function readu32(b: buffer, offset: number): number
	reverse(b, offset, 4)
	return buffer.readu32(b, offset)
end

local function readi32(b: buffer, offset: number): number
	reverse(b, offset, 4)
	return buffer.readi32(b, offset)
end

local function readf32(b: buffer, offset: number): number
	reverse(b, offset, 4)
	return buffer.readf32(b, offset)
end

local function readf64(b: buffer, offset: number): number
	reverse(b, offset, 8)
	return buffer.readf64(b, offset)
end

local function parse(message: buffer, offset: number): (any, number)
	local byte = readu8(message, offset)

	if byte == 0xC0 then -- nil
		return nil, offset + 1
	elseif byte == 0xC2 then -- false
		return false, offset + 1
	elseif byte == 0xC3 then -- true
		return true, offset + 1
	elseif byte == 0xC4 then -- bin 8
		local length = readu8(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 2, length)
		return newBuf, offset + 2 + length
	elseif byte == 0xC5 then -- bin 16
		local length = readu16(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 3, length)
		return newBuf, offset + 3 + length
	elseif byte == 0xC6 then -- bin 32
		local length = readu32(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 5, length)
		return newBuf, offset + 5 + length
	elseif byte == 0xC7 then -- ext 8
		local length = readu8(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 3, length)
		return msgpack.Extension.new(readu8(message, offset + 2), newBuf), offset + 3 + length
	elseif byte == 0xC8 then -- ext 16
		local length = readu16(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 4, length)
		return msgpack.Extension.new(readu8(message, offset + 3), newBuf), offset + 4 + length
	elseif byte == 0xC9 then -- ext 32
		local length = readu32(message, offset + 1)
		local newBuf = bufferCreate(length)
		bufferCopy(newBuf, 0, message, offset + 6, length)
		return msgpack.Extension.new(readu8(message, offset + 5), newBuf), offset + 6 + length
	elseif byte == 0xCA then -- float 32
		return readf32(message, offset + 1), offset + 5
	elseif byte == 0xCB then -- float 64
		return readf64(message, offset + 1), offset + 9
	elseif byte == 0xCC then -- uint 8
		return readu8(message, offset + 1), offset + 2
	elseif byte == 0xCD then -- uint 16
		return readu16(message, offset + 1), offset + 3
	elseif byte == 0xCE then -- uint 32
		return readu32(message, offset + 1), offset + 5
	elseif byte == 0xCF then -- uint 64
		return msgpack.UInt64.new(readu32(message, offset + 1), readu32(message, offset + 5)), offset + 9
	elseif byte == 0xD0 then -- int 8
		return readi8(message, offset + 1), offset + 2
	elseif byte == 0xD1 then -- int 16
		return readi16(message, offset + 1), offset + 3
	elseif byte == 0xD2 then -- int 32
		return readi32(message, offset + 1), offset + 5
	elseif byte == 0xD3 then -- int 64
		return msgpack.Int64.new(readu32(message, offset + 1), readu32(message, offset + 5)), offset + 9
	elseif byte == 0xD4 then -- fixext 1
		local newBuf = bufferCreate(1)
		bufferCopy(newBuf, 0, message, offset + 2, 1)
		return msgpack.Extension.new(readu8(message, offset + 1), newBuf), offset + 3
	elseif byte == 0xD5 then -- fixext 2
		local newBuf = bufferCreate(2)
		bufferCopy(newBuf, 0, message, offset + 2, 2)
		return msgpack.Extension.new(readu8(message, offset + 1), newBuf), offset + 4
	elseif byte == 0xD6 then -- fixext 4
		local newBuf = bufferCreate(4)
		bufferCopy(newBuf, 0, message, offset + 2, 4)
		return msgpack.Extension.new(readu8(message, offset + 1), newBuf), offset + 6
	elseif byte == 0xD7 then -- fixext 8
		local newBuf = bufferCreate(8)
		bufferCopy(newBuf, 0, message, offset + 2, 8)
		return msgpack.Extension.new(readu8(message, offset + 1), newBuf), offset + 10
	elseif byte == 0xD8 then -- fixext 16
		local newBuf = bufferCreate(16)
		bufferCopy(newBuf, 0, message, offset + 2, 16)
		return msgpack.Extension.new(readu8(message, offset + 1), newBuf), offset + 18
	elseif byte == 0xD9 then -- str 8
		local length = readu8(message, offset + 1)
		return readstring(message, offset + 2, length), offset + 2 + length
	elseif byte == 0xDA then -- str 16
		local length = readu16(message, offset + 1)
		return readstring(message, offset + 3, length), offset + 3 + length
	elseif byte == 0xDB then -- str 32
		local length = readu32(message, offset + 1)
		return readstring(message, offset + 5, length), offset + 5 + length
	elseif byte == 0xDC then -- array 16
		local length = readu16(message, offset + 1)
		local array = tableCreate(length)
		local newOffset = offset + 3

		for i = 1, length do
			array[i], newOffset = parse(message, newOffset)
		end

		return array, newOffset
	elseif byte == 0xDD then -- array 32
		local length = readu32(message, offset + 1)
		local array = tableCreate(length)
		local newOffset = offset + 5

		for i = 1, length do
			array[i], newOffset = parse(message, newOffset)
		end

		return array, newOffset
	elseif byte == 0xDE then -- map 16
		local length = readu16(message, offset + 1)
		local dictionary = {}
		local newOffset = offset + 3
		local key

		for _ = 1, length do
			key, newOffset = parse(message, newOffset)
			dictionary[key], newOffset = parse(message, newOffset)
		end

		return dictionary, newOffset
	elseif byte == 0xDF then -- map 32
		local length = readu32(message, offset + 1)
		local dictionary = {}
		local newOffset = offset + 5
		local key

		for _ = 1, length do
			key, newOffset = parse(message, newOffset)
			dictionary[key], newOffset = parse(message, newOffset)
		end

		return dictionary, newOffset
	elseif byte >= 0xE0 then -- negative fixint
		return byte - 256, offset + 1
	elseif byte <= 0x7F then -- positive fixint
		return byte, offset + 1
	elseif byte - 0x80 <= 0x8F - 0x80 then -- fixmap
		local length = band(byte, 0xF)
		local dictionary = {}
		local newOffset = offset + 1
		local key

		for _ = 1, length do
			key, newOffset = parse(message, newOffset)
			dictionary[key], newOffset = parse(message, newOffset)
		end

		return dictionary, newOffset
	elseif byte - 0x90 <= 0x9F - 0x90 then -- fixarray
		local length = band(byte, 0xF)
		local array = tableCreate(length)
		local newOffset = offset + 1

		for i = 1, length do
			array[i], newOffset = parse(message, newOffset)
		end

		return array, newOffset
	elseif byte - 0xA0 <= 0xBF - 0xA0 then -- fixstr
		local length = byte - 0xA0
		return readstring(message, offset + 1, length), offset + 1 + length
	end

	error("Not all decoder cases are handled, report as bug to msgpack-luau maintainer")
end

local function computeLength(data: any, tableSet: { [any]: boolean }): number
	local dtype = type(data)
	if data == nil then
		return 1
	elseif dtype == "boolean" then
		return 1
	elseif dtype == "string" then
		local length = #data

		if length <= 31 then
			return 1 + length
		elseif length <= 0xFF then
			return 2 + length
		elseif length <= 0xFFFF then
			return 3 + length
		elseif length <= 0xFFFFFFFF then
			return 5 + length
		end

		error("Could not encode - too long string")
	elseif dtype == "buffer" then
		local length = bufferLen(data)

		if length <= 0xFF then
			return 2 + length
		elseif length <= 0xFFFF then
			return 3 + length
		elseif length <= 0xFFFFFFFF then
			return 5 + length
		end

		error("Could not encode - too long binary buffer")
	elseif dtype == "number" then
		-- represents NaN, Inf, -Inf as float 32 to save space
		if data == 0 then
			return 1
		elseif data ~= data then -- NaN
			return 5
		elseif data == math.huge then
			return 5
		elseif data == -math.huge then
			return 5
		end

		local integral, fractional = modf(data)
		local sign = sign(data)

		if fractional ~= 0 or integral > 0xFFFFFFFF or integral < -0x80000000 then
			-- float 64
			return 9
		end

		if sign > 0 then
			if integral <= 127 then -- positive fixint
				return 1
			elseif integral <= 0xFF then -- uint 8
				return 2
			elseif integral <= 0xFFFF then -- uint 16
				return 3
			elseif integral <= 0xFFFFFFFF then -- uint 32
				return 5
			end
		else
			if integral >= -0x20 then -- negative fixint
				return 1
			elseif integral >= -0x80 then -- int 8
				return 2
			elseif integral >= -0x8000 then -- int 16
				return 3
			elseif integral >= -0x80000000 then -- int 32
				return 5
			end
		end

		error(string.format('Could not encode - unhandled number "%s"', typeof(data)))
	elseif dtype == "table" then
		local msgpackType = data._msgpackType

		if msgpackType then
			if msgpackType == msgpack.Int64 or msgpackType == msgpack.UInt64 then
				return 9
			elseif msgpackType == msgpack.Extension then
				local length = bufferLen(data.data)

				if length == 1 then
					return 3
				elseif length == 2 then
					return 4
				elseif length == 4 then
					return 6
				elseif length == 8 then
					return 10
				elseif length == 16 then
					return 18
				elseif length <= 0xFF then
					return 3 + length
				elseif length <= 0xFFFF then
					return 4 + length
				elseif length <= 0xFFFFFFFF then
					return 6 + length
				end

				error("Could not encode - too long extension data")
			end
		end

		if tableSet[data] then
			error("Can not serialize cyclic table")
		else
			tableSet[data] = true
		end

		local length = #data
		local mapLength = 0

		for _, _ in pairs(data) do
			mapLength += 1
		end

		local headerLen
		if mapLength <= 15 then
			headerLen = 1
		elseif mapLength <= 0xFFFF then
			headerLen = 3
		elseif mapLength <= 0xFFFFFFFF then
			headerLen = 5
		else
			if length == mapLength then
				error("Could not encode - too long array")
			else
				error("Could not encode - too long map")
			end
		end

		if length == mapLength then -- array
			local contentLen = 0
			for _, v in ipairs(data) do
				contentLen += computeLength(v, tableSet)
			end

			return headerLen + contentLen
		else -- map
			local contentLen = 0
			for k, v in pairs(data) do
				contentLen += computeLength(k, tableSet)
				contentLen += computeLength(v, tableSet)
			end

			return headerLen + contentLen
		end
	end

	error(string.format('Could not encode - unsupported datatype "%s"', typeof(data)))
end

local extensionTypeLUT = {
	[1] = 0xD4,
	[2] = 0xD5,
	[4] = 0xD6,
	[8] = 0xD7,
	[16] = 0xD8,
}

local function encode(result: buffer, offset: number, data: any): number
	local dtype = type(data)
	if data == nil then
		writestring(result, offset, "\xC0")
		return offset + 1
	elseif data == false then
		writestring(result, offset, "\xC2")
		return offset + 1
	elseif data == true then
		writestring(result, offset, "\xC3")
		return offset + 1
	elseif dtype == "string" then
		local length = #data

		if length <= 31 then
			writeu8(result, offset, bor(0xA0, length))
			writestring(result, offset + 1, data)
			return offset + 1 + length
		elseif length <= 0xFF then
			writeu8(result, offset, 0xD9)
			writeu8(result, offset + 1, length)
			writestring(result, offset + 2, data)
			return offset + 2 + length
		elseif length <= 0xFFFF then
			writeu8(result, offset, 0xDA)
			writeu16(result, offset + 1, length)
			writestring(result, offset + 3, data)
			return offset + 3 + length
		elseif length <= 0xFFFFFFFF then
			writeu8(result, offset, 0xDB)
			writeu32(result, offset + 1, length)
			writestring(result, offset + 5, data)
			return offset + 5 + length
		end

		error("Could not encode - too long string")
	elseif dtype == "buffer" then
		local length = bufferLen(data)

		if length <= 0xFF then
			writeu8(result, offset, 0xC4)
			writeu8(result, offset + 1, length)
			bufferCopy(result, offset + 2, data)
			return offset + 2 + length
		elseif length <= 0xFFFF then
			writeu8(result, offset, 0xC5)
			writeu16(result, offset + 1, length)
			bufferCopy(result, offset + 3, data)
			return offset + 3 + length
		elseif length <= 0xFFFFFFFF then
			writeu8(result, offset, 0xC6)
			writeu32(result, offset + 1, length)
			bufferCopy(result, offset + 5, data)
			return offset + 5 + length
		end

		error("Could not encode - too long binary buffer")
	elseif dtype == "number" then
		-- represents NaN, Inf, -Inf as float 32 to save space
		if data == 0 then
			writeu8(result, offset, 0)
			return offset + 1
		elseif data ~= data then -- NaN
			writestring(result, offset, "\xCA\x7F\x80\x00\x01")
			return offset + 5
		elseif data == math.huge then
			writestring(result, offset, "\xCA\x7F\x80\x00\x00")
			return offset + 5
		elseif data == -math.huge then
			writestring(result, offset, "\xCA\xFF\x80\x00\x00")
			return offset + 5
		end

		local integral, fractional = modf(data)
		local sign = sign(data)

		if fractional ~= 0 or integral > 0xFFFFFFFF or integral < -0x80000000 then
			-- float 64
			writeu8(result, offset, 0xCB)
			writef64(result, offset + 1, data)
			return offset + 9
		end

		if sign > 0 then
			if integral <= 127 then -- positive fixint
				writeu8(result, offset, integral)
				return offset + 1
			elseif integral <= 0xFF then -- uint 8
				writeu8(result, offset, 0xCC)
				writeu8(result, offset + 1, integral)
				return offset + 2
			elseif integral <= 0xFFFF then -- uint 16
				writeu8(result, offset, 0xCD)
				writeu16(result, offset + 1, integral)
				return offset + 3
			elseif integral <= 0xFFFFFFFF then -- uint 32
				writeu8(result, offset, 0xCE)
				writeu32(result, offset + 1, integral)
				return offset + 5
			end
		else
			if integral >= -0x20 then -- negative fixint
				writeu8(result, offset, bor(0xE0, extract(integral, 0, 5)))
				return offset + 1
			elseif integral >= -0x80 then -- int 8
				writeu8(result, offset, 0xD0)
				writei8(result, offset + 1, integral)
				return offset + 2
			elseif integral >= -0x8000 then -- int 16
				writeu8(result, offset, 0xD1)
				writei16(result, offset + 1, integral)
				return offset + 3
			elseif integral >= -0x80000000 then -- int 32
				writeu8(result, offset, 0xD2)
				writei32(result, offset + 1, integral)
				return offset + 5
			end
		end

		error(string.format('Could not encode - unhandled number "%s"', typeof(data)))
	elseif dtype == "table" then
		local msgpackType = data._msgpackType

		if msgpackType then
			if msgpackType == msgpack.Int64 or msgpackType == msgpack.UInt64 then
				local intType = if msgpackType == msgpack.UInt64 then 0xCF else 0xD3
				writeu8(result, offset, intType)
				writeu32(result, offset + 1, data.mostSignificantPart)
				writeu32(result, offset + 5, data.leastSignificantPart)
				return offset + 9
			elseif msgpackType == msgpack.Extension then
				local length = bufferLen(data.data)
				local extType = extensionTypeLUT[length]

				if extType then
					writeu8(result, offset, extType)
					writeu8(result, offset + 1, data.type)
					bufferCopy(result, offset + 2, data.data)
					return offset + 2 + length
				end

				if length <= 0xFF then
					writeu8(result, offset, 0xC7)
					writeu8(result, offset + 1, length)
					writeu8(result, offset + 2, data.type)
					bufferCopy(result, offset + 3, data.data)
					return offset + 3 + length
				elseif length <= 0xFFFF then
					writeu8(result, offset, 0xC8)
					writeu16(result, offset + 1, length)
					writeu8(result, offset + 3, data.type)
					bufferCopy(result, offset + 4, data.data)
					return offset + 4 + length
				elseif length <= 0xFFFFFFFF then
					writeu8(result, offset, 0xC9)
					writeu32(result, offset + 1, length)
					writeu8(result, offset + 5, data.type)
					bufferCopy(result, offset + 6, data.data)
					return offset + 6 + length
				end

				error("Could not encode - too long extension data")
			end
		end

		local length = #data
		local mapLength = 0

		for _, _ in pairs(data) do
			mapLength += 1
		end

		if length == mapLength then -- array
			local newOffset = offset
			if length <= 15 then
				writeu8(result, offset, bor(0x90, mapLength))
				newOffset += 1
			elseif length <= 0xFFFF then
				writeu8(result, offset, 0xDC)
				writeu16(result, offset + 1, length)
				newOffset += 3
			elseif length <= 0xFFFFFFFF then
				writeu8(result, offset, 0xDD)
				writeu32(result, offset + 1, length)
				newOffset += 5
			else
				error("Could not encode - too long array")
			end

			for _, v in ipairs(data) do
				newOffset = encode(result, newOffset, v)
			end

			return newOffset
		else -- map
			local newOffset = offset
			if mapLength <= 15 then
				writeu8(result, offset, bor(0x80, mapLength))
				newOffset += 1
			elseif mapLength <= 0xFFFF then
				writeu8(result, offset, 0xDE)
				writeu16(result, offset + 1, mapLength)
				newOffset += 3
			elseif mapLength <= 0xFFFFFFFF then
				writeu8(result, offset, 0xDF)
				writeu32(result, offset + 1, mapLength)
				newOffset += 5
			else
				error("Could not encode - too long map")
			end

			for k, v in pairs(data) do
				newOffset = encode(result, newOffset, k)
				newOffset = encode(result, newOffset, v)
			end

			return newOffset
		end
	end

	error(string.format('Could not encode - unsupported datatype "%s"', typeof(data)))
end

msgpack.Int64 = {}

function msgpack.Int64.new(mostSignificantPart: number, leastSignificantPart: number): Int64
	return {
		_msgpackType = msgpack.Int64,
		mostSignificantPart = mostSignificantPart,
		leastSignificantPart = leastSignificantPart,
	}
end

msgpack.UInt64 = {}

function msgpack.UInt64.new(mostSignificantPart: number, leastSignificantPart: number): UInt64
	return {
		_msgpackType = msgpack.UInt64,
		mostSignificantPart = mostSignificantPart,
		leastSignificantPart = leastSignificantPart,
	}
end

msgpack.Extension = {}

function msgpack.Extension.new(extensionType: number, blob: buffer): Extension
	return {
		_msgpackType = msgpack.Extension,
		type = extensionType,
		data = blob,
	}
end

function msgpack.utf8Encode(message: string): string
	local messageLength = #message
	local nBytes = math.ceil(messageLength * (8 / 7))
	local result = bufferCreate(nBytes)

	local bitPointer = 0
	for i = 1, nBytes do
		local j = 1 + floor(bitPointer / 8)
		local bitRemainder = bitPointer % 8
		local byte = sbyte(message, j)

		if bitRemainder == 0 then
			writeu8(result, i - 1, extract(byte, 1, 7))
		elseif bitRemainder == 1 then
			writeu8(result, i - 1, extract(byte, 0, 7))
		else
			local nextByte = sbyte(message, j + 1) or 0
			writeu8(
				result,
				i - 1,
				bor(
					lshift(extract(byte, 0, 8 - bitRemainder), bitRemainder - 1),
					extract(nextByte, 9 - bitRemainder, bitRemainder - 1)
				)
			)
		end

		bitPointer += 7
	end

	return buffer.tostring(result)
end

function msgpack.utf8Decode(message: string): string
	local nBytes = floor(#message * 7 / 8)
	local result = bufferCreate(nBytes)

	local bitPointer = 0
	for i = 1, nBytes do
		local bitRemainder = bitPointer % 7
		local byte = sbyte(message, 1 + floor(bitPointer / 7))
		local nextByte = sbyte(message, 2 + floor(bitPointer / 7))

		writeu8(
			result,
			i - 1,
			bor(
				lshift(extract(byte, 0, 7 - bitRemainder), bitRemainder + 1),
				extract(nextByte, 6 - bitRemainder, 1 + bitRemainder)
			)
		)

		bitPointer += 8
	end

	return buffer.tostring(result)
end

function msgpack.decode(message: buffer): any
	if buffer.len(message) == 0 then
		error("Could not decode - input string is too short")
	end
	return (parse(message, 0))
end

function msgpack.encode(data: any): buffer
	local length = computeLength(data, {})
	local result = bufferCreate(length)
	encode(result, 0, data)
	return result
end

export type Int64 = { _msgpackType: typeof(msgpack.Int64), mostSignificantPart: number, leastSignificantPart: number }
export type UInt64 = { _msgpackType: typeof(msgpack.UInt64), mostSignificantPart: number, leastSignificantPart: number }
export type Extension = { _msgpackType: typeof(msgpack.Extension), type: number, data: buffer }

return msgpack

--[[
MIT License

Copyright (c) 2024 Valts Liepiņš & Authors.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]


--!strict

local MessagePack = require("@shared/msgpack")
local XXH32 = require("@shared/crypto/XXH32")

-- thank you rojo
local CFRAME_ID_LOOKUP_TABLE = table.freeze({
	[0x02] = CFrame.fromEulerAnglesYXZ(0, 0, 0),
	[0x03] = CFrame.fromEulerAnglesYXZ(math.rad(90), 0, 0),
	[0x05] = CFrame.fromEulerAnglesYXZ(0, math.rad(180), math.rad(180)),
	[0x06] = CFrame.fromEulerAnglesYXZ(math.rad(-90), 0, 0),
	[0x07] = CFrame.fromEulerAnglesYXZ(0, math.rad(180), math.rad(90)),
	[0x09] = CFrame.fromEulerAnglesYXZ(0, math.rad(90), math.rad(90)),
	[0x0a] = CFrame.fromEulerAnglesYXZ(0, 0, math.rad(90)),
	[0x0c] = CFrame.fromEulerAnglesYXZ(0, math.rad(-90), math.rad(90)),
	[0x0d] = CFrame.fromEulerAnglesYXZ(math.rad(-90), math.rad(-90), 0),
	[0x0e] = CFrame.fromEulerAnglesYXZ(math.rad(0), math.rad(-90), 0),
	[0x10] = CFrame.fromEulerAnglesYXZ(math.rad(90), math.rad(-90), 0),
	[0x11] = CFrame.fromEulerAnglesYXZ(math.rad(0), math.rad(90), 180),

	[0x14] = CFrame.fromEulerAnglesYXZ(0, math.rad(180), 0),
	[0x15] = CFrame.fromEulerAnglesYXZ(math.rad(-90), math.rad(-180), 0),
	[0x17] = CFrame.fromEulerAnglesYXZ(0, 0, math.rad(180)),
	[0x18] = CFrame.fromEulerAnglesYXZ(math.rad(90), math.rad(180), 0),
	[0x19] = CFrame.fromEulerAnglesYXZ(0, 0, math.rad(-90)),
	[0x1b] = CFrame.fromEulerAnglesYXZ(0, math.rad(-90), math.rad(-90)),
	[0x1c] = CFrame.fromEulerAnglesYXZ(0, math.rad(-180), math.rad(-90)),
	[0x1e] = CFrame.fromEulerAnglesYXZ(0, math.rad(90), math.rad(-90)),
	[0x1f] = CFrame.fromEulerAnglesYXZ(math.rad(90), math.rad(90), 0),
	[0x20] = CFrame.fromEulerAnglesYXZ(0, math.rad(90), 0),
	[0x22] = CFrame.fromEulerAnglesYXZ(math.rad(-90), math.rad(90), 0),
	[0x23] = CFrame.fromEulerAnglesYXZ(0, math.rad(-90), math.rad(180)),
})

type Pair<T> = {
	encode: (datatype: T) -> buffer,
	decode: (buf: buffer) -> T,
	type: number,
}

local BASIC_DATATYPE_TO_PAIR: { [string]: Pair<any> } = {
	CFrame = {
		encode = function(datatype: CFrame)
			local position = datatype.Position
			local rotation = datatype.Rotation

			local id
			for key, value in CFRAME_ID_LOOKUP_TABLE do
				if rotation == value then
					id = key
					break
				end
			end

			local buf = buffer.create(if id then 13 else 49)

			buffer.writeu8(buf, 0, id or 0)

			if id then
				buffer.writef32(buf, 1, position.X)
				buffer.writef32(buf, 5, position.Y)
				buffer.writef32(buf, 9, position.Z)
			else
				for index, component in { datatype:GetComponents() } do
					buffer.writef32(buf, ((index - 1) * 4) + 1, component)
				end
			end

			return buf
		end,

		decode = function(buf: buffer)
			local id = buffer.readu8(buf, 0)

			if id == 0 then
				local positionX, positionY, positionZ, xx, xy, xz, yx, yy, yz, zx, zy, zz =
					buffer.readf32(buf, 1),
					buffer.readf32(buf, 5),
					buffer.readf32(buf, 9),
					buffer.readf32(buf, 13),
					buffer.readf32(buf, 17),
					buffer.readf32(buf, 21),
					buffer.readf32(buf, 25),
					buffer.readf32(buf, 29),
					buffer.readf32(buf, 33),
					buffer.readf32(buf, 37),
					buffer.readf32(buf, 41),
					buffer.readf32(buf, 45)

				return CFrame.new(positionX, positionY, positionZ, xx, xy, xz, yx, yy, yz, zx, zy, zz)
			else
				local positionX, positionY, positionZ =
					buffer.readf32(buf, 1), buffer.readf32(buf, 4), buffer.readf32(buf, 8)

				return CFrame.new(positionX, positionY, positionZ) * CFRAME_ID_LOOKUP_TABLE[id]
			end
		end,

		type = 13,
	},

	Vector3 = {
		encode = function(datatype: Vector3)
			local buf = buffer.create(12)
			buffer.writef64(buf, 0, datatype.X)
			buffer.writef64(buf, 8, datatype.Y)
			buffer.writef64(buf, 16, datatype.Z)
			return buf
		end,

		decode = function(buf: buffer)
			return Vector3.new(buffer.readf64(buf, 0), buffer.readf64(buf, 8), buffer.readf64(buf, 16))
		end,

		type = 14,
	},
}

-- Instances are specially handled
local INSTANCE_EXTENSION_TYPE: number = 12 -- 0x0c

local TYPE_EXISTS: { [number]: string } = {
	[INSTANCE_EXTENSION_TYPE] = "Instance",
}

for index, pair in BASIC_DATATYPE_TO_PAIR do
	TYPE_EXISTS[pair.type] = index
end

table.freeze(TYPE_EXISTS)

local Module = {}

-- Returns a u32
local function computeDecentChecksum(instance: Instance): number
	-- We should only use datapoints here that are actually garuanteed to be shared with the server
	-- So something like :GetFullName() might seem ok, but it's return could actually differ on the client and server (as the client could change the instance's parent or name).
	-- Only other datapoint I could think of would be the instance's creation date, but keeping track of that on all instances is expensive.
	return XXH32(buffer.fromstring(`{instance.ClassName}`))
end

-- Returns a MessagePack encoded string blob and a list of instances to be shipped alongside the blob.
-- This function MAY error, as buffer operations can fail if there is not enough memory on the server.
-- You should also wrap this function in a pcall.
function Module.encode(data: any): (buffer, { Instance })
	-- do not allow outside callers to mess with instance table
	local index = 1
	local function encode(data: any, instanceArray: { Instance }, first: boolean)
		local datatype = typeof(data)

		if datatype == "table" then
			local new = {}

			for key, value in pairs(data) do
				new[key] = encode(value, instanceArray, false)
			end

			data = new
		elseif datatype == "Instance" then
			local extensionData = buffer.create(8)
			buffer.writeu32(extensionData, 0, computeDecentChecksum(data))
			buffer.writeu32(extensionData, 4, index)

			local extension = MessagePack.Extension.new(INSTANCE_EXTENSION_TYPE, extensionData)

			table.insert(instanceArray, data :: Instance)
			index += 1

			data = extension
		else
			local pair = BASIC_DATATYPE_TO_PAIR[datatype]
			if pair then
				data = MessagePack.Extension.new(pair.type, pair.encode(data))
			end
		end

		return data, instanceArray
	end

	local dataWithoutInstances, instanceArray = encode(data, {}, true)
	return MessagePack.encode(dataWithoutInstances), instanceArray
end

-- Decodes a MessagePack blob with an instance array, returning the decoded data.
-- This function CAN and WILL error if given invalid data. Please wrap in a pcall.
function Module.decode(data: buffer, instanceArray: { Instance }): any
	local result = MessagePack.decode(data)

	local function decode(data: any)
		if typeof(data) == "table" then
			if data._msgpackType ~= nil and TYPE_EXISTS[data.type :: number] then
				if data.type == INSTANCE_EXTENSION_TYPE then
					local extensionData: buffer = data.data
					local index = buffer.readu32(extensionData, 4)
					local instance = instanceArray[index]

					if not instance then
						error("Integrity check failed: instanceArray[index] is nil", 2)
					end

					if typeof(instance) ~= "Instance" then
						error("Integrity check failed: instanceArray[index] returned a non-instance value", 2)
					end

					local checksum = buffer.readu32(extensionData, 0)
					local expected = computeDecentChecksum(instance)

					if checksum ~= expected then
						error(
							`Integrity check failed: hash check failed! expected {expected} but got {checksum} from extension data [0-4]`,
							2
						)
					end

					return instance
				else
					return BASIC_DATATYPE_TO_PAIR[TYPE_EXISTS[data.type :: number]].decode(data.data :: buffer)
				end
			end

			for key, value in pairs(data) do
				data[key] = decode(value)
			end
		end

		return data
	end

	return decode(result)
end

return table.freeze(Module)


local Enum = require("@shared/enum")

return {
	Server = Enum({
		"KeyExchange",
		"Handshake",
		"Event",
		"Invoke",
	}),
	Client = Enum({
		"Event",
		"InvokeResult",
	}),
}


-- A list of possible services that the network remotes can be parented to.

if _G.dev then
	-- While in development, it might be helpful to know where the remotes are located at.
	return { game:GetService("ReplicatedStorage") }
end

-- Some services are commented out because they either might break some scripts, or the services clone its children in some cases (StarterGui for example).
return {
	--game:GetService("Chat"),
	game:GetService("FriendService"),
	game:GetService("InsertService"),
	game:GetService("JointsService"),
	--game:GetService("Lighting"),
	game:GetService("LocalizationService"),
	game:GetService("MarketplaceService"),
	game:GetService("MaterialService"),
	--game:GetService("Players"),
	game:GetService("ProximityPromptService"),
	game:GetService("ReplicatedStorage"),
	--game:GetService("SoundService"),
	--game:GetService("StarterGui"),
	--game:GetService("StarterPack"),
	--game:GetService("StarterPlayer"),
	--game:GetService("Teams"),
	game:GetService("TestService"),
	game:GetService("TextChatService"),
	game:GetService("VoiceChatService"),
	--game:GetService("Workspace"),
}


--!strict

local Module = {}

-- as per fips 203 & 204
Module.ML_KEM_1024_PUBLIC_KEY_SIZE = 1568
Module.ML_KEM_1024_CIPHERTEXT_SIZE = 1568

local RunService = game:GetService("RunService")
local HttpService = game:GetService("HttpService")

local Blake3 = require("@shared/crypto/blake3")
local AEAD = require("@shared/crypto/AEAD")
local Encoding = require("@shared/network/encoding")

local allStaticFactors = (if RunService:IsStudio() then "00000000-0000-0000-0000-000000000000" else game.JobId)
	.. game.CreatorType.Name
	.. tostring(game.CreatorId)
	.. tostring(game.GameId)
	.. tostring(game.PlaceId)
	.. tostring(game.PlaceVersion)

-- context size limit is 255
local sharedStaticFactorsHash = Blake3.Digest(buffer.fromstring(allStaticFactors), 64)
local base = `opensb@networking->{buffer.tostring(sharedStaticFactorsHash)}->`

function Module.getDsaContext(player: Player)
	return buffer.fromstring(`{base}{player.UserId}`)
end

function Module.robloxEntropyProvider(bytesLeft: number): buffer?
	local seed = ""
	local bytesGenerated = 0
	while bytesGenerated < bytesLeft do
		local guid = string.gsub(HttpService:GenerateGUID(false), "-", "")
		local bytesToAdd = math.min(#guid, bytesLeft - bytesGenerated)
		seed ..= string.sub(guid, 1, bytesToAdd)
		bytesGenerated += bytesToAdd
	end

	return buffer.fromstring(seed)
end

-- encrypting / decrypting
export type networkData = {
	staticKey: buffer,

	nonce: number, -- Last used nonce, next nonce to use should be this + 1
	lastNonce: number, -- Last nonce sent, new nonces should always be greater than this one
}

local function padNonce(nonce: number): buffer
	local buf = buffer.create(12)
	buffer.writeu32(buf, 0, nonce)

	return buf
end

local function encryptBuffer(networkData: networkData, data: buffer): (buffer, number, buffer)
	networkData.nonce += 1
	local nonce = networkData.nonce

	local ciphertext, tag = AEAD.Encrypt(data, networkData.staticKey, padNonce(nonce))
	return ciphertext, nonce, tag
end

function Module.encryptAnything(networkData: networkData, data: any): (buffer, number, buffer, { Instance })
	local encodedData, instancesArray = Encoding.encode(data)
	local ciphertext, nonce, tag = encryptBuffer(networkData, encodedData)

	return ciphertext, nonce, tag, instancesArray
end

local function decryptBuffer(networkData: networkData, ciphertext: buffer, nonce: number, tag: buffer): buffer
	return assert(
		AEAD.Decrypt(ciphertext, networkData.staticKey, padNonce(nonce), tag),
		"failed decrypting data with aead"
	)
end

local function decryptAnything(
	networkData: networkData,
	ciphertext: buffer,
	nonce: number,
	tag: buffer,
	instanceArray: { Instance }
): any
	return Encoding.decode(decryptBuffer(networkData, ciphertext, nonce, tag), instanceArray)
end

function Module.decryptAnythingAndVerify(networkData: networkData, ciphertext, nonce, tag, instanceArray): any?
	if
		type(ciphertext) ~= "buffer"
		or type(nonce) ~= "number"
		or type(tag) ~= "buffer"
		or type(instanceArray) ~= "table"
	then
		return nil
	end

	if nonce <= networkData.lastNonce then
		-- This is a replayed nonce
		return nil
	end

	local data = decryptAnything(networkData, ciphertext, nonce, tag, instanceArray :: { Instance })
	networkData.lastNonce = nonce

	return data
end

return table.freeze(Module)


return require("@shared/enum")({
	"Log",
	"Warning",
	"Error",
	"Success",
	"Information",
	"System",
})


--!strict

local table = table
local ipairs = ipairs

-- Only really used for a safe PlayerRemoved (fired after they leave!!) signal (for cleanup things).

local Signal = require("@shared/signal")

local PlayerList = {}
PlayerList.Added = Signal.new()
PlayerList.Removed = Signal.new()

local Players = game:GetService("Players")
local list: { [Player]: true } = {}

function PlayerList:GetList(): { Player }
	local players = {}
	for player in list do
		table.insert(players, player)
	end

	return players
end

local function added(player: Player)
	if list[player] == true then
		return
	end

	list[player] = true
	PlayerList.Added:Fire(player)
end

local function removed(player: Player)
	if list[player] == nil then
		return
	end

	list[player] = nil
	PlayerList.Removed:Fire(player)
end

game:GetService("RunService").Heartbeat:Connect(function()
	for player in list do
		if player.Parent == Players then
			continue
		end

		removed(player)
	end

	for _, player in ipairs(Players:GetPlayers()) do
		if list[player] then
			continue
		end

		added(player)
	end
end)

Players.PlayerAdded:Connect(added) -- Fire .Added as soon as possible
-- We could connect Players.PlayerRemoving, but then the .Removed event will fire before the player has fully left

return table.freeze(PlayerList)


--!strict

local WorkerManagers = require("@shared/workerManagers")

local function protect(instance: Instance, level: "remove" | "write" | "read")
	WorkerManagers:Send("protect", instance, level)
end

local function unprotect(instance: Instance)
	WorkerManagers:Send("unprotect", instance)
end

return table.freeze({
	add = protect,
	remove = unprotect,
})


return require("@shared/enum")({
	"Script",
	"LocalScript",
	"ModuleScript",
})


local setmetatable = setmetatable
local Instance = Instance
local math = math
local table = table

local Signal = {}
Signal.__index = Signal

function Signal.new()
	local self = setmetatable({}, Signal)
	self._bindable = Instance.new("BindableEvent")
	self._argMap = {}

	-- Events connected first always fire last
	self._bindable.Event:Connect(function(key)
		self._argMap[key] = nil
	end)

	return self
end

function Signal:Destroy()
	if self._bindable then
		self._bindable:Destroy()
		self._bindable = nil
	end

	setmetatable(self, nil)
end

function Signal:Fire(...)
	local key = math.random()
	self._argMap[key] = table.pack(...)
	self._bindable:Fire(key)
end

function Signal:Connect(callback)
	return self._bindable.Event:Connect(function(key)
		local args = self._argMap[key]
		if args then
			callback(table.unpack(args, 1, args.n))
		end
	end)
end

function Signal:Once(callback)
	return self._bindable.Event:Once(function(key)
		local args = self._argMap[key]
		if args then
			callback(table.unpack(args, 1, args.n))
		end
	end)
end

function Signal:Wait()
	local key = self._bindable.Event:Wait()
	local args = self._argMap[key]

	if args then
		return table.unpack(args, 1, args.n)
	end
end

export type Signal<Args...> = {
	Destroy: (self: Signal<Args...>) -> (),
	Fire: (self: Signal<Args...>, Args...) -> (),
	Connect: (self: Signal<Args...>, callback: (Args...) -> ()) -> RBXScriptConnection,
	Once: (self: Signal<Args...>, callback: (Args...) -> ()) -> RBXScriptConnection,
	Wait: (self: Signal<Args...>) -> Args...,
}

return Signal :: {
	new: <Args...>() -> Signal<Args...>,
}


local debug = debug
local table = table
local string = string
local ipairs = ipairs

local Functions = require("@shared/functions")

local getFunctionName = Functions.getFunctionName

local StackTrace = {}

export type StackTrace = { { source: string, line: number, name: string? } }

function StackTrace.get(level: number?): StackTrace
	local stackTrace = {}

	local offset = level or 1
	local currentLevel = 0
	while true do
		currentLevel += 1

		local func, source, line = debug.info(currentLevel + offset, "fsl")
		if not func then
			break
		end

		stackTrace[currentLevel] = table.freeze({ source = source, line = line, name = getFunctionName(func) })
	end

	return table.freeze(stackTrace)
end

function StackTrace.fromScriptContextStackTrace(stackTrace: string): StackTrace
	local traces = string.split(stackTrace, "\n")
	local numTraces = #traces - 1 -- Last trace is always "" (so we skip it)

	local newStackTrace = table.create(numTraces)
	for index = 1, numTraces do
		local source, line, name = string.match(traces[index], "^(.+), line (%d+) %- function (.+)$")
		if not name then
			source, line = string.match(traces[index], "^(.+), line (%d+)$")
		end

		newStackTrace[index] = table.freeze({ source = source, line = line, name = name })
	end

	return table.freeze(newStackTrace)
end

function StackTrace.formatScriptStackTrace(stackTrace: StackTrace): { string }
	local strs = { "Stack Begin" }
	for _, stack in ipairs(stackTrace) do
		local source, line, name = stack.source, stack.line, stack.name
		if name then
			table.insert(strs, `Script '{source}', Line {line} - function {name}`)
		else
			table.insert(strs, `Script '{source}', Line {line}`)
		end
	end

	table.insert(strs, "Stack End")
	return strs
end

return table.freeze(StackTrace)


local setmetatable = setmetatable
local table = table
local error = error

local Signal = require("@shared/signal")
local Log = require("@shared/log")

type Signal<Args...> = Signal.Signal<Args...>

local ManagerCommunication = {}
ManagerCommunication.__index = ManagerCommunication

function ManagerCommunication.new(...)
	local self = setmetatable({}, ManagerCommunication)
	self:init(...)

	return self
end

function ManagerCommunication:init(
	send: BindableEvent,
	receive: BindableEvent,
	invoke: BindableFunction,
	invoked: BindableFunction
)
	self.__send = send
	self.__invoke = invoke

	self.__events = {}
	self.__invokes = {}

	self.__sendOnReady = {}
	self.__resumeOnReady = {}

	receive.Event:Connect(function(topic: string, ...)
		local event = self.__events[topic]
		if not event then
			return
		end

		event:Fire(...)
	end)

	invoked.OnInvoke = function(topic: string, ...)
		local callback = self.__invokes[topic]
		if not callback then
			return error(`"{topic}" is not a valid topic.`, 2)
		end

		return callback(...)
	end

	self:once("ready", function()
		local toSend = table.clone(self.__sendOnReady)
		table.clear(self.__sendOnReady)
		table.freeze(self.__sendOnReady) -- Marks that we're ready

		local toResume = table.clone(self.__resumeOnReady)
		table.clear(self.__resumeOnReady)
		table.freeze(self.__resumeOnReady)

		Log.debug(`Receiver ready! Sending {#toSend} events and resuming {#toResume} threads.`)

		for _, data in ipairs(toSend) do
			self.__send:Fire(unpack(data, 1, data.n))
		end

		for _, thread in ipairs(toResume) do
			pcall(coroutine.resume, thread)
		end
	end)

	table.freeze(self)
end

function ManagerCommunication:isReceiverReady()
	return table.isfrozen(self.__sendOnReady)
end

function ManagerCommunication:awaitReceiverReady()
	if self:isReceiverReady() then
		return
	end

	table.insert(self.__resumeOnReady, coroutine.running())
	coroutine.yield()
end

function ManagerCommunication:ready(awaitReceiver: boolean)
	Log.debug("Communication ready!")

	if awaitReceiver then
		self:Send("ready")
		return
	end

	self.__send:Fire("ready")
end

function ManagerCommunication:on<Args...>(topic: string, callback: (Args...) -> ())
	-- This will not destroy the signal once all connections are disconnected, but it's likely this will never happen.
	-- If we add topics that are temporary and used once then this should be reworked to avoid memory leaks.

	local event: Signal<Args...> = self.__events[topic]
	if not event then
		event = Signal.new()
		self.__events[topic] = event
	end

	return event:Connect(callback)
end

function ManagerCommunication:once<Args...>(topic: string, callback: (Args...) -> ())
	local event: Signal<Args...> = self.__events[topic]
	if not event then
		event = Signal.new()
		self.__events[topic] = event
	elseif _G.dev then
		Log.warn(
			debug.traceback(
				"You are calling once on an event that already exists; this will destroy the event and delete the topic after the event is fired. This can and most likely WILL lead to unexpected behavior."
			)
		)
	end

	return event:Once(function(...)
		event:Destroy()
		self.__events[topic] = nil
		callback(...)
	end)
end

function ManagerCommunication:onInvoke<Args..., Ret...>(topic: string, callback: (Args...) -> Ret...)
	self.__invokes[topic] = callback
end

function ManagerCommunication:Send(topic: string, ...)
	if self:isReceiverReady() then
		self.__send:Fire(topic, ...)
	else
		table.insert(self.__sendOnReady, table.pack(topic, ...))
	end
end

function ManagerCommunication:Invoke<Args..., Ret...>(topic: string, ...: Args...): Ret...
	self:awaitReceiverReady()
	return self.__invoke:Invoke(topic, ...)
end

type ManagerCommunication = {
	isReceiverReady: (self: ManagerCommunication) -> boolean,
	awaitReceiverReady: (self: ManagerCommunication) -> (),
	ready: (self: ManagerCommunication, awaitReceiver: boolean) -> (),
	on: <Args...>(self: ManagerCommunication, topic: string, callback: (Args...) -> ()) -> RBXScriptConnection,
	once: <Args...>(self: ManagerCommunication, topic: string, callback: (Args...) -> ()) -> RBXScriptConnection,
	onInvoke: <Args..., Ret...>(self: ManagerCommunication, topic: string, callback: (Args...) -> Ret...) -> (),
	Send: (self: ManagerCommunication, topic: string, ...any) -> (),
	Invoke: <Args..., Ret...>(self: ManagerCommunication, topic: string, Args...) -> Ret...,
}

return ManagerCommunication :: {
	new: (
		send: BindableEvent,
		receive: BindableEvent,
		invoke: BindableFunction,
		invoked: BindableFunction
	) -> ManagerCommunication,
	init: (
		self: ManagerCommunication,
		send: BindableEvent,
		receive: BindableEvent,
		invoke: BindableFunction,
		invoked: BindableFunction
	) -> (),
} & ManagerCommunication


--!strict

-- number = 0 | 1 | 2
--[[
each further level implies the last
0 - remove protection [ allow reads & writes & descendant inserts ]
1 - remove protection & no writing [ allow reads ]
2 - remove protection & write protection & read protection [ ghost ]
]]

-- local ManagerCommunication = require("@shared/wm/communication")

local protectedInstances: { [Instance]: number } = {}
local protectedClasses: { [string]: number } = {}

do
	local yaml = table.freeze(require("@self/classes.yaml") :: { [string]: any })
	local function registerClassesAtLevel(classes: { string }, level: number)
		for _, class in ipairs(classes or {}) do
			protectedClasses[class] = level
		end
	end

	registerClassesAtLevel(yaml.remove_protected, 0)
	registerClassesAtLevel(yaml.write_protected, 1)
	registerClassesAtLevel(yaml.read_protected, 2)

	table.freeze(protectedClasses)
end

local function getLevel(instance: Instance)
	return (protectedInstances[instance] or protectedClasses[instance.ClassName]) or -1
end

local function isRemoveProtected(instance: Instance)
	return getLevel(instance) >= 0
end

local function isWriteProtected(instance: Instance)
	return getLevel(instance) >= 1
end

local function isReadProtected(instance: Instance)
	return getLevel(instance) == 2
end

type Level = "remove" | "write" | "read"

local function isProtected(instance: Instance, level: Level)
	if level == "remove" then
		return isRemoveProtected(instance)
	elseif level == "write" then
		return isWriteProtected(instance)
	elseif level == "read" then
		return isReadProtected(instance)
	else
		error(`got unknown level: {level}`, 2)
	end
end

local function protect(instance: Instance, level: Level)
	if level == "remove" then
		protectedInstances[instance] = 0
	elseif level == "write" then
		protectedInstances[instance] = 1
	elseif level == "read" then
		protectedInstances[instance] = 2
	else
		error(`got unknown level: {level}`, 2)
	end
end

local function unprotect(instance: Instance)
	protectedInstances[instance] = nil
end

local function getProtection(instance: Instance): Level?
	local level = getLevel(instance)
	if level == -1 then
		return nil
	elseif level == 0 then
		return "remove"
	elseif level == 1 then
		return "write"
	elseif level == 2 then
		return "read"
	else
		error(`got unknown level: {level}`)
	end
end

local function init()
	-- listen for requests on the backend
	local ManagerCommunication = require("@shared/wm/communication")
	ManagerCommunication:on("protect", protect)
	ManagerCommunication:on("unprotect", unprotect)
end

return table.freeze({
	isRemoveProtected = isRemoveProtected,
	isWriteProtected = isWriteProtected,
	isReadProtected = isReadProtected,
	isProtected = isProtected,

	add = protect,
	remove = unprotect,
	get = getProtection,
	init = init,
})


local Log = require("@shared/log")
local Functions = require("@shared/functions")

local ManagerCommunication = require("@shared/wm/communication")

local WorkerManagers = {}

function WorkerManagers:Init(root, managerName)
	Log.debug("Creating WorkerManagers...")

	-- Messy but whatever
	local managerTemplate = root:WaitForChild(managerName)
	if managerTemplate.ClassName == "ModuleScript" then
		managerTemplate:WaitForChild("start")
	end

	task.defer(game.Destroy, managerTemplate) -- Destroy old refrence, after we clone it.
	managerTemplate = managerTemplate:Clone()

	if managerTemplate.ClassName == "ModuleScript" then
		managerTemplate:WaitForChild("start").Enabled = true
	else
		managerTemplate.Enabled = true
	end

	local function createManager(parent, index)
		local manager = managerTemplate:Clone()
		local communicationExchange = Instance.new("BindableFunction")
		local send, recieve, invoke, invoked =
			Instance.new("BindableEvent"),
			Instance.new("BindableEvent"),
			Instance.new("BindableFunction"),
			Instance.new("BindableFunction")

		send.Name = Functions.randomInstanceName()
		recieve.Name = Functions.randomInstanceName()
		invoke.Name = Functions.randomInstanceName()
		invoked.Name = Functions.randomInstanceName()

		-- Secure (ish) exchange (atleast can't be obtained by other scripts).
		communicationExchange.OnInvoke = function()
			communicationExchange.OnInvoke = Functions.empty
			communicationExchange:Destroy()

			Log.debug(`Exchanged communication bindables with {managerName}{index}.`)
			return recieve, send, invoked, invoke -- Reversed on the recieveing end
		end

		communicationExchange.Name = Functions.randomInstanceName()
		communicationExchange.Parent = manager

		local Manager = ManagerCommunication.new(send, recieve, invoke, invoked)
		WorkerManagers[index] = Manager

		manager.Name ..= `{index}@{math.random()}` -- We give them a random name to help filter out sb stack traces from other code
		manager.Parent = parent

		return Manager
	end

	WorkerManagers.main = createManager(root, 0)
	for index = 1, _G.workerThreads do
		local actor = Instance.new("Actor")
		createManager(actor, index)
		actor.Parent = root
	end

	table.freeze(WorkerManagers)
end

function WorkerManagers:ready()
	-- Sends with awaitReceiver set to true, meaning it will use the normal :Send() function (which waits for the receiving end to be fully ready).
	-- The other side does not send with this set to true, as our side will always be ready to receive the "ready" event.

	WorkerManagers.main:ready(true)
	for _, Manager in ipairs(WorkerManagers) do
		Manager:ready(true)
	end
end

function WorkerManagers:on(topic: string, callback: (any...) -> never)
	WorkerManagers.main:on(topic, callback)
	for _, Manager in ipairs(WorkerManagers) do
		Manager:on(topic, callback)
	end
end

function WorkerManagers:onInvoke(topic: string, callback: (any...) -> any...)
	WorkerManagers.main:onInvoke(topic, callback)
	for _, Manager in ipairs(WorkerManagers) do
		Manager:onInvoke(topic, callback)
	end
end

function WorkerManagers:Send(topic: string, ...)
	WorkerManagers.main:Send(topic, ...)
	for _, Manager in ipairs(WorkerManagers) do
		Manager:Send(topic, ...)
	end
end

return WorkerManagers


local __start = os.clock()
local networkAttribute = { next(script:GetAttributes()) }

local Log = require("@shared/log")
Log:SetPrefix("[SB Client]")
Log.print("Loading...")

do
	local wm = require("@shared/wm")
	wm.isWorkerManager = false
	table.freeze(wm)
end

-- Fetch assets and destroy script
local sbActor = script.Parent
local root = sbActor.Parent

do
	local thread = coroutine.running()
	task.defer(
		function() -- Defer has a small yield (under a frame) allowing us to delete the script (instances can't change their parent instantly after there were parented / created)
			sbActor:Destroy()
			script:Destroy()
			script = nil

			task.spawn(thread)
		end
	)

	coroutine.yield() -- Yield thread until script has been destroyed, so events dont get connected in the process (then disconnected by :Destroy())
end

local Assets = require("@shared/assets")
Assets:Init(root, "assets")

-- Services and modules
Log.debug("Loading modules...")

local Network = require("@client/network")
local Commands = require("@client/commands")
local UI = require("@client/ui")
local StarterScripts = require("@client/starterScripts")
local Output = require("@client/output")
local WorkerManagers = require("@shared/workerManagers")

WorkerManagers:Init(root, "localWorkerManager")
require("@client/scriptManager")
Network:Init(networkAttribute)
Commands:Init({ require("@client/commands/default"), require("@client/commands/get") })
UI:Init()
StarterScripts:Init()

-- Finalize
Log.debug("Finalizing...")

WorkerManagers:ready()

local loadTime = math.round((os.clock() - __start) * 1000)
Log.print(`Loaded in {loadTime}ms.`)

task.defer(function()
	-- Make sure UI is loaded.
	UI:WaitUntilLoaded()

	local buildType = if _G.dev then "development" else "production"
	Output:append(Output.MessageType.System, `OpenSB {buildType} build. Ran in {loadTime}ms.`)
end)

return nil