--[=[
	Cryptography library: Cryptographically Secure RNG

	Usage:
		local RandomFloat = CSPRNG.Random()
		local RandomInt = CSPRNG.RandomInt(1, 100)
		local RandomNumber = CSPRNG.RandomNumber(0.5, 10.5)
		local RandomBytes = CSPRNG.RandomBytes(32)
		local RandomHex = CSPRNG.RandomHex(16)
		local FastString = CSPRNG.RandomString(16, false)
		local FastBuffer = CSPRNG.RandomString(32, true)

		local Ed25519Clamped = CSPRNG.Ed25519ClampedBytes(SomeBuffer)
		local Ed25519Random = CSPRNG.Ed25519Random()

		CSPRNG.AddEntropyProvider(function)
		CSPRNG.RemoveEntropyProvider(function)
		CSPRNG.Reseed()

		CSPRNG.BytesLeft
--]=]

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

local Conversions = require("@self/Conversions")
local ChaCha20 = require("@self/ChaCha20")
local Blake3 = require("@self/Blake3")

export type EntropyProvider = (BytesLeft: number) -> buffer?

type CSPRNGModule = {
	BlockExpansion: boolean,

	SizeTarget: number,
	RekeyAfter: number,

	Key: buffer,
	Nonce: buffer,
	Buffer: buffer,

	Counter: number,
	BufferPosition: number,
	BufferSize: number,
	BytesLeft: number,

	EntropyProviders: { EntropyProvider },

	Reseed: (CustomEntropy: buffer?) -> (),
	AddEntropyProvider: (ProviderFunction: EntropyProvider) -> (),
	RemoveEntropyProvider: (ProviderFunction: EntropyProvider) -> (),

	Random: () -> number,
	RandomInt: (Min: number, Max: number?) -> number,
	RandomNumber: (Min: number, Max: number?) -> number,
	RandomBytes: (Count: number) -> buffer,
	RandomString: (Length: number, AsBuffer: boolean?) -> string | buffer,
	RandomHex: (Length: number) -> string,
	Ed25519ClampedBytes: (Input: buffer) -> buffer,
	Ed25519Random: () -> buffer,
}

local BLOCK_SIZE = 64
local KEY_SIZE = 32
local NONCE_SIZE = 12

local CSPRNG: CSPRNGModule = {
	BlockExpansion = true,
	SizeTarget = 2048,
	RekeyAfter = 1024,

	Key = buffer.create(0),
	Nonce = buffer.create(0),
	Buffer = buffer.create(0),

	Counter = 0,
	BufferPosition = 0,
	BufferSize = 0,
	BytesLeft = 0,

	EntropyProviders = {}
} :: CSPRNGModule

local INPUT_BUFFER = buffer.create(BLOCK_SIZE)
local REKEY_THRESHOLD = math.max(math.floor(CSPRNG.RekeyAfter), 2)
local SIZE_TARGET_CLAMPED = math.clamp(math.floor(CSPRNG.SizeTarget), 64, 4294967295)

local function Reset()
	CSPRNG.Key = buffer.create(0)
	CSPRNG.Nonce = buffer.create(0)
	CSPRNG.Buffer = buffer.create(0)

	CSPRNG.Counter = 0
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = 0
end

local function GatherEntropy(CustomEntropy: buffer?): number
	local EntropyBuffers = buffer.create(1024)
	local Offset = 0

	local function WriteToBuffer(Source: buffer)
		local Size = buffer.len(Source)
		buffer.copy(EntropyBuffers, Offset, Source, 0, Size)
		Offset += Size
	end

	local CurrentTime = 1.234
	if tick then
		CurrentTime = tick()
		local TimeBuffer = buffer.create(8)
		buffer.writef64(TimeBuffer, 0, CurrentTime)
		WriteToBuffer(TimeBuffer)
	end

	local ClockTime = os.clock()
	local ClockBuffer = buffer.create(8)
	buffer.writef64(ClockBuffer, 0, ClockTime)
	WriteToBuffer(ClockBuffer)

	local UnixTime = os.time()
	local UnixBuffer = buffer.create(8)
	buffer.writeu32(UnixBuffer, 0, UnixTime % 0x100000000)
	buffer.writeu32(UnixBuffer, 4, math.floor(UnixTime / 0x100000000))
	WriteToBuffer(UnixBuffer)

	local DateTimeMillis = 5.678
	if DateTime then
		DateTimeMillis = DateTime.now().UnixTimestampMillis
		local DateTimeBuffer = buffer.create(8)
		buffer.writef64(DateTimeBuffer, 0, DateTimeMillis)
		WriteToBuffer(DateTimeBuffer)

		local DateTimePrecisionBuffer = buffer.create(16)
		buffer.writef32(DateTimePrecisionBuffer, 0, DateTimeMillis / 1000)
		buffer.writef32(DateTimePrecisionBuffer, 4, (DateTimeMillis % 1000) / 100)
		buffer.writef32(DateTimePrecisionBuffer, 8, DateTimeMillis / 86400000)
		buffer.writef32(DateTimePrecisionBuffer, 12, (DateTimeMillis * 0.001) % 1)
		WriteToBuffer(DateTimePrecisionBuffer)
	else
		WriteToBuffer(buffer.create(24))
	end

	local FracTimeBuffer = buffer.create(16)
	buffer.writef32(FracTimeBuffer, 0, ClockTime / 100)
	buffer.writef32(FracTimeBuffer, 4, CurrentTime / 1000)
	buffer.writef32(FracTimeBuffer, 8, (ClockTime * 12345.6789) % 1)
	buffer.writef32(FracTimeBuffer, 12, (CurrentTime * 98765.4321) % 1)
	WriteToBuffer(FracTimeBuffer)

	local NoiseBuffer = buffer.create(32)
	for Index = 0, 7 do
		local Noise1 = math.noise(ClockTime + Index, UnixTime + Index, ClockTime + UnixTime + Index)
		local Noise2 = math.noise(CurrentTime + Index * 0.1, DateTimeMillis * 0.0001 + Index, ClockTime * 1.5 + Index)
		local Noise3 = math.noise(UnixTime * 0.01 + Index, ClockTime + DateTimeMillis * 0.001, CurrentTime + Index * 2)
		local Noise4 = math.noise(DateTimeMillis * 0.00001 + Index, UnixTime + ClockTime + Index, CurrentTime * 0.1 + Index)

		buffer.writef32(NoiseBuffer, Index * 4, Noise1 + Noise2 + Noise3 + Noise4)
	end
	WriteToBuffer(NoiseBuffer)

	local BenchmarkTimings = buffer.create(32)
	for Index = 0, 7 do
		local StartTime = os.clock()
		local Sum = 0

		local Iterations = 50 + (Index * 25)
		for Iteration = 1, Iterations do
			Sum += Iteration * Iteration + math.sin(Iteration / 10) * math.cos(Iteration / 7)
		end

		local EndTime = os.clock()
		local TimingDelta = EndTime - StartTime
		buffer.writef32(BenchmarkTimings, Index * 4, TimingDelta * 1000000)
	end
	WriteToBuffer(BenchmarkTimings)

	local AllocTimings = buffer.create(24)
	for Index = 0, 5 do
		local AllocStart = os.clock()

		for AllocIndex = 1, 20 do
			local _TempBuf = buffer.create(64 + AllocIndex)
		end

		local AllocEnd = os.clock()
		buffer.writef32(AllocTimings, Index * 4, (AllocEnd - AllocStart) * 10000000)
	end
	WriteToBuffer(AllocTimings)

	local MicroTime = math.floor(CurrentTime * 1000000)
	local MicroTimeBuffer = buffer.create(8)
	buffer.writeu32(MicroTimeBuffer, 0, MicroTime % 0x100000000)
	buffer.writeu32(MicroTimeBuffer, 4, math.floor(MicroTime / 0x100000000))
	WriteToBuffer(MicroTimeBuffer)

	if game then
		if game.JobId and #game.JobId > 0 then
			local JobIdBuffer = buffer.fromstring(game.JobId)
			WriteToBuffer(JobIdBuffer)
		end

		if game.PlaceId then
			local PlaceIdBuffer = buffer.create(8)
			buffer.writeu32(PlaceIdBuffer, 0, game.PlaceId % 0x100000000)
			buffer.writeu32(PlaceIdBuffer, 4, math.floor(game.PlaceId / 0x100000000))
			WriteToBuffer(PlaceIdBuffer)
		end

		if workspace and workspace.DistributedGameTime then
			local DistTimeBuffer = buffer.create(8)
			buffer.writef64(DistTimeBuffer, 0, workspace.DistributedGameTime)
			WriteToBuffer(DistTimeBuffer)

			local DistMicroTime = math.floor(workspace.DistributedGameTime * 1000000)
			local DistMicroBuffer = buffer.create(8)
			buffer.writeu32(DistMicroBuffer, 0, DistMicroTime % 0x100000000)
			buffer.writeu32(DistMicroBuffer, 4, math.floor(DistMicroTime / 0x100000000))
			WriteToBuffer(DistMicroBuffer)
		end
	end

	local AddressEntropy = buffer.create(128)
	for Index = 0, 7 do
		local TempTable = {}
		local TempFunc = function() end
		local TempBuffer = buffer.create(0)
		local TempUserdata = newproxy()

		local TableAddr = string.gsub(tostring(TempTable), "table: ", "")
		local FuncAddr = string.gsub(tostring(TempFunc), "function: ", "")
		local BufferAddr = string.gsub(tostring(TempBuffer), "buffer: ", "")
		local UserdataAddr = string.gsub(tostring(TempUserdata), "userdata: ", "")

		local TableHash = 0
		local ThreadHash = 0
		local FuncHash = 0
		local BufferHash = 0
		local UserdataHash = 0

		for AddrIndex = 1, #TableAddr do
			TableHash = bit32.bxor(TableHash, string.byte(TableAddr, AddrIndex)) * 31
		end

		if coroutine then
			local ThreadAddr = string.gsub(tostring(coroutine.create(function() end)), "thread: ", "")
			for AddrIndex = 1, #ThreadAddr do
				ThreadHash = bit32.bxor(ThreadHash, string.byte(ThreadAddr, AddrIndex)) * 31
			end
		end

		for AddrIndex = 1, #FuncAddr do
			FuncHash = bit32.bxor(FuncHash, string.byte(FuncAddr, AddrIndex)) * 37
		end
		for AddrIndex = 1, #BufferAddr do
			BufferHash = bit32.bxor(BufferHash, string.byte(BufferAddr, AddrIndex)) * 41
		end
		for AddrIndex = 1, #UserdataAddr do
			UserdataHash = bit32.bxor(UserdataHash, string.byte(UserdataAddr, AddrIndex)) * 43
		end

		buffer.writeu32(AddressEntropy, Index * 16, TableHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 4, ThreadHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 8, FuncHash)
		buffer.writeu32(AddressEntropy, Index * 16 + 12, bit32.bxor(BufferHash, UserdataHash))
	end
	WriteToBuffer(AddressEntropy)

	local function AddExtraEntropy(Entropy: buffer?, Warn: boolean, Provider: string?)
		if not Entropy then
			return
		end

		local BytesLeft = 1024 - Offset

		if BytesLeft > 0 then
			local Extra = buffer.len(Entropy) - BytesLeft
			local Truncated = math.min(BytesLeft, buffer.len(Entropy))

			if Extra > 0 and Warn and Provider then
				warn(`CSPRNG: {Provider} returned {Extra} bytes more than available and was truncated to {Truncated} bytes`)
			end

			buffer.copy(EntropyBuffers, Offset, Entropy, 0, Truncated)
		end
	end

	for Index, Provider in CSPRNG.EntropyProviders do
		local BytesLeft = 1024 - Offset
		if BytesLeft > 0 then
			local Success: boolean, ExtraEntropy: buffer? = pcall(Provider, BytesLeft)
			if not Success then
				warn(`CSPRNG Provider errored with {ExtraEntropy}`)
			end

			AddExtraEntropy(ExtraEntropy, true, `Entropy Provider #{Index}`)
		end
	end

	if CustomEntropy then
		AddExtraEntropy(CustomEntropy, false)
	end

	local KeyMaterial = Blake3(EntropyBuffers, KEY_SIZE + NONCE_SIZE)

	CSPRNG.Key = buffer.create(KEY_SIZE)
	buffer.copy(CSPRNG.Key, 0, KeyMaterial, 0, KEY_SIZE)

	CSPRNG.Nonce = buffer.create(NONCE_SIZE)
	buffer.copy(CSPRNG.Nonce, 0, KeyMaterial, KEY_SIZE, NONCE_SIZE)

	return buffer.len(EntropyBuffers) - Offset
end

local function GenerateBlock()
	buffer.fill(INPUT_BUFFER, 0, 0, BLOCK_SIZE)
	local ChaChaOutput = ChaCha20(INPUT_BUFFER, CSPRNG.Key, CSPRNG.Nonce, CSPRNG.Counter, 20)

	CSPRNG.Buffer = if CSPRNG.BlockExpansion then Blake3(ChaChaOutput, SIZE_TARGET_CLAMPED) else ChaChaOutput
	CSPRNG.BufferPosition = 0
	CSPRNG.BufferSize = buffer.len(CSPRNG.Buffer)
	CSPRNG.Counter += 1

	if CSPRNG.Counter % REKEY_THRESHOLD == 0 then
		GatherEntropy()
		CSPRNG.Counter = 0
	end
end

local function GetBytes(Count: number): buffer
	local Result = buffer.create(Count)
	local ResultPosition = 0

	while ResultPosition < Count do
		if CSPRNG.BufferPosition >= CSPRNG.BufferSize then
			GenerateBlock()
		end

		local BytesNeeded = Count - ResultPosition
		local BytesAvailable = CSPRNG.BufferSize - CSPRNG.BufferPosition
		local BytesToCopy = math.min(BytesNeeded, BytesAvailable)

		buffer.copy(Result, ResultPosition, CSPRNG.Buffer, CSPRNG.BufferPosition, BytesToCopy)
		ResultPosition += BytesToCopy
		CSPRNG.BufferPosition += BytesToCopy
	end

	return Result
end

local function GetFloat(): number
	if CSPRNG.BufferPosition + 8 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value1 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	local Value2 = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition + 4)
	CSPRNG.BufferPosition += 8

	local High = bit32.rshift(Value1, 5)
	local Low = bit32.rshift(Value2, 6)

	return (High * 67108864.0 + Low) / 9007199254740992.0
end

local function GetIntRange(Min: number, Max: number): number
	local Range = Max - Min + 1
	local MaxUInt32 = 0xFFFFFFFF
	local Limit = MaxUInt32 - (MaxUInt32 % Range)

	if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
		GenerateBlock()
	end

	local Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
	CSPRNG.BufferPosition += 4

	if bit32.band(Range, Range - 1) == 0 then
		return Min + bit32.band(Value, Range - 1)
	else
		while Value > Limit do
			if CSPRNG.BufferPosition + 4 > CSPRNG.BufferSize then
				GenerateBlock()
			end
			Value = buffer.readu32(CSPRNG.Buffer, CSPRNG.BufferPosition)
			CSPRNG.BufferPosition += 4
		end

		return Min + (Value % Range)
	end
end

local function GetNumberRange(Min: number, Max: number): number
	if Min > Max then
		Min, Max = Max, Min
	end

	local Range = Max - Min
	if Range <= 0 then
		return Min
	end

	return Min + (GetFloat() * Range)
end

local function GetRandomString(Length: number, AsBuffer: boolean?): string | buffer
	local Characters = buffer.create(Length)

	for Index = 0, Length - 1 do
		buffer.writeu8(Characters, Index, GetIntRange(36, 122))
	end

	return if AsBuffer
		then Characters
		else buffer.tostring(Characters)
end

local function GetEd25519RandomBytes(): buffer
	local Output = buffer.create(32)

	for Index = 0, 31 do
		buffer.writeu8(Output, Index, GetIntRange(0, 255))
	end

	return Output
end

local function GetEd25519ClampedBytes(Input: buffer): buffer
	local Output = buffer.create(32)
	buffer.copy(Output, 0, Input, 0, 32)

	local FirstByte = buffer.readu8(Output, 0)
	FirstByte = bit32.band(FirstByte, 0xF8)
	buffer.writeu8(Output, 0, FirstByte)

	local LastByte = buffer.readu8(Output, 31)
	LastByte = bit32.band(LastByte, 0x7F)
	LastByte = bit32.bor(LastByte, 0x40)
	buffer.writeu8(Output, 31, LastByte)

	local HasVariation = false
	local FirstMiddleByte = buffer.readu8(Output, 1)
	for Index = 2, 30 do
		if buffer.readu8(Output, Index) ~= FirstMiddleByte then
			HasVariation = true
			break
		end
	end

	if not HasVariation then
		buffer.writeu8(Output, 15, bit32.bxor(FirstMiddleByte, 0x55))
	end

	return Output
end

local function GetHexString(Length: number): string
	local BytesNeeded = Length / 2
	local Bytes = GetBytes(BytesNeeded)
	local Hex = Conversions.ToHex(Bytes)

	return Hex
end

function CSPRNG.AddEntropyProvider(ProviderFunction: EntropyProvider)
	table.insert(CSPRNG.EntropyProviders, ProviderFunction)
end

function CSPRNG.RemoveEntropyProvider(ProviderFunction: EntropyProvider)
	for Index = #CSPRNG.EntropyProviders, 1, -1 do
		if CSPRNG.EntropyProviders[Index] == ProviderFunction then
			table.remove(CSPRNG.EntropyProviders, Index)
			break
		end
	end
end

function CSPRNG.Random(): number
	return GetFloat()
end

function CSPRNG.RandomInt(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

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

	if Max and Max < Min then
		error(`Max ({Max}) can't be less than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 1
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetIntRange(ActualMin, ActualMax)
end

function CSPRNG.RandomNumber(Min: number, Max: number?): number
	if Max and type(Max) ~= "number" then
		error(`Max must be a number or nil, got {typeof(Max)}`, 2)
	end

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

	if Max and Max < Min then
		error(`Max ({Max}) must be bigger than Min ({Min})`, 2)
	end

	if Max and Max == Min then
		error(`Max ({Max}) can't be equal to Min ({Min})`, 2)
	end

	local ActualMax: number
	local ActualMin: number

	if Max == nil then
		ActualMax = Min
		ActualMin = 0
	else
		ActualMax = Max
		ActualMin = Min
	end

	return GetNumberRange(ActualMin, ActualMax)
end

function CSPRNG.RandomBytes(Count: number): buffer
	if type(Count) ~= "number" then
		error(`Count must be a number, got {typeof(Count)}`, 2)
	end

	if Count <= 0 then
		error(`Count must be bigger than 0, got {Count}`, 2)
	end

	if Count % 1 ~= 0 then
		error("Count must be an integer", 2)
	end

	return GetBytes(Count)
end

function CSPRNG.RandomString(Length: number, AsBuffer: boolean?): string | buffer
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if AsBuffer ~= nil and type(AsBuffer) ~= "boolean" then
		error(`AsBuffer must be a boolean or nil, got {typeof(AsBuffer)}`, 2)
	end

	return GetRandomString(Length, AsBuffer)
end

function CSPRNG.RandomHex(Length: number): string
	if type(Length) ~= "number" then
		error(`Length must be a number, got {typeof(Length)}`, 2)
	end

	if Length <= 0 then
		error(`Length must be bigger than 0, got {Length}`, 2)
	end

	if Length % 1 ~= 0 then
		error("Length must be an integer", 2)
	end

	if Length % 2 ~= 0 then
		error(`Length must be even, got {Length}`, 2)
	end

	return GetHexString(Length)
end

function CSPRNG.Ed25519ClampedBytes(Input: buffer): buffer
	if type(Input) ~= "buffer" then
		error(`Input must be a buffer, got {typeof(Input)}`, 2)
	end

	return GetEd25519ClampedBytes(Input)
end

function CSPRNG.Ed25519Random(): buffer
	return GetEd25519ClampedBytes(GetEd25519RandomBytes())
end

function CSPRNG.Reseed(CustomEntropy: buffer?)
	if CustomEntropy ~= nil and type(CustomEntropy) ~= "buffer" then
		error(`CustomEntropy must be a buffer or nil, got {typeof(CustomEntropy)}`, 2)
	end

	Reset()
	GatherEntropy(CustomEntropy)
end

CSPRNG.BytesLeft = GatherEntropy()
GenerateBlock()

return CSPRNG


--[=[
	Finite field arithmetic modulo Q = 8380417

	Arithmetic operations over the prime field Z_q where q = 2^23 - 2^13 + 1.
	All operations maintain elements in canonical form [0, Q).

	Example usage:
		local Field = require(script)

		local A = 12345
		local B = 67890
		local Sum = Field.Add(A, B)
		local Product = Field.Multiply(A, B)
		local Inverse = Field.Inverse(A)
--]=]

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

local Q = 8380417

local Field = {}

function Field.Add(A: number, B: number): number
	local Sum = A + B
	return if Sum >= Q then Sum - Q else Sum
end

function Field.Negate(A: number): number
	return if A == 0 then 0 else Q - A
end

function Field.Subtract(A: number, B: number): number
	local Diff = A - B
	return if Diff < 0 then Diff + Q else Diff
end

function Field.Multiply(A: number, B: number): number
	return (A * B) % Q
end

function Field.Power(Base: number, Exponent: number): number
	if Exponent == 0 then
		return 1
	end

	if Exponent == 1 then
		return Base % Q
	end

	local Result = 1
	local CurrentBase = Base % Q
	local Exp = Exponent

	while Exp > 0 do
		if bit32.band(Exp, 1) == 1 then
			Result = Field.Multiply(Result, CurrentBase)
		end

		CurrentBase = Field.Multiply(CurrentBase, CurrentBase)
		Exp = bit32.rshift(Exp, 1)
	end

	return Result
end

function Field.Inverse(A: number): number
	return Field.Power(A, Q - 2)
end

function Field.Divide(A: number, B: number): number
	return Field.Multiply(A, Field.Inverse(B))
end

return Field


--[=[
	Number Theoretic Transform for degree-255 polynomials

	 Forward and inverse NTT over Z_q for polynomial multiplication.
	Uses Cooley-Tukey and Gentleman-Sande algorithms with precomputed roots.

	Example usage:
		local NTT = require(script)

		local Poly = buffer.create(256 * 4) -- 256 coefficients, 4 bytes each
		NTT.ForwardNTT(Poly)
		-- Perform operations in NTT domain
		NTT.InverseNTT(Poly)
--]=]

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

local Field = require("./Field")

local LOG2N = 8
local ZETA = 1753
local Q = 8380417

local N = bit32.lshift(1, LOG2N)
local INV_N = Field.Inverse(N)

local ZETA_EXP, ZETA_NEG_EXP = buffer.create(N * 4), buffer.create(N * 4) do
	for I = 0, N - 1 do
		local Reversed = 0
		local Value = I

		for J = 0, LOG2N - 1 do
			local Bit = bit32.band(bit32.rshift(Value, J), 1)
			Reversed = bit32.bxor(Reversed, bit32.lshift(Bit, LOG2N - 1 - J))
		end

		local ZetaExp = Field.Power(ZETA, Reversed)
		local ZetaNegExp = Field.Negate(ZetaExp)

		buffer.writeu32(ZETA_EXP, I * 4, ZetaExp)
		buffer.writeu32(ZETA_NEG_EXP, I * 4, ZetaNegExp)
	end
end

local Ntt = {}

function Ntt.ForwardNTT(Poly: buffer)
	local Zeta = ZETA_EXP
	local Modulus = Q

	for L = LOG2N - 1, 0, -1 do
		local Len = bit32.lshift(1, L)
		local LenX2 = bit32.lshift(Len, 1)
		local KBeg = bit32.rshift(N, L + 1)

		for Start = 0, N - 1, LenX2 do
			local KNow = KBeg + bit32.rshift(Start, L + 1)
			local ZetaExpValue = buffer.readu32(Zeta, KNow * 4)

			for I = Start, Start + Len - 1 do
				local IOffset = I * 4
				local ILenOffset = (I + Len) * 4

				local PolyI = buffer.readu32(Poly, IOffset)
				local PolyILen = buffer.readu32(Poly, ILenOffset)

				local Tmp = (ZetaExpValue * PolyILen) % Modulus

				local Sub = if PolyI >= Tmp then PolyI - Tmp else PolyI - Tmp + Modulus
				local Add = PolyI + Tmp
				Add = if Add >= Modulus then Add - Modulus else Add

				buffer.writeu32(Poly, ILenOffset, Sub)
				buffer.writeu32(Poly, IOffset, Add)
			end
		end
	end
end

function Ntt.ForwardNTTWithOffset(Poly: buffer, BaseOffset: number)
	local Zeta = ZETA_EXP
	local Modulus = Q

	for L = LOG2N - 1, 0, -1 do
		local Len = bit32.lshift(1, L)
		local LenX2 = bit32.lshift(Len, 1)
		local KBeg = bit32.rshift(N, L + 1)

		for Start = 0, N - 1, LenX2 do
			local KNow = KBeg + bit32.rshift(Start, L + 1)
			local ZetaExpValue = buffer.readu32(Zeta, KNow * 4)

			for I = Start, Start + Len - 1 do
				local IOffset = BaseOffset + I * 4
				local ILenOffset = BaseOffset + (I + Len) * 4

				local PolyI = buffer.readu32(Poly, IOffset)
				local PolyILen = buffer.readu32(Poly, ILenOffset)

				local Tmp = (ZetaExpValue * PolyILen) % Modulus

				local Sub = if PolyI >= Tmp then PolyI - Tmp else PolyI - Tmp + Modulus
				local Add = PolyI + Tmp
				Add = if Add >= Modulus then Add - Modulus else Add

				buffer.writeu32(Poly, ILenOffset, Sub)
				buffer.writeu32(Poly, IOffset, Add)
			end
		end
	end
end

function Ntt.InverseNTT(Poly: buffer)
	local Zeta = ZETA_NEG_EXP
	local Inv_n = INV_N
	local Modulus = Q

	for L = 0, LOG2N - 1 do
		local Len = bit32.lshift(1, L)
		local LenX2 = bit32.lshift(Len, 1)
		local KBeg = bit32.rshift(N, L) - 1

		for Start = 0, N - 1, LenX2 do
			local KNow = KBeg - bit32.rshift(Start, L + 1)
			local NegZetaExpValue = buffer.readu32(Zeta, KNow * 4)

			for I = Start, Start + Len - 1 do
				local IOffset = I * 4
				local ILenOffset = (I + Len) * 4

				local PolyI = buffer.readu32(Poly, IOffset)
				local PolyILen = buffer.readu32(Poly, ILenOffset)

				local Sum = PolyI + PolyILen
				Sum = if Sum >= Modulus then Sum - Modulus else Sum

				local Diff = if PolyI >= PolyILen then PolyI - PolyILen else PolyI - PolyILen + Modulus

				local Product = (Diff * NegZetaExpValue) % Modulus

				buffer.writeu32(Poly, IOffset, Sum)
				buffer.writeu32(Poly, ILenOffset, Product)
			end
		end
	end

	for I = 0, N - 1 do
		local Offset = I * 4
		local PolyValue = buffer.readu32(Poly, Offset)
		local Result = (PolyValue * Inv_n) % Modulus
		buffer.writeu32(Poly, Offset, Result)
	end
end

Ntt.ZETA_NEG_EXP = ZETA_NEG_EXP
Ntt.INV_N = INV_N

return Ntt


--[=[
	Bit packing and unpacking utilities

	Serializes polynomials to byte arrays with various bit widths.
	Supports encoding/decoding of polynomial coefficients and hint bits.

	Example usage:
		local BitPacking = require(script)

		local Poly = buffer.create(256 * 4)
		local Encoded = buffer.create(96) -- For 3-bit encoding
		BitPacking.Encode(Poly, Encoded, 3)
		BitPacking.Decode(Encoded, Poly, 3)
--]=]

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

local N = 256
local POLY_BYTES = N * 4

local BitPacking = {}

function BitPacking.Encode(Poly: buffer, Arr: buffer, Sbw: number)
	local ArrLen = buffer.len(Arr)
	buffer.fill(Arr, 0, 0, ArrLen)

	if Sbw == 3 then
		for I = 0, 31 do
			local PBase = I * 32
			local BBase = I * 3

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0x7)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0x7)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0x7)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0x7)
			local P4 = bit32.band(buffer.readu32(Poly, PBase + 16), 0x7)
			local P5 = bit32.band(buffer.readu32(Poly, PBase + 20), 0x7)
			local P6 = bit32.band(buffer.readu32(Poly, PBase + 24), 0x7)
			local P7 = bit32.band(buffer.readu32(Poly, PBase + 28), 0x7)

			local Byte0 = bit32.bor(P0, bit32.lshift(P1, 3), bit32.lshift(P2, 6))
			local Byte1 = bit32.bor(bit32.rshift(P2, 2), bit32.lshift(P3, 1), bit32.lshift(P4, 4), bit32.lshift(P5, 7))
			local Byte2 = bit32.bor(bit32.rshift(P5, 1), bit32.lshift(P6, 2), bit32.lshift(P7, 5))

			buffer.writeu8(Arr, BBase, Byte0)
			buffer.writeu8(Arr, BBase + 1, Byte1)
			buffer.writeu8(Arr, BBase + 2, Byte2)
		end

	elseif Sbw == 4 then
		for I = 0, 31 do
			local PBase = I * 32
			local BBase = I * 4

			local Word = buffer.readu32(Poly, PBase)
			local P0 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 4)
			local P1 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 8)
			local P2 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 12)
			local P3 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 16)
			local P4 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 20)
			local P5 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 24)
			local P6 = bit32.band(Word, 0xF)
			Word = buffer.readu32(Poly, PBase + 28)
			local P7 = bit32.band(Word, 0xF)

			buffer.writeu32(Arr, BBase, bit32.bor(
				P0, bit32.lshift(P1, 4), bit32.lshift(P2, 8), bit32.lshift(P3, 12),
				bit32.lshift(P4, 16), bit32.lshift(P5, 20), bit32.lshift(P6, 24), bit32.lshift(P7, 28)
				))
		end

	elseif Sbw == 6 then
		for I = 0, 63 do
			local PBase = I * 16
			local BBase = I * 3

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0x3F)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0x3F)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0x3F)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0x3F)

			local Packed = bit32.bor(P0, bit32.lshift(P1, 6), bit32.lshift(P2, 12), bit32.lshift(P3, 18))

			buffer.writeu8(Arr, BBase, bit32.band(Packed, 0xFF))
			buffer.writeu8(Arr, BBase + 1, bit32.band(bit32.rshift(Packed, 8), 0xFF))
			buffer.writeu8(Arr, BBase + 2, bit32.rshift(Packed, 16))
		end

	elseif Sbw == 10 then
		for I = 0, 63 do
			local PBase = I * 16
			local BBase = I * 5

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0x3FF)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0x3FF)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0x3FF)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0x3FF)

			local Lo = bit32.bor(P0, bit32.lshift(P1, 10), bit32.lshift(P2, 20))
			local Hi = bit32.bor(bit32.rshift(P2, 12), bit32.lshift(P3, 8))

			buffer.writeu32(Arr, BBase, Lo)
			buffer.writeu8(Arr, BBase + 4, bit32.band(Hi, 0xFF))
		end

	elseif Sbw == 13 then
		for I = 0, 31 do
			local PBase = I * 32
			local BBase = I * 13

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0x1FFF)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0x1FFF)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0x1FFF)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0x1FFF)
			local P4 = bit32.band(buffer.readu32(Poly, PBase + 16), 0x1FFF)
			local P5 = bit32.band(buffer.readu32(Poly, PBase + 20), 0x1FFF)
			local P6 = bit32.band(buffer.readu32(Poly, PBase + 24), 0x1FFF)
			local P7 = bit32.band(buffer.readu32(Poly, PBase + 28), 0x1FFF)

			local W0 = bit32.bor(P0, bit32.lshift(P1, 13), bit32.lshift(P2, 26))
			local W1 = bit32.bor(bit32.rshift(P2, 6), bit32.lshift(P3, 7), bit32.lshift(P4, 20))
			local W2 = bit32.bor(bit32.rshift(P4, 12), bit32.lshift(P5, 1), bit32.lshift(P6, 14), bit32.lshift(P7, 27))
			local W3 = bit32.rshift(P7, 5)

			buffer.writeu32(Arr, BBase, W0)
			buffer.writeu32(Arr, BBase + 4, W1)
			buffer.writeu32(Arr, BBase + 8, W2)
			buffer.writeu8(Arr, BBase + 12, W3)
		end

	elseif Sbw == 18 then
		for I = 0, 63 do
			local PBase = I * 16
			local BBase = I * 9

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0x3FFFF)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0x3FFFF)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0x3FFFF)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0x3FFFF)

			local W0 = bit32.bor(P0, bit32.lshift(P1, 18))
			local W1 = bit32.bor(bit32.rshift(P1, 14), bit32.lshift(P2, 4), bit32.lshift(P3, 22))
			local W2 = bit32.rshift(P3, 10)

			buffer.writeu32(Arr, BBase, W0)
			buffer.writeu32(Arr, BBase + 4, W1)
			buffer.writeu8(Arr, BBase + 8, W2)
		end

	elseif Sbw == 20 then
		for I = 0, 63 do
			local PBase = I * 16
			local BBase = I * 10

			local P0 = bit32.band(buffer.readu32(Poly, PBase), 0xFFFFF)
			local P1 = bit32.band(buffer.readu32(Poly, PBase + 4), 0xFFFFF)
			local P2 = bit32.band(buffer.readu32(Poly, PBase + 8), 0xFFFFF)
			local P3 = bit32.band(buffer.readu32(Poly, PBase + 12), 0xFFFFF)

			local W0 = bit32.bor(P0, bit32.lshift(P1, 20))
			local W1 = bit32.bor(bit32.rshift(P1, 12), bit32.lshift(P2, 8), bit32.lshift(P3, 28))
			local W2 = bit32.rshift(P3, 4)

			buffer.writeu32(Arr, BBase, W0)
			buffer.writeu32(Arr, BBase + 4, W1)
			buffer.writeu16(Arr, BBase + 8, W2)
		end

	else
		local Mask = bit32.lshift(1, Sbw) - 1
		local BitPos = 0

		for I = 0, N - 1 do
			local Value = bit32.band(buffer.readu32(Poly, I * 4), Mask)
			local BitsRemaining = Sbw

			while BitsRemaining > 0 do
				local ByteIdx = bit32.rshift(BitPos, 3)
				local BitOffset = bit32.band(BitPos, 7)
				local BitsInByte = math.min(BitsRemaining, 8 - BitOffset)

				local BitMask = bit32.lshift(1, BitsInByte) - 1
				local Bits = bit32.band(Value, BitMask)
				local Current = buffer.readu8(Arr, ByteIdx)

				buffer.writeu8(Arr, ByteIdx, bit32.bor(Current, bit32.lshift(Bits, BitOffset)))

				Value = bit32.rshift(Value, BitsInByte)
				BitPos += BitsInByte
				BitsRemaining -= BitsInByte
			end
		end
	end
end

function BitPacking.Decode(Arr: buffer, Poly: buffer, Sbw: number)
	buffer.fill(Poly, 0, 0, POLY_BYTES)

	if Sbw == 3 then
		for I = 0, 31 do
			local BBase = I * 3
			local PBase = I * 32

			local B0 = buffer.readu8(Arr, BBase)
			local B1 = buffer.readu8(Arr, BBase + 1)
			local B2 = buffer.readu8(Arr, BBase + 2)

			buffer.writeu32(Poly, PBase, bit32.band(B0, 0x7))
			buffer.writeu32(Poly, PBase + 4, bit32.band(bit32.rshift(B0, 3), 0x7))
			buffer.writeu32(Poly, PBase + 8, bit32.bor(bit32.rshift(B0, 6), bit32.lshift(bit32.band(B1, 0x1), 2)))
			buffer.writeu32(Poly, PBase + 12, bit32.band(bit32.rshift(B1, 1), 0x7))
			buffer.writeu32(Poly, PBase + 16, bit32.band(bit32.rshift(B1, 4), 0x7))
			buffer.writeu32(Poly, PBase + 20, bit32.bor(bit32.rshift(B1, 7), bit32.lshift(bit32.band(B2, 0x3), 1)))
			buffer.writeu32(Poly, PBase + 24, bit32.band(bit32.rshift(B2, 2), 0x7))
			buffer.writeu32(Poly, PBase + 28, bit32.rshift(B2, 5))
		end

	elseif Sbw == 4 then
		for I = 0, 31 do
			local BBase = I * 4
			local PBase = I * 32

			local Word = buffer.readu32(Arr, BBase)

			buffer.writeu32(Poly, PBase, bit32.band(Word, 0xF))
			buffer.writeu32(Poly, PBase + 4, bit32.band(bit32.rshift(Word, 4), 0xF))
			buffer.writeu32(Poly, PBase + 8, bit32.band(bit32.rshift(Word, 8), 0xF))
			buffer.writeu32(Poly, PBase + 12, bit32.band(bit32.rshift(Word, 12), 0xF))
			buffer.writeu32(Poly, PBase + 16, bit32.band(bit32.rshift(Word, 16), 0xF))
			buffer.writeu32(Poly, PBase + 20, bit32.band(bit32.rshift(Word, 20), 0xF))
			buffer.writeu32(Poly, PBase + 24, bit32.band(bit32.rshift(Word, 24), 0xF))
			buffer.writeu32(Poly, PBase + 28, bit32.rshift(Word, 28))
		end

	elseif Sbw == 6 then
		for I = 0, 63 do
			local BBase = I * 3
			local PBase = I * 16

			local B0 = buffer.readu8(Arr, BBase)
			local B1 = buffer.readu8(Arr, BBase + 1)
			local B2 = buffer.readu8(Arr, BBase + 2)

			local Packed = bit32.bor(B0, bit32.lshift(B1, 8), bit32.lshift(B2, 16))

			buffer.writeu32(Poly, PBase, bit32.band(Packed, 0x3F))
			buffer.writeu32(Poly, PBase + 4, bit32.band(bit32.rshift(Packed, 6), 0x3F))
			buffer.writeu32(Poly, PBase + 8, bit32.band(bit32.rshift(Packed, 12), 0x3F))
			buffer.writeu32(Poly, PBase + 12, bit32.rshift(Packed, 18))
		end

	elseif Sbw == 10 then
		for I = 0, 63 do
			local BBase = I * 5
			local PBase = I * 16

			local Lo = buffer.readu32(Arr, BBase)
			local Hi = buffer.readu8(Arr, BBase + 4)

			buffer.writeu32(Poly, PBase, bit32.band(Lo, 0x3FF))
			buffer.writeu32(Poly, PBase + 4, bit32.band(bit32.rshift(Lo, 10), 0x3FF))
			buffer.writeu32(Poly, PBase + 8, bit32.bor(bit32.rshift(Lo, 20), bit32.lshift(bit32.band(Hi, 0x3), 12)))
			buffer.writeu32(Poly, PBase + 12, bit32.rshift(Hi, 2))
		end

	elseif Sbw == 13 then
		for I = 0, 31 do
			local BBase = I * 13
			local PBase = I * 32

			local W0 = buffer.readu32(Arr, BBase)
			local W1 = buffer.readu32(Arr, BBase + 4)
			local W2 = buffer.readu32(Arr, BBase + 8)
			local W3 = buffer.readu8(Arr, BBase + 12)

			buffer.writeu32(Poly, PBase, bit32.band(W0, 0x1FFF))
			buffer.writeu32(Poly, PBase + 4, bit32.band(bit32.rshift(W0, 13), 0x1FFF))
			buffer.writeu32(Poly, PBase + 8, bit32.bor(bit32.rshift(W0, 26), bit32.lshift(bit32.band(W1, 0x7F), 6)))
			buffer.writeu32(Poly, PBase + 12, bit32.band(bit32.rshift(W1, 7), 0x1FFF))
			buffer.writeu32(Poly, PBase + 16, bit32.bor(bit32.rshift(W1, 20), bit32.lshift(bit32.band(W2, 0x1), 12)))
			buffer.writeu32(Poly, PBase + 20, bit32.band(bit32.rshift(W2, 1), 0x1FFF))
			buffer.writeu32(Poly, PBase + 24, bit32.band(bit32.rshift(W2, 14), 0x1FFF))
			buffer.writeu32(Poly, PBase + 28, bit32.bor(bit32.rshift(W2, 27), bit32.lshift(W3, 5)))
		end

	elseif Sbw == 18 then
		for I = 0, 63 do
			local BBase = I * 9
			local PBase = I * 16

			local W0 = buffer.readu32(Arr, BBase)
			local W1 = buffer.readu32(Arr, BBase + 4)
			local W2 = buffer.readu8(Arr, BBase + 8)

			buffer.writeu32(Poly, PBase, bit32.band(W0, 0x3FFFF))
			buffer.writeu32(Poly, PBase + 4, bit32.bor(bit32.rshift(W0, 18), bit32.lshift(bit32.band(W1, 0xF), 14)))
			buffer.writeu32(Poly, PBase + 8, bit32.band(bit32.rshift(W1, 4), 0x3FFFF))
			buffer.writeu32(Poly, PBase + 12, bit32.bor(bit32.rshift(W1, 22), bit32.lshift(W2, 10)))
		end

	elseif Sbw == 20 then
		for I = 0, 63 do
			local BBase = I * 10
			local PBase = I * 16

			local W0 = buffer.readu32(Arr, BBase)
			local W1 = buffer.readu32(Arr, BBase + 4)
			local W2 = buffer.readu16(Arr, BBase + 8)

			buffer.writeu32(Poly, PBase, bit32.band(W0, 0xFFFFF))
			buffer.writeu32(Poly, PBase + 4, bit32.bor(bit32.rshift(W0, 20), bit32.lshift(bit32.band(W1, 0xFF), 12)))
			buffer.writeu32(Poly, PBase + 8, bit32.bor(bit32.rshift(W1, 8), bit32.lshift(bit32.band(W2, 0xF), 24)))
			buffer.writeu32(Poly, PBase + 12, bit32.bor(bit32.rshift(W1, 28), bit32.lshift(W2, 4)))
		end

	else
		local Mask = bit32.lshift(1, Sbw) - 1
		local BitPos = 0

		for I = 0, N - 1 do
			local Value = 0
			local BitsCollected = 0

			while BitsCollected < Sbw do
				local ByteIdx = bit32.rshift(BitPos, 3)
				local BitOffset = bit32.band(BitPos, 7)
				local BitsAvailable = 8 - BitOffset
				local BitsToRead = math.min(Sbw - BitsCollected, BitsAvailable)

				local ByteVal = buffer.readu8(Arr, ByteIdx)
				local Extracted = bit32.band(bit32.rshift(ByteVal, BitOffset), bit32.lshift(1, BitsToRead) - 1)

				Value = bit32.bor(Value, bit32.lshift(Extracted, BitsCollected))

				BitPos += BitsToRead
				BitsCollected += BitsToRead
			end

			buffer.writeu32(Poly, I * 4, bit32.band(Value, Mask))
		end
	end
end

function BitPacking.EncodeHintBits(H: buffer, Arr: buffer, K: number, Omega: number)
	buffer.fill(Arr, 0, 0, Omega + K)

	local Idx = 0

	for I = 0, K - 1 do
		local PolyOffset = I * N * 4

		for J = 0, N - 1 do
			if buffer.readu32(H, PolyOffset + J * 4) ~= 0 then
				buffer.writeu8(Arr, Idx, J)
				Idx += 1

				if Idx >= Omega then
					break
				end
			end
		end

		buffer.writeu8(Arr, Omega + I, Idx)
	end
end

function BitPacking.DecodeHintBits(Arr: buffer, H: buffer, K: number, Omega: number): boolean
	buffer.fill(H, 0, 0, K * N * 4)

	local Idx = 0
	local Failed = false

	for I = 0, K - 1 do
		local PolyOffset = I * N * 4
		local Till = buffer.readu8(Arr, Omega + I)

		if Till < Idx or Till > Omega then
			Failed = true
		end

		if not Failed then
			local Prev = -1

			for J = Idx, Till - 1 do
				local Position = buffer.readu8(Arr, J)

				if Position <= Prev or Position >= N then
					Failed = true
					break
				end

				buffer.writeu32(H, PolyOffset + Position * 4, 1)
				Prev = Position
			end
		end

		Idx = Till
	end

	if not Failed then
		for I = Idx, Omega - 1 do
			if buffer.readu8(Arr, I) ~= 0 then
				Failed = true
				break
			end
		end
	end

	return Failed
end

return BitPacking


--[=[
	Parameter validation for ML-DSA algorithms

	FIPS 204 specification.

	Example usage:
		local Params = require(script)

		local IsValid = Params.CheckKeygenParams(4, 4, 13, 2) -- ML-DSA-44
		local ValidEta = Params.CheckEta(2)
--]=]

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

local Q = 8380417

local Params = {}

function Params.CheckEta(Eta: number): boolean
	return Eta == 2 or Eta == 4
end

function Params.CheckNonce(Nonce: number): boolean
	return Nonce == 0 or Nonce == 4 or Nonce == 5 or Nonce == 7
end

function Params.CheckGamma1(Gamma1: number): boolean
	return Gamma1 == bit32.lshift(1, 17) or Gamma1 == bit32.lshift(1, 19)
end

function Params.CheckGamma2(Gamma2: number): boolean
	return Gamma2 == math.floor((Q - 1) / 88) or Gamma2 == math.floor((Q - 1) / 32)
end

function Params.CheckTau(Tau: number): boolean
	return Tau == 39 or Tau == 49 or Tau == 60
end

function Params.CheckD(D: number): boolean
	return D == 13
end

function Params.CheckKeygenParams(K: number, L: number, D: number, Eta: number): boolean
	return (K == 4 and L == 4 and D == 13 and Eta == 2) or
		(K == 6 and L == 5 and D == 13 and Eta == 4) or
		(K == 8 and L == 7 and D == 13 and Eta == 2)
end

function Params.CheckSigningParams(K: number, L: number, D: number, Eta: number, Gamma1: number, Gamma2: number, Tau: number, Beta: number, Omega: number, Lambda: number): boolean
	return (K == 4 and L == 4 and D == 13 and Eta == 2 and
		Gamma1 == bit32.lshift(1, 17) and
		Gamma2 == math.floor((Q - 1) / 88) and
		Tau == 39 and Beta == Tau * Eta and Omega == 80 and Lambda == 128) or
		(K == 6 and L == 5 and D == 13 and Eta == 4 and
			Gamma1 == bit32.lshift(1, 19) and
			Gamma2 == math.floor((Q - 1) / 32) and
			Tau == 49 and Beta == Tau * Eta and Omega == 55 and Lambda == 192) or
		(K == 8 and L == 7 and D == 13 and Eta == 2 and
			Gamma1 == bit32.lshift(1, 19) and
			Gamma2 == math.floor((Q - 1) / 32) and
			Tau == 60 and Beta == Tau * Eta and Omega == 75 and Lambda == 256)
end

function Params.CheckVerifyParams(K: number, L: number, D: number, Gamma1: number, Gamma2: number, Tau: number, Beta: number, Omega: number, Lambda: number): boolean
	return (K == 4 and L == 4 and D == 13 and
		Gamma1 == bit32.lshift(1, 17) and
		Gamma2 == math.floor((Q - 1) / 88) and
		Tau == 39 and Beta == Tau * 2 and Omega == 80 and Lambda == 128) or
		(K == 6 and L == 5 and D == 13 and
			Gamma1 == bit32.lshift(1, 19) and
			Gamma2 == math.floor((Q - 1) / 32) and
			Tau == 49 and Beta == Tau * 4 and Omega == 55 and Lambda == 192) or
		(K == 8 and L == 7 and D == 13 and
			Gamma1 == bit32.lshift(1, 19) and
			Gamma2 == math.floor((Q - 1) / 32) and
			Tau == 60 and Beta == Tau * 2 and Omega == 75 and Lambda == 256)
end

return Params


--[=[
	Vector operations on degree-255 polynomials

	Works on vectors of polynomials including matrix multiplication,
	NTT transforms, and encoding/decoding for ML-DSA.

	Example usage:
		local PolyVec = require(script)

		local Vec = buffer.create(4 * 256 * 4) -- 4 polynomials
		PolyVec.ForwardNTT(Vec, 4)
		local Norm = PolyVec.InfinityNorm(Vec, 4)
--]=]

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

local Field = require("./Field")
local NTT = require("./NTT")
local BitPacking = require("./Pack")

local LOG2N = 8
local ZETA = 1753
local Q = 8380417

local N = 256
local INV_N = Field.Inverse(N)

local ZETA_EXP, ZETA_NEG_EXP = buffer.create(N * 4), buffer.create(N * 4) do
	for I = 0, N - 1 do
		local Reversed = 0
		local Value = I

		for J = 0, LOG2N - 1 do
			local Bit = bit32.band(bit32.rshift(Value, J), 1)
			Reversed = bit32.bxor(Reversed, bit32.lshift(Bit, LOG2N - 1 - J))
		end

		local ZetaExp = Field.Power(ZETA, Reversed)
		local ZetaNegExp = Field.Negate(ZetaExp)

		buffer.writeu32(ZETA_EXP, I * 4, ZetaExp)
		buffer.writeu32(ZETA_NEG_EXP, I * 4, ZetaNegExp)
	end
end

local PolyVec = {}

function PolyVec.ForwardNTT(Vec: buffer, K: number)
	local Num = N
	for I = 0, K - 1 do
		local Offset = I * Num * 4
		NTT.ForwardNTTWithOffset(Vec, Offset)
	end
end

function PolyVec.InverseNTT(Vec: buffer, K: number)
	local Zeta = ZETA_NEG_EXP
	local InvN = INV_N
	local Num = N
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for L = 0, 7 do
			local Len = bit32.lshift(1, L)
			local LenX2 = bit32.lshift(Len, 1)
			local KBeg = bit32.rshift(Num, L) - 1

			for Start = 0, Num - 1, LenX2 do
				local KNow = KBeg - bit32.rshift(Start, L + 1)
				local NegZetaExpValue = buffer.readu32(Zeta, KNow * 4)

				for J = Start, Start + Len - 1 do
					local JOffset = Offset + J * 4
					local JLenOffset = Offset + (J + Len) * 4

					local PolyJ = buffer.readu32(Vec, JOffset)
					local PolyJLen = buffer.readu32(Vec, JLenOffset)

					local Sum = PolyJ + PolyJLen
					Sum = if Sum >= Modulus then Sum - Modulus else Sum

					local Diff = if PolyJ >= PolyJLen then PolyJ - PolyJLen else PolyJ - PolyJLen + Modulus
					local Product = (Diff * NegZetaExpValue) % Modulus

					buffer.writeu32(Vec, JOffset, Sum)
					buffer.writeu32(Vec, JLenOffset, Product)
				end
			end
		end

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local PolyValue = buffer.readu32(Vec, JOffset)
			local Result = (PolyValue * InvN) % Modulus
			buffer.writeu32(Vec, JOffset, Result)
		end
	end
end

function PolyVec.Power2Round(PolyVec: buffer, PolyHi: buffer, PolyLo: buffer, K: number, D: number)
	local Max = bit32.lshift(1, D - 1)
	local Num = N
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local R = buffer.readu32(PolyVec, JOffset)

			local T1 = R + Max - 1
			local T2 = bit32.rshift(T1, D)
			local T3 = bit32.lshift(T2, D)

			local Hi = T2
			local Lo = if R >= T3 then R - T3 else R - T3 + Modulus

			buffer.writeu32(PolyHi, JOffset, Hi)
			buffer.writeu32(PolyLo, JOffset, Lo)
		end
	end
end

function PolyVec.MatrixMultiply(A: buffer, B: buffer, C: buffer, ARows: number, ACols: number, BRows: number, BCols: number)
	local Num = N
	local Modulus = Q

	buffer.fill(C, 0, 0, ARows * BCols * N * 4)

	for I = 0, ARows - 1 do
		for J = 0, BCols - 1 do
			local COffset = (I * BCols + J) * Num * 4

			for K = 0, ACols - 1 do
				local AOffset = (I * ACols + K) * Num * 4
				local BOffset = (K * BCols + J) * Num * 4

				for L = 0, Num - 1 do
					local ElementOffset = L * 4
					local AValue = buffer.readu32(A, AOffset + ElementOffset)
					local BValue = buffer.readu32(B, BOffset + ElementOffset)

					local Product = (AValue * BValue) % Modulus

					local CValue = buffer.readu32(C, COffset + ElementOffset)
					local Sum = CValue + Product
					Sum = if Sum >= Modulus then Sum - Modulus else Sum

					buffer.writeu32(C, COffset + ElementOffset, Sum)
				end
			end
		end
	end
end

function PolyVec.AddTo(Src: buffer, Dst: buffer, K: number)
	local TotalElements = K * N
	local Modulus = Q

	for I = 0, TotalElements - 1 do
		local Offset = I * 4
		local SrcValue = buffer.readu32(Src, Offset)
		local DstValue = buffer.readu32(Dst, Offset)

		local Sum = DstValue + SrcValue
		Sum = if Sum >= Modulus then Sum - Modulus else Sum

		buffer.writeu32(Dst, Offset, Sum)
	end
end

function PolyVec.Negate(Vec: buffer, K: number)
	local TotalElements = K * N
	local Modulus = Q

	for I = 0, TotalElements - 1 do
		local Offset = I * 4
		local Value = buffer.readu32(Vec, Offset)
		local Negated = if Value == 0 then 0 else Modulus - Value

		buffer.writeu32(Vec, Offset, Negated)
	end
end

function PolyVec.SubFromX(Vec: buffer, K: number, X: number)
	local Modulus = Q
	local Num = N
	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local PolyValue = buffer.readu32(Vec, JOffset)

			local Result = if X >= PolyValue then X - PolyValue else X - PolyValue + Modulus

			buffer.writeu32(Vec, JOffset, Result)
		end
	end
end

function PolyVec.Encode(Src: buffer, Dst: buffer, K: number, Sbw: number)
	local PolyByteLength = math.floor((N * Sbw) / 8)
	local Num = N

	for I = 0, K - 1 do
		local SrcOffset = I * Num * 4
		local DstOffset = I * PolyByteLength

		if Sbw == 3 then
			local ItrCnt = 32
			for J = 0, ItrCnt - 1 do
				local POffset = SrcOffset + J * 32
				local BOffset = DstOffset + J * 3

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0x7)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0x7)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0x7)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0x7)
				local P4 = bit32.band(buffer.readu32(Src, POffset + 16), 0x7)
				local P5 = bit32.band(buffer.readu32(Src, POffset + 20), 0x7)
				local P6 = bit32.band(buffer.readu32(Src, POffset + 24), 0x7)
				local P7 = bit32.band(buffer.readu32(Src, POffset + 28), 0x7)

				buffer.writeu8(Dst, BOffset + 0, bit32.bor(bit32.lshift(bit32.band(P2, 0x3), 6), bit32.lshift(P1, 3), P0))
				buffer.writeu8(Dst, BOffset + 1, bit32.bor(bit32.lshift(bit32.band(P5, 0x1), 7), bit32.lshift(P4, 4), bit32.lshift(P3, 1), bit32.rshift(P2, 2)))
				buffer.writeu8(Dst, BOffset + 2, bit32.bor(bit32.lshift(P7, 5), bit32.lshift(P6, 2), bit32.rshift(P5, 1)))
			end
		elseif Sbw == 4 then
			for J = 0, 127 do
				local POffset = SrcOffset + J * 8
				local P0 = bit32.band(buffer.readu32(Src, POffset), 0xF)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0xF)
				buffer.writeu8(Dst, DstOffset + J, bit32.bor(bit32.lshift(P1, 4), P0))
			end
		elseif Sbw == 6 then
			for J = 0, 63 do
				local POffset = SrcOffset + J * 16
				local BOffset = DstOffset + J * 3

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0x3F)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0x3F)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0x3F)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0x3F)

				buffer.writeu8(Dst, BOffset + 0, bit32.bor(bit32.lshift(bit32.band(P1, 0x3), 6), P0))
				buffer.writeu8(Dst, BOffset + 1, bit32.bor(bit32.lshift(bit32.band(P2, 0xF), 4), bit32.rshift(P1, 2)))
				buffer.writeu8(Dst, BOffset + 2, bit32.bor(bit32.lshift(P3, 2), bit32.rshift(P2, 4)))
			end
		elseif Sbw == 10 then
			for J = 0, 63 do
				local POffset = SrcOffset + J * 16
				local BOffset = DstOffset + J * 5

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0x3FF)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0x3FF)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0x3FF)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0x3FF)

				buffer.writeu8(Dst, BOffset + 0, bit32.band(P0, 0xFF))
				buffer.writeu8(Dst, BOffset + 1, bit32.bor(bit32.lshift(bit32.band(P1, 0x3F), 2), bit32.rshift(P0, 8)))
				buffer.writeu8(Dst, BOffset + 2, bit32.bor(bit32.lshift(bit32.band(P2, 0xF), 4), bit32.rshift(P1, 6)))
				buffer.writeu8(Dst, BOffset + 3, bit32.bor(bit32.lshift(bit32.band(P3, 0x3), 6), bit32.rshift(P2, 4)))
				buffer.writeu8(Dst, BOffset + 4, bit32.rshift(P3, 2))
			end
		elseif Sbw == 13 then
			for J = 0, 31 do
				local POffset = SrcOffset + J * 32
				local BOffset = DstOffset + J * 13

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0x1FFF)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0x1FFF)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0x1FFF)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0x1FFF)
				local P4 = bit32.band(buffer.readu32(Src, POffset + 16), 0x1FFF)
				local P5 = bit32.band(buffer.readu32(Src, POffset + 20), 0x1FFF)
				local P6 = bit32.band(buffer.readu32(Src, POffset + 24), 0x1FFF)
				local P7 = bit32.band(buffer.readu32(Src, POffset + 28), 0x1FFF)

				buffer.writeu8(Dst, BOffset + 0, bit32.band(P0, 0xFF))
				buffer.writeu8(Dst, BOffset + 1, bit32.bor(bit32.lshift(bit32.band(P1, 0x7), 5), bit32.rshift(P0, 8)))
				buffer.writeu8(Dst, BOffset + 2, bit32.rshift(P1, 3))
				buffer.writeu8(Dst, BOffset + 3, bit32.bor(bit32.lshift(bit32.band(P2, 0x3F), 2), bit32.rshift(P1, 11)))
				buffer.writeu8(Dst, BOffset + 4, bit32.bor(bit32.lshift(bit32.band(P3, 0x1), 7), bit32.rshift(P2, 6)))
				buffer.writeu8(Dst, BOffset + 5, bit32.rshift(P3, 1))
				buffer.writeu8(Dst, BOffset + 6, bit32.bor(bit32.lshift(bit32.band(P4, 0xF), 4), bit32.rshift(P3, 9)))
				buffer.writeu8(Dst, BOffset + 7, bit32.rshift(P4, 4))
				buffer.writeu8(Dst, BOffset + 8, bit32.bor(bit32.lshift(bit32.band(P5, 0x7F), 1), bit32.rshift(P4, 12)))
				buffer.writeu8(Dst, BOffset + 9, bit32.bor(bit32.lshift(bit32.band(P6, 0x3), 6), bit32.rshift(P5, 7)))
				buffer.writeu8(Dst, BOffset + 10, bit32.rshift(P6, 2))
				buffer.writeu8(Dst, BOffset + 11, bit32.bor(bit32.lshift(bit32.band(P7, 0x1F), 3), bit32.rshift(P6, 10)))
				buffer.writeu8(Dst, BOffset + 12, bit32.rshift(P7, 5))
			end
		elseif Sbw == 18 then
			for J = 0, 63 do
				local POffset = SrcOffset + J * 16
				local BOffset = DstOffset + J * 9

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0x3FFFF)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0x3FFFF)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0x3FFFF)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0x3FFFF)

				buffer.writeu8(Dst, BOffset + 0, bit32.band(P0, 0xFF))
				buffer.writeu8(Dst, BOffset + 1, bit32.band(bit32.rshift(P0, 8), 0xFF))
				buffer.writeu8(Dst, BOffset + 2, bit32.bor(bit32.rshift(P0, 16), bit32.lshift(bit32.band(P1, 0x3F), 2)))
				buffer.writeu8(Dst, BOffset + 3, bit32.band(bit32.rshift(P1, 6), 0xFF))
				buffer.writeu8(Dst, BOffset + 4, bit32.bor(bit32.rshift(P1, 14), bit32.lshift(bit32.band(P2, 0xF), 4)))
				buffer.writeu8(Dst, BOffset + 5, bit32.band(bit32.rshift(P2, 4), 0xFF))
				buffer.writeu8(Dst, BOffset + 6, bit32.bor(bit32.rshift(P2, 12), bit32.lshift(bit32.band(P3, 0x3), 6)))
				buffer.writeu8(Dst, BOffset + 7, bit32.band(bit32.rshift(P3, 2), 0xFF))
				buffer.writeu8(Dst, BOffset + 8, bit32.rshift(P3, 10))
			end
		elseif Sbw == 20 then
			for J = 0, 63 do
				local POffset = SrcOffset + J * 16
				local BOffset = DstOffset + J * 10

				local P0 = bit32.band(buffer.readu32(Src, POffset + 0), 0xFFFFF)
				local P1 = bit32.band(buffer.readu32(Src, POffset + 4), 0xFFFFF)
				local P2 = bit32.band(buffer.readu32(Src, POffset + 8), 0xFFFFF)
				local P3 = bit32.band(buffer.readu32(Src, POffset + 12), 0xFFFFF)

				buffer.writeu8(Dst, BOffset + 0, bit32.band(P0, 0xFF))
				buffer.writeu8(Dst, BOffset + 1, bit32.band(bit32.rshift(P0, 8), 0xFF))
				buffer.writeu8(Dst, BOffset + 2, bit32.bor(bit32.rshift(P0, 16), bit32.lshift(bit32.band(P1, 0xF), 4)))
				buffer.writeu8(Dst, BOffset + 3, bit32.band(bit32.rshift(P1, 4), 0xFF))
				buffer.writeu8(Dst, BOffset + 4, bit32.band(bit32.rshift(P1, 12), 0xFF))
				buffer.writeu8(Dst, BOffset + 5, bit32.band(P2, 0xFF))
				buffer.writeu8(Dst, BOffset + 6, bit32.band(bit32.rshift(P2, 8), 0xFF))
				buffer.writeu8(Dst, BOffset + 7, bit32.bor(bit32.rshift(P2, 16), bit32.lshift(bit32.band(P3, 0xF), 4)))
				buffer.writeu8(Dst, BOffset + 8, bit32.band(bit32.rshift(P3, 4), 0xFF))
				buffer.writeu8(Dst, BOffset + 9, bit32.rshift(P3, 12))
			end
		else
			local PolyBuffer = buffer.create(N * 4)
			local ByteBuffer = buffer.create(PolyByteLength)
			buffer.copy(PolyBuffer, 0, Src, SrcOffset, N * 4)
			BitPacking.Encode(PolyBuffer, ByteBuffer, Sbw)
			buffer.copy(Dst, DstOffset, ByteBuffer, 0, PolyByteLength)
		end
	end
end

function PolyVec.Decode(Src: buffer, Dst: buffer, K: number, Sbw: number)
	local PolyByteLength = math.floor((N * Sbw) / 8)
	local Num = N

	for I = 0, K - 1 do
		local SrcOffset = I * PolyByteLength
		local DstOffset = I * Num * 4

		if Sbw == 3 then
			for J = 0, 31 do
				local BOffset = SrcOffset + J * 3
				local POffset = DstOffset + J * 32

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)

				buffer.writeu32(Dst, POffset + 0, bit32.band(B0, 0x7))
				buffer.writeu32(Dst, POffset + 4, bit32.band(bit32.rshift(B0, 3), 0x7))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(bit32.rshift(B0, 6), bit32.lshift(bit32.band(B1, 0x1), 2)))
				buffer.writeu32(Dst, POffset + 12, bit32.band(bit32.rshift(B1, 1), 0x7))
				buffer.writeu32(Dst, POffset + 16, bit32.band(bit32.rshift(B1, 4), 0x7))
				buffer.writeu32(Dst, POffset + 20, bit32.bor(bit32.rshift(B1, 7), bit32.lshift(bit32.band(B2, 0x3), 1)))
				buffer.writeu32(Dst, POffset + 24, bit32.band(bit32.rshift(B2, 2), 0x7))
				buffer.writeu32(Dst, POffset + 28, bit32.rshift(B2, 5))
			end
		elseif Sbw == 4 then
			for J = 0, 127 do
				local B = buffer.readu8(Src, SrcOffset + J)
				local POffset = DstOffset + J * 8
				buffer.writeu32(Dst, POffset, bit32.band(B, 0xF))
				buffer.writeu32(Dst, POffset + 4, bit32.rshift(B, 4))
			end
		elseif Sbw == 6 then
			for J = 0, 63 do
				local BOffset = SrcOffset + J * 3
				local POffset = DstOffset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)

				buffer.writeu32(Dst, POffset + 0, bit32.band(B0, 0x3F))
				buffer.writeu32(Dst, POffset + 4, bit32.bor(bit32.rshift(B0, 6), bit32.lshift(bit32.band(B1, 0xF), 2)))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(bit32.rshift(B1, 4), bit32.lshift(bit32.band(B2, 0x3), 4)))
				buffer.writeu32(Dst, POffset + 12, bit32.rshift(B2, 2))
			end
		elseif Sbw == 10 then
			for J = 0, 63 do
				local BOffset = SrcOffset + J * 5
				local POffset = DstOffset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)

				buffer.writeu32(Dst, POffset + 0, bit32.bor(B0, bit32.lshift(bit32.band(B1, 0x3), 8)))
				buffer.writeu32(Dst, POffset + 4, bit32.bor(bit32.rshift(B1, 2), bit32.lshift(bit32.band(B2, 0xF), 6)))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(bit32.rshift(B2, 4), bit32.lshift(bit32.band(B3, 0x3F), 4)))
				buffer.writeu32(Dst, POffset + 12, bit32.bor(bit32.rshift(B3, 6), bit32.lshift(B4, 2)))
			end
		elseif Sbw == 13 then
			for J = 0, 31 do
				local BOffset = SrcOffset + J * 13
				local POffset = DstOffset + J * 32

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)
				local B5 = buffer.readu8(Src, BOffset + 5)
				local B6 = buffer.readu8(Src, BOffset + 6)
				local B7 = buffer.readu8(Src, BOffset + 7)
				local B8 = buffer.readu8(Src, BOffset + 8)
				local B9 = buffer.readu8(Src, BOffset + 9)
				local B10 = buffer.readu8(Src, BOffset + 10)
				local B11 = buffer.readu8(Src, BOffset + 11)
				local B12 = buffer.readu8(Src, BOffset + 12)

				buffer.writeu32(Dst, POffset + 0, bit32.bor(B0, bit32.lshift(bit32.band(B1, 0x1F), 8)))
				buffer.writeu32(Dst, POffset + 4, bit32.bor(bit32.rshift(B1, 5), bit32.lshift(B2, 3), bit32.lshift(bit32.band(B3, 0x3), 11)))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(bit32.rshift(B3, 2), bit32.lshift(bit32.band(B4, 0x7F), 6)))
				buffer.writeu32(Dst, POffset + 12, bit32.bor(bit32.rshift(B4, 7), bit32.lshift(B5, 1), bit32.lshift(bit32.band(B6, 0xF), 9)))
				buffer.writeu32(Dst, POffset + 16, bit32.bor(bit32.rshift(B6, 4), bit32.lshift(B7, 4), bit32.lshift(bit32.band(B8, 0x1), 12)))
				buffer.writeu32(Dst, POffset + 20, bit32.bor(bit32.rshift(B8, 1), bit32.lshift(bit32.band(B9, 0x3F), 7)))
				buffer.writeu32(Dst, POffset + 24, bit32.bor(bit32.rshift(B9, 6), bit32.lshift(B10, 2), bit32.lshift(bit32.band(B11, 0x7), 10)))
				buffer.writeu32(Dst, POffset + 28, bit32.bor(bit32.rshift(B11, 3), bit32.lshift(B12, 5)))
			end
		elseif Sbw == 18 then
			for J = 0, 63 do
				local BOffset = SrcOffset + J * 9
				local POffset = DstOffset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)
				local B5 = buffer.readu8(Src, BOffset + 5)
				local B6 = buffer.readu8(Src, BOffset + 6)
				local B7 = buffer.readu8(Src, BOffset + 7)
				local B8 = buffer.readu8(Src, BOffset + 8)

				buffer.writeu32(Dst, POffset + 0, bit32.bor(B0, bit32.lshift(B1, 8), bit32.lshift(bit32.band(B2, 0x3), 16)))
				buffer.writeu32(Dst, POffset + 4, bit32.bor(bit32.rshift(B2, 2), bit32.lshift(B3, 6), bit32.lshift(bit32.band(B4, 0xF), 14)))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(bit32.rshift(B4, 4), bit32.lshift(B5, 4), bit32.lshift(bit32.band(B6, 0x3F), 12)))
				buffer.writeu32(Dst, POffset + 12, bit32.bor(bit32.rshift(B6, 6), bit32.lshift(B7, 2), bit32.lshift(B8, 10)))
			end
		elseif Sbw == 20 then
			for J = 0, 63 do
				local BOffset = SrcOffset + J * 10
				local POffset = DstOffset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)
				local B5 = buffer.readu8(Src, BOffset + 5)
				local B6 = buffer.readu8(Src, BOffset + 6)
				local B7 = buffer.readu8(Src, BOffset + 7)
				local B8 = buffer.readu8(Src, BOffset + 8)
				local B9 = buffer.readu8(Src, BOffset + 9)

				buffer.writeu32(Dst, POffset + 0, bit32.bor(B0, bit32.lshift(B1, 8), bit32.lshift(bit32.band(B2, 0xF), 16)))
				buffer.writeu32(Dst, POffset + 4, bit32.bor(bit32.rshift(B2, 4), bit32.lshift(B3, 4), bit32.lshift(B4, 12)))
				buffer.writeu32(Dst, POffset + 8, bit32.bor(B5, bit32.lshift(B6, 8), bit32.lshift(bit32.band(B7, 0xF), 16)))
				buffer.writeu32(Dst, POffset + 12, bit32.bor(bit32.rshift(B7, 4), bit32.lshift(B8, 4), bit32.lshift(B9, 12)))
			end
		else
			local ByteBuffer = buffer.create(PolyByteLength)
			local PolyBuffer = buffer.create(N * 4)
			buffer.copy(ByteBuffer, 0, Src, SrcOffset, PolyByteLength)
			BitPacking.Decode(ByteBuffer, PolyBuffer, Sbw)
			buffer.copy(Dst, DstOffset, PolyBuffer, 0, N * 4)
		end
	end
end

function PolyVec.HighBits(Src: buffer, Dst: buffer, K: number, Alpha: number)
	local T0 = bit32.rshift(Alpha, 1)
	local T1 = Q - 1
	local Modulus = Q
	local Num = N

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local R = buffer.readu32(Src, JOffset)

			local T2 = R + T0 - 1
			local T3 = math.floor(T2 / Alpha)
			local T4 = T3 * Alpha

			local R0 = if R >= T4 then R - T4 else R - T4 + Modulus
			local T5 = if R >= R0 then R - R0 else R - R0 + Modulus

			local Flag = (T5 == T1)
			local R1 = if Flag then 0 else T3

			buffer.writeu32(Dst, JOffset, R1)
		end
	end
end

function PolyVec.LowBits(Src: buffer, Dst: buffer, K: number, Alpha: number)
	local T0 = bit32.rshift(Alpha, 1)
	local T1 = Q - 1

	local Modulus = Q
	local Num = N

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local R = buffer.readu32(Src, JOffset)

			local T2 = R + T0 - 1
			local T3 = math.floor(T2 / Alpha)
			local T4 = T3 * Alpha

			local R0 = if R >= T4 then R - T4 else R - T4 + Modulus
			local T5 = if R >= R0 then R - R0 else R - R0 + Modulus

			local Flag = (T5 == T1)
			local R0_ = if Flag then (if R0 >= 1 then R0 - 1 else R0 - 1 + Modulus) else R0

			buffer.writeu32(Dst, JOffset, R0_)
		end
	end
end

function PolyVec.MultiplyByPoly(PolyBuffer: buffer, SrcVec: buffer, DstVec: buffer, K: number)
	local Modulus = Q
	local Num = N

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local PolyValue = buffer.readu32(PolyBuffer, J * 4)
			local SrcValue = buffer.readu32(SrcVec, JOffset)

			local Product = (PolyValue * SrcValue) % Modulus

			buffer.writeu32(DstVec, JOffset, Product)
		end
	end
end

function PolyVec.InfinityNorm(Vec: buffer, K: number): number
	local Result = 0
	local QBy2 = math.floor(Q / 2)
	local Modulus = Q
	local Num = N

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local PolyValue = buffer.readu32(Vec, JOffset)

			local CurrentValue = if PolyValue > QBy2 then (if PolyValue == 0 then 0 else Modulus - PolyValue) else PolyValue
			if CurrentValue > Result then
				Result = CurrentValue
			end
		end
	end

	return Result
end

function PolyVec.MakeHint(PolyA: buffer, PolyB: buffer, PolyC: buffer, K: number, Alpha: number)
	local T0 = bit32.rshift(Alpha, 1)
	local T1 = Q - 1

	local Num = N
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local Z = buffer.readu32(PolyA, JOffset)
			local R = buffer.readu32(PolyB, JOffset)

			local T2_R = R + T0 - 1
			local T3_R = math.floor(T2_R / Alpha)
			local T4_R = T3_R * Alpha
			local R0_R = if R >= T4_R then R - T4_R else R - T4_R + Modulus
			local T5_R = if R >= R0_R then R - R0_R else R - R0_R + Modulus
			local Flag_R = (T5_R == T1)
			local R1 = if Flag_R then 0 else T3_R

			local Sum = R + Z
			Sum = if Sum >= Modulus then Sum - Modulus else Sum

			local T2_V = Sum + T0 - 1
			local T3_V = math.floor(T2_V / Alpha)
			local T4_V = T3_V * Alpha
			local R0_V = if Sum >= T4_V then Sum - T4_V else Sum - T4_V + Modulus
			local T5_V = if Sum >= R0_V then Sum - R0_V else Sum - R0_V + Modulus
			local Flag_V = (T5_V == T1)
			local V1 = if Flag_V then 0 else T3_V

			local Hint = if R1 ~= V1 then 1 else 0
			buffer.writeu32(PolyC, JOffset, Hint)
		end
	end
end

function PolyVec.UseHint(PolyH: buffer, PolyR: buffer, PolyRZ: buffer, K: number, Alpha: number)
	local M = math.floor((Q - 1) / Alpha)
	local T0 = bit32.rshift(Alpha, 1)
	local T1 = Q - T0
	local T1_Q = Q - 1
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * N * 4

		for J = 0, N - 1 do
			local JOffset = Offset + J * 4
			local H = buffer.readu32(PolyH, JOffset)
			local R = buffer.readu32(PolyR, JOffset)

			local T2 = R + T0 - 1
			local T3 = math.floor(T2 / Alpha)
			local T4 = T3 * Alpha

			local R0 = if R >= T4 then R - T4 else R - T4 + Modulus

			local T5 = if R >= R0 then R - R0 else R - R0 + Modulus
			local Flag = (T5 == T1_Q)

			local R1 = if Flag then 0 else T3
			local R0_ = if Flag then (if R0 >= 1 then R0 - 1 else R0 - 1 + Modulus) else R0

			if H == 1 then
				if R0_ > 0 and R0_ < T1 then
					R1 += 1
				else
					R1 -= 1
				end
			end

			buffer.writeu32(PolyRZ, JOffset, (R1 % M + M) % M)
		end
	end
end

function PolyVec.Count1s(Vec: buffer, K: number): number
	local Count = 0

	for I = 0, K * N - 1 do
		local Value = buffer.readu32(Vec, I * 4)
		Count += Value
	end

	return Count
end

function PolyVec.LeftShift(Vec: buffer, K: number, D: number)
	local Num = N
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * Num * 4

		for J = 0, Num - 1 do
			local JOffset = Offset + J * 4
			local PolyValue = buffer.readu32(Vec, JOffset)

			local Shifted = bit32.lshift(PolyValue, D)
			local Result = if Shifted >= Modulus then Shifted % Modulus else Shifted

			buffer.writeu32(Vec, JOffset, Result)
		end
	end
end

function PolyVec.Copy(Src: buffer, Dst: buffer, K: number)
	buffer.copy(Dst, 0, Src, 0, K * N * 4)
end

return PolyVec


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

local SHA3 = {}

local LOW_ROUND, HIGH_ROUND = buffer.create(96), buffer.create(96) do
	local HighFactorKeccak = 0
	local ShiftRegister = 29
	local function GetNextBit(): number
		local Result = ShiftRegister % 2
		ShiftRegister = bit32.bxor((ShiftRegister - Result) // 2, 142 * Result)

		return Result
	end

	for Index = 0, 23 do
		local LowValue = 0
		local Multiplier: number

		for _ = 1, 6 do
			Multiplier = if Multiplier then Multiplier * Multiplier * 2 else 1
			LowValue += GetNextBit() * Multiplier
		end

		local HighValue = GetNextBit() * Multiplier
		buffer.writeu32(HIGH_ROUND, Index * 4, HighValue)
		buffer.writeu32(LOW_ROUND, Index * 4, LowValue + HighValue * HighFactorKeccak)
	end
end

local LANES_LOW = buffer.create(100)
local LANES_HIGH = buffer.create(100)

local function Keccak(LanesLow: buffer, LanesHigh: buffer, InputBuffer: buffer, Offset: number, Size: number, BlockSizeInBytes: number): ()
	local QuadWordsQuantity = BlockSizeInBytes // 8
	local RCHigh, RCLow = HIGH_ROUND, LOW_ROUND

	for Position = Offset, Offset + Size - 1, BlockSizeInBytes do
		for Index = 0, (QuadWordsQuantity - 1) * 4, 4 do
			local BufferPos = Position + Index * 2

			buffer.writeu32(LanesLow, Index, bit32.bxor(
				buffer.readu32(LanesLow, Index),
				buffer.readu32(InputBuffer, BufferPos)
				))

			buffer.writeu32(LanesHigh, Index, bit32.bxor(
				buffer.readu32(LanesHigh, Index),
				buffer.readu32(InputBuffer, BufferPos + 4)
				))
		end

		local Lane01Low, Lane01High = buffer.readu32(LanesLow, 0), buffer.readu32(LanesHigh, 0)
		local Lane02Low, Lane02High = buffer.readu32(LanesLow, 4), buffer.readu32(LanesHigh, 4)
		local Lane03Low, Lane03High = buffer.readu32(LanesLow, 8), buffer.readu32(LanesHigh, 8)

		local Lane04Low, Lane04High = buffer.readu32(LanesLow, 12), buffer.readu32(LanesHigh, 12)
		local Lane05Low, Lane05High = buffer.readu32(LanesLow, 16), buffer.readu32(LanesHigh, 16)
		local Lane06Low, Lane06High = buffer.readu32(LanesLow, 20), buffer.readu32(LanesHigh, 20)

		local Lane07Low, Lane07High = buffer.readu32(LanesLow, 24), buffer.readu32(LanesHigh, 24)
		local Lane08Low, Lane08High = buffer.readu32(LanesLow, 28), buffer.readu32(LanesHigh, 28)
		local Lane09Low, Lane09High = buffer.readu32(LanesLow, 32), buffer.readu32(LanesHigh, 32)

		local Lane10Low, Lane10High = buffer.readu32(LanesLow, 36), buffer.readu32(LanesHigh, 36)
		local Lane11Low, Lane11High = buffer.readu32(LanesLow, 40), buffer.readu32(LanesHigh, 40)
		local Lane12Low, Lane12High = buffer.readu32(LanesLow, 44), buffer.readu32(LanesHigh, 44)

		local Lane13Low, Lane13High = buffer.readu32(LanesLow, 48), buffer.readu32(LanesHigh, 48)
		local Lane14Low, Lane14High = buffer.readu32(LanesLow, 52), buffer.readu32(LanesHigh, 52)
		local Lane15Low, Lane15High = buffer.readu32(LanesLow, 56), buffer.readu32(LanesHigh, 56)

		local Lane16Low, Lane16High = buffer.readu32(LanesLow, 60), buffer.readu32(LanesHigh, 60)
		local Lane17Low, Lane17High = buffer.readu32(LanesLow, 64), buffer.readu32(LanesHigh, 64)
		local Lane18Low, Lane18High = buffer.readu32(LanesLow, 68), buffer.readu32(LanesHigh, 68)

		local Lane19Low, Lane19High = buffer.readu32(LanesLow, 72), buffer.readu32(LanesHigh, 72)
		local Lane20Low, Lane20High = buffer.readu32(LanesLow, 76), buffer.readu32(LanesHigh, 76)
		local Lane21Low, Lane21High = buffer.readu32(LanesLow, 80), buffer.readu32(LanesHigh, 80)

		local Lane22Low, Lane22High = buffer.readu32(LanesLow, 84), buffer.readu32(LanesHigh, 84)
		local Lane23Low, Lane23High = buffer.readu32(LanesLow, 88), buffer.readu32(LanesHigh, 88)
		local Lane24Low, Lane24High = buffer.readu32(LanesLow, 92), buffer.readu32(LanesHigh, 92)

		local Lane25Low, Lane25High = buffer.readu32(LanesLow, 96), buffer.readu32(LanesHigh, 96)

		for RoundIndex = 0, 92, 4 do
			local Column1Low, Column1High = bit32.bxor(Lane01Low, Lane06Low, Lane11Low, Lane16Low, Lane21Low), bit32.bxor(Lane01High, Lane06High, Lane11High, Lane16High, Lane21High)
			local Column2Low, Column2High = bit32.bxor(Lane02Low, Lane07Low, Lane12Low, Lane17Low, Lane22Low), bit32.bxor(Lane02High, Lane07High, Lane12High, Lane17High, Lane22High)
			local Column3Low, Column3High = bit32.bxor(Lane03Low, Lane08Low, Lane13Low, Lane18Low, Lane23Low), bit32.bxor(Lane03High, Lane08High, Lane13High, Lane18High, Lane23High)
			local Column4Low, Column4High = bit32.bxor(Lane04Low, Lane09Low, Lane14Low, Lane19Low, Lane24Low), bit32.bxor(Lane04High, Lane09High, Lane14High, Lane19High, Lane24High)
			local Column5Low, Column5High = bit32.bxor(Lane05Low, Lane10Low, Lane15Low, Lane20Low, Lane25Low), bit32.bxor(Lane05High, Lane10High, Lane15High, Lane20High, Lane25High)

			local DeltaLow, DeltaHigh = bit32.bxor(Column1Low, Column3Low * 2 + Column3High // 2147483648), bit32.bxor(Column1High, Column3High * 2 + Column3Low // 2147483648)
			local Temp0Low, Temp0High = bit32.bxor(DeltaLow, Lane02Low), bit32.bxor(DeltaHigh, Lane02High)
			local Temp1Low, Temp1High = bit32.bxor(DeltaLow, Lane07Low), bit32.bxor(DeltaHigh, Lane07High)
			local Temp2Low, Temp2High = bit32.bxor(DeltaLow, Lane12Low), bit32.bxor(DeltaHigh, Lane12High)
			local Temp3Low, Temp3High = bit32.bxor(DeltaLow, Lane17Low), bit32.bxor(DeltaHigh, Lane17High)
			local Temp4Low, Temp4High = bit32.bxor(DeltaLow, Lane22Low), bit32.bxor(DeltaHigh, Lane22High)

			Lane02Low = Temp1Low // 1048576 + (Temp1High * 4096); Lane02High = Temp1High // 1048576 + (Temp1Low * 4096)
			Lane07Low = Temp3Low // 524288 + (Temp3High * 8192); Lane07High = Temp3High // 524288 + (Temp3Low * 8192)
			Lane12Low = Temp0Low * 2 + Temp0High // 2147483648; Lane12High = Temp0High * 2 + Temp0Low // 2147483648
			Lane17Low = Temp2Low * 1024 + Temp2High // 4194304; Lane17High = Temp2High * 1024 + Temp2Low // 4194304
			Lane22Low = Temp4Low * 4 + Temp4High // 1073741824; Lane22High = Temp4High * 4 + Temp4Low // 1073741824

			DeltaLow = bit32.bxor(Column2Low, Column4Low * 2 + Column4High // 2147483648); DeltaHigh = bit32.bxor(Column2High, Column4High * 2 + Column4Low // 2147483648)
			Temp0Low = bit32.bxor(DeltaLow, Lane03Low); Temp0High = bit32.bxor(DeltaHigh, Lane03High)
			Temp1Low = bit32.bxor(DeltaLow, Lane08Low); Temp1High = bit32.bxor(DeltaHigh, Lane08High)
			Temp2Low = bit32.bxor(DeltaLow, Lane13Low); Temp2High = bit32.bxor(DeltaHigh, Lane13High)
			Temp3Low = bit32.bxor(DeltaLow, Lane18Low); Temp3High = bit32.bxor(DeltaHigh, Lane18High)
			Temp4Low = bit32.bxor(DeltaLow, Lane23Low); Temp4High = bit32.bxor(DeltaHigh, Lane23High)

			Lane03Low = Temp2Low // 2097152 + (Temp2High * 2048); Lane03High = Temp2High // 2097152 + (Temp2Low * 2048)
			Lane08Low = Temp4Low // 8 + bit32.bor(Temp4High * 536870912, 0); Lane08High = Temp4High // 8 + bit32.bor(Temp4Low * 536870912, 0)
			Lane13Low = Temp1Low * 64 + Temp1High // 67108864; Lane13High = Temp1High * 64 + Temp1Low // 67108864
			Lane18Low = (Temp3Low * 32768) + Temp3High // 131072; Lane18High = (Temp3High * 32768) + Temp3Low // 131072
			Lane23Low = Temp0Low // 4 + bit32.bor(Temp0High * 1073741824, 0); Lane23High = Temp0High // 4 + bit32.bor(Temp0Low * 1073741824, 0)

			DeltaLow = bit32.bxor(Column3Low, Column5Low * 2 + Column5High // 2147483648); DeltaHigh = bit32.bxor(Column3High, Column5High * 2 + Column5Low // 2147483648)
			Temp0Low = bit32.bxor(DeltaLow, Lane04Low); Temp0High = bit32.bxor(DeltaHigh, Lane04High)
			Temp1Low = bit32.bxor(DeltaLow, Lane09Low); Temp1High = bit32.bxor(DeltaHigh, Lane09High)
			Temp2Low = bit32.bxor(DeltaLow, Lane14Low); Temp2High = bit32.bxor(DeltaHigh, Lane14High)
			Temp3Low = bit32.bxor(DeltaLow, Lane19Low); Temp3High = bit32.bxor(DeltaHigh, Lane19High)
			Temp4Low = bit32.bxor(DeltaLow, Lane24Low); Temp4High = bit32.bxor(DeltaHigh, Lane24High)

			Lane04Low = bit32.bor(Temp3Low * 2097152, 0) + Temp3High // 2048; Lane04High = bit32.bor(Temp3High * 2097152, 0) + Temp3Low // 2048
			Lane09Low = bit32.bor(Temp0Low * 268435456, 0) + Temp0High // 16; Lane09High = bit32.bor(Temp0High * 268435456, 0) + Temp0Low // 16
			Lane14Low = bit32.bor(Temp2Low * 33554432, 0) + Temp2High // 128; Lane14High = bit32.bor(Temp2High * 33554432, 0) + Temp2Low // 128
			Lane19Low = Temp4Low // 256 + bit32.bor(Temp4High * 16777216, 0); Lane19High = Temp4High // 256 + bit32.bor(Temp4Low * 16777216, 0)
			Lane24Low = Temp1Low // 512 + bit32.bor(Temp1High * 8388608, 0); Lane24High = Temp1High // 512 + bit32.bor(Temp1Low * 8388608, 0)
			DeltaLow = bit32.bxor(Column4Low, Column1Low * 2 + Column1High // 2147483648); DeltaHigh = bit32.bxor(Column4High, Column1High * 2 + Column1Low // 2147483648)

			Temp0Low = bit32.bxor(DeltaLow, Lane05Low); Temp0High = bit32.bxor(DeltaHigh, Lane05High)
			Temp1Low = bit32.bxor(DeltaLow, Lane10Low); Temp1High = bit32.bxor(DeltaHigh, Lane10High)
			Temp2Low = bit32.bxor(DeltaLow, Lane15Low); Temp2High = bit32.bxor(DeltaHigh, Lane15High)
			Temp3Low = bit32.bxor(DeltaLow, Lane20Low); Temp3High = bit32.bxor(DeltaHigh, Lane20High)
			Temp4Low = bit32.bxor(DeltaLow, Lane25Low); Temp4High = bit32.bxor(DeltaHigh, Lane25High)

			Lane05Low = (Temp4Low * 16384) + Temp4High // 262144; Lane05High = (Temp4High * 16384) + Temp4Low // 262144
			Lane10Low = bit32.bor(Temp1Low * 1048576, 0) + Temp1High // 4096; Lane10High = bit32.bor(Temp1High * 1048576, 0) + Temp1Low // 4096
			Lane15Low = Temp3Low * 256 + Temp3High // 16777216; Lane15High = Temp3High * 256 + Temp3Low // 16777216
			Lane20Low = bit32.bor(Temp0Low * 134217728, 0) + Temp0High // 32; Lane20High = bit32.bor(Temp0High * 134217728, 0) + Temp0Low // 32
			Lane25Low = Temp2Low // 33554432 + Temp2High * 128; Lane25High = Temp2High // 33554432 + Temp2Low * 128

			DeltaLow = bit32.bxor(Column5Low, Column2Low * 2 + Column2High // 2147483648); DeltaHigh = bit32.bxor(Column5High, Column2High * 2 + Column2Low // 2147483648)
			Temp1Low = bit32.bxor(DeltaLow, Lane06Low); Temp1High = bit32.bxor(DeltaHigh, Lane06High)
			Temp2Low = bit32.bxor(DeltaLow, Lane11Low); Temp2High = bit32.bxor(DeltaHigh, Lane11High)
			Temp3Low = bit32.bxor(DeltaLow, Lane16Low); Temp3High = bit32.bxor(DeltaHigh, Lane16High)
			Temp4Low = bit32.bxor(DeltaLow, Lane21Low); Temp4High = bit32.bxor(DeltaHigh, Lane21High)
			Lane06Low = Temp2Low * 8 + Temp2High // 536870912; Lane06High = Temp2High * 8 + Temp2Low // 536870912
			Lane11Low = (Temp4Low * 262144) + Temp4High // 16384; Lane11High = (Temp4High * 262144) + Temp4Low // 16384
			Lane16Low = Temp1Low // 268435456 + Temp1High * 16; Lane16High = Temp1High // 268435456 + Temp1Low * 16
			Lane21Low = Temp3Low // 8388608 + Temp3High * 512; Lane21High = Temp3High // 8388608 + Temp3Low * 512
			Lane01Low = bit32.bxor(DeltaLow, Lane01Low); Lane01High = bit32.bxor(DeltaHigh, Lane01High)

			Lane01Low, Lane02Low, Lane03Low, Lane04Low, Lane05Low = bit32.bxor(Lane01Low, bit32.band(-1 - Lane02Low, Lane03Low)), bit32.bxor(Lane02Low, bit32.band(-1 - Lane03Low, Lane04Low)), bit32.bxor(Lane03Low, bit32.band(-1 - Lane04Low, Lane05Low)), bit32.bxor(Lane04Low, bit32.band(-1 - Lane05Low, Lane01Low)), bit32.bxor(Lane05Low, bit32.band(-1 - Lane01Low, Lane02Low)) :: number
			Lane01High, Lane02High, Lane03High, Lane04High, Lane05High = bit32.bxor(Lane01High, bit32.band(-1 - Lane02High, Lane03High)), bit32.bxor(Lane02High, bit32.band(-1 - Lane03High, Lane04High)), bit32.bxor(Lane03High, bit32.band(-1 - Lane04High, Lane05High)), bit32.bxor(Lane04High, bit32.band(-1 - Lane05High, Lane01High)), bit32.bxor(Lane05High, bit32.band(-1 - Lane01High, Lane02High)) :: number
			Lane06Low, Lane07Low, Lane08Low, Lane09Low, Lane10Low = bit32.bxor(Lane09Low, bit32.band(-1 - Lane10Low, Lane06Low)), bit32.bxor(Lane10Low, bit32.band(-1 - Lane06Low, Lane07Low)), bit32.bxor(Lane06Low, bit32.band(-1 - Lane07Low, Lane08Low)), bit32.bxor(Lane07Low, bit32.band(-1 - Lane08Low, Lane09Low)), bit32.bxor(Lane08Low, bit32.band(-1 - Lane09Low, Lane10Low)) :: number
			Lane06High, Lane07High, Lane08High, Lane09High, Lane10High = bit32.bxor(Lane09High, bit32.band(-1 - Lane10High, Lane06High)), bit32.bxor(Lane10High, bit32.band(-1 - Lane06High, Lane07High)), bit32.bxor(Lane06High, bit32.band(-1 - Lane07High, Lane08High)), bit32.bxor(Lane07High, bit32.band(-1 - Lane08High, Lane09High)), bit32.bxor(Lane08High, bit32.band(-1 - Lane09High, Lane10High)) :: number
			Lane11Low, Lane12Low, Lane13Low, Lane14Low, Lane15Low = bit32.bxor(Lane12Low, bit32.band(-1 - Lane13Low, Lane14Low)), bit32.bxor(Lane13Low, bit32.band(-1 - Lane14Low, Lane15Low)), bit32.bxor(Lane14Low, bit32.band(-1 - Lane15Low, Lane11Low)), bit32.bxor(Lane15Low, bit32.band(-1 - Lane11Low, Lane12Low)), bit32.bxor(Lane11Low, bit32.band(-1 - Lane12Low, Lane13Low)) :: number
			Lane11High, Lane12High, Lane13High, Lane14High, Lane15High = bit32.bxor(Lane12High, bit32.band(-1 - Lane13High, Lane14High)), bit32.bxor(Lane13High, bit32.band(-1 - Lane14High, Lane15High)), bit32.bxor(Lane14High, bit32.band(-1 - Lane15High, Lane11High)), bit32.bxor(Lane15High, bit32.band(-1 - Lane11High, Lane12High)), bit32.bxor(Lane11High, bit32.band(-1 - Lane12High, Lane13High)) :: number
			Lane16Low, Lane17Low, Lane18Low, Lane19Low, Lane20Low = bit32.bxor(Lane20Low, bit32.band(-1 - Lane16Low, Lane17Low)), bit32.bxor(Lane16Low, bit32.band(-1 - Lane17Low, Lane18Low)), bit32.bxor(Lane17Low, bit32.band(-1 - Lane18Low, Lane19Low)), bit32.bxor(Lane18Low, bit32.band(-1 - Lane19Low, Lane20Low)), bit32.bxor(Lane19Low, bit32.band(-1 - Lane20Low, Lane16Low)) :: number
			Lane16High, Lane17High, Lane18High, Lane19High, Lane20High = bit32.bxor(Lane20High, bit32.band(-1 - Lane16High, Lane17High)), bit32.bxor(Lane16High, bit32.band(-1 - Lane17High, Lane18High)), bit32.bxor(Lane17High, bit32.band(-1 - Lane18High, Lane19High)), bit32.bxor(Lane18High, bit32.band(-1 - Lane19High, Lane20High)), bit32.bxor(Lane19High, bit32.band(-1 - Lane20High, Lane16High)) :: number
			Lane21Low, Lane22Low, Lane23Low, Lane24Low, Lane25Low = bit32.bxor(Lane23Low, bit32.band(-1 - Lane24Low, Lane25Low)), bit32.bxor(Lane24Low, bit32.band(-1 - Lane25Low, Lane21Low)), bit32.bxor(Lane25Low, bit32.band(-1 - Lane21Low, Lane22Low)), bit32.bxor(Lane21Low, bit32.band(-1 - Lane22Low, Lane23Low)), bit32.bxor(Lane22Low, bit32.band(-1 - Lane23Low, Lane24Low)) :: number
			Lane21High, Lane22High, Lane23High, Lane24High, Lane25High = bit32.bxor(Lane23High, bit32.band(-1 - Lane24High, Lane25High)), bit32.bxor(Lane24High, bit32.band(-1 - Lane25High, Lane21High)), bit32.bxor(Lane25High, bit32.band(-1 - Lane21High, Lane22High)), bit32.bxor(Lane21High, bit32.band(-1 - Lane22High, Lane23High)), bit32.bxor(Lane22High, bit32.band(-1 - Lane23High, Lane24High)) :: number

			Lane01Low = bit32.bxor(Lane01Low, buffer.readu32(RCLow, RoundIndex))
			Lane01High = bit32.bxor(Lane01High, buffer.readu32(RCHigh, RoundIndex))
		end

		buffer.writeu32(LanesLow, 0, Lane01Low); buffer.writeu32(LanesHigh, 0, Lane01High)
		buffer.writeu32(LanesLow, 4, Lane02Low); buffer.writeu32(LanesHigh, 4, Lane02High)
		buffer.writeu32(LanesLow, 8, Lane03Low); buffer.writeu32(LanesHigh, 8, Lane03High)
		buffer.writeu32(LanesLow, 12, Lane04Low); buffer.writeu32(LanesHigh, 12, Lane04High)
		buffer.writeu32(LanesLow, 16, Lane05Low); buffer.writeu32(LanesHigh, 16, Lane05High)
		buffer.writeu32(LanesLow, 20, Lane06Low); buffer.writeu32(LanesHigh, 20, Lane06High)
		buffer.writeu32(LanesLow, 24, Lane07Low); buffer.writeu32(LanesHigh, 24, Lane07High)
		buffer.writeu32(LanesLow, 28, Lane08Low); buffer.writeu32(LanesHigh, 28, Lane08High)
		buffer.writeu32(LanesLow, 32, Lane09Low); buffer.writeu32(LanesHigh, 32, Lane09High)
		buffer.writeu32(LanesLow, 36, Lane10Low); buffer.writeu32(LanesHigh, 36, Lane10High)
		buffer.writeu32(LanesLow, 40, Lane11Low); buffer.writeu32(LanesHigh, 40, Lane11High)
		buffer.writeu32(LanesLow, 44, Lane12Low); buffer.writeu32(LanesHigh, 44, Lane12High)
		buffer.writeu32(LanesLow, 48, Lane13Low); buffer.writeu32(LanesHigh, 48, Lane13High)
		buffer.writeu32(LanesLow, 52, Lane14Low); buffer.writeu32(LanesHigh, 52, Lane14High)
		buffer.writeu32(LanesLow, 56, Lane15Low); buffer.writeu32(LanesHigh, 56, Lane15High)
		buffer.writeu32(LanesLow, 60, Lane16Low); buffer.writeu32(LanesHigh, 60, Lane16High)
		buffer.writeu32(LanesLow, 64, Lane17Low); buffer.writeu32(LanesHigh, 64, Lane17High)
		buffer.writeu32(LanesLow, 68, Lane18Low); buffer.writeu32(LanesHigh, 68, Lane18High)
		buffer.writeu32(LanesLow, 72, Lane19Low); buffer.writeu32(LanesHigh, 72, Lane19High)
		buffer.writeu32(LanesLow, 76, Lane20Low); buffer.writeu32(LanesHigh, 76, Lane20High)
		buffer.writeu32(LanesLow, 80, Lane21Low); buffer.writeu32(LanesHigh, 80, Lane21High)
		buffer.writeu32(LanesLow, 84, Lane22Low); buffer.writeu32(LanesHigh, 84, Lane22High)
		buffer.writeu32(LanesLow, 88, Lane23Low); buffer.writeu32(LanesHigh, 88, Lane23High)
		buffer.writeu32(LanesLow, 92, Lane24Low); buffer.writeu32(LanesHigh, 92, Lane24High)
		buffer.writeu32(LanesLow, 96, Lane25Low); buffer.writeu32(LanesHigh, 96, Lane25High)
	end
end

local function ProcessSponge(Message: buffer, CapacityBits: number, OutputBytes: number, DomainSeparator: number): buffer
	local RateBytes = (1600 - CapacityBits) // 8
	buffer.fill(LANES_LOW, 0, 0, 100)
	buffer.fill(LANES_HIGH, 0, 0, 100)

	local LanesLow = LANES_LOW
	local LanesHigh = LANES_HIGH

	local MessageLength: number = buffer.len(Message)
	local PaddedLength: number = MessageLength + 1

	local Remainder = PaddedLength % RateBytes
	if Remainder ~= 0 then
		PaddedLength += (RateBytes - Remainder)
	end

	local PaddedMessage = buffer.create(PaddedLength)

	if MessageLength > 0 then
		buffer.copy(PaddedMessage, 0, Message, 0, MessageLength)
	end

	if PaddedLength - MessageLength == 1 then
		buffer.writeu8(PaddedMessage, MessageLength, bit32.bor(DomainSeparator, 0x80))
	else
		buffer.writeu8(PaddedMessage, MessageLength, DomainSeparator)
		if PaddedLength - MessageLength > 2 then
			buffer.fill(PaddedMessage, MessageLength + 1, 0, PaddedLength - MessageLength - 2)
		end
		buffer.writeu8(PaddedMessage, PaddedLength - 1, 0x80)
	end

	Keccak(LanesLow, LanesHigh, PaddedMessage, 0, PaddedLength, RateBytes)

	local Output = buffer.create(OutputBytes)
	local OutputOffset = 0

	local ZeroBuffer = buffer.create(RateBytes)
	while OutputOffset < OutputBytes do
		local BytesThisRound = math.min(RateBytes, OutputBytes - OutputOffset)

		for ByteIndex = 0, BytesThisRound - 1 do
			local AbsoluteIndex = OutputOffset + ByteIndex
			if AbsoluteIndex < OutputBytes then
				local Lane = ByteIndex // 8
				local ByteInLane = ByteIndex % 8
				local LaneOffset = Lane * 4

				local Value
				if ByteInLane < 4 then
					Value = bit32.extract(buffer.readu32(LanesLow, LaneOffset), ByteInLane * 8, 8)
				else
					Value = bit32.extract(buffer.readu32(LanesHigh, LaneOffset), (ByteInLane - 4) * 8, 8)
				end
				buffer.writeu8(Output, AbsoluteIndex, Value)
			end
		end

		OutputOffset += BytesThisRound

		if OutputOffset < OutputBytes then
			Keccak(LanesLow, LanesHigh, ZeroBuffer, 0, RateBytes, RateBytes)
		end
	end

	return Output
end

function SHA3.SHAKE128(Message: buffer, OutputBytes: number): buffer
	return ProcessSponge(Message, 256, OutputBytes, 0x1F)
end

function SHA3.SHAKE256(Message: buffer, OutputBytes: number): buffer
	return ProcessSponge(Message, 512, OutputBytes, 0x1F)
end

return SHA3


--[=[
	ML-DSA Sampling

	Example usage:
		local Sampling = require(script)

		local Rho = buffer.create(32)
		local Matrix = buffer.create(4 * 4 * 256 * 4)
		Sampling.ExpandA(Rho, Matrix, 4, 4)
--]=]

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

local XOF = require("./XOF")
local Params = require("./Params")

local N = 256
local Q = 8380417

local Sampling = {}

local WORKSPACE_MSG_34 = buffer.create(34)
local WORKSPACE_MSG_66 = buffer.create(66)

function Sampling.ExpandA(Rho: buffer, Mat: buffer, K: number, L: number)
	buffer.copy(WORKSPACE_MSG_34, 0, Rho, 0, 32)

	local Message = WORKSPACE_MSG_34
	local Num = N
	local Modulus = Q
	local XOF = XOF

	for I = 0, K - 1 do
		for J = 0, L - 1 do
			local Offset = (I * L + J) * Num * 4

			buffer.writeu8(Message, 32, J)
			buffer.writeu8(Message, 33, I)

			XOF.Reset128()
			XOF.Absorb128(Message)

			local CoeffCount = 0
			local ChunkSize = 504

			while CoeffCount < Num do
				local TotalOutput = XOF.Squeeze128(ChunkSize)
				local ByteOffset = 0
				local OutputLen = buffer.len(TotalOutput)

				while CoeffCount < Num and ByteOffset + 5 < OutputLen do
					local Chunk = buffer.readu32(TotalOutput, ByteOffset)
					local B3 = buffer.readu8(TotalOutput, ByteOffset + 4)
					local B4 = buffer.readu8(TotalOutput, ByteOffset + 5)

					local T3 = bit32.band(Chunk, 0x7FFFFF)
					if T3 < Modulus then
						buffer.writeu32(Mat, Offset + CoeffCount * 4, T3)
						CoeffCount += 1
					end

					local T4 = bit32.bor(
						bit32.rshift(Chunk, 24),
						bit32.lshift(B3, 8),
						bit32.lshift(bit32.band(B4, 0x7F), 16)
					)
					if T4 < Modulus and CoeffCount < Num then
						buffer.writeu32(Mat, Offset + CoeffCount * 4, T4)
						CoeffCount += 1
					end

					ByteOffset += 6
				end

				while CoeffCount < Num and ByteOffset + 2 < OutputLen do
					local T0 = bit32.band(buffer.readu8(TotalOutput, ByteOffset + 2), 0x7F)
					local T1 = buffer.readu8(TotalOutput, ByteOffset + 1)
					local T2 = buffer.readu8(TotalOutput, ByteOffset)

					local T3 = bit32.bor(
						bit32.lshift(T0, 16),
						bit32.lshift(T1, 8),
						T2
					)

					if T3 < Modulus then
						buffer.writeu32(Mat, Offset + CoeffCount * 4, T3)
						CoeffCount += 1
					end

					ByteOffset += 3
				end
			end
		end
	end
end

function Sampling.ExpandS(RhoPrime: buffer, Vec: buffer, Eta: number, K: number, StartNonce: number)
	if not Params.CheckEta(Eta) or not Params.CheckNonce(StartNonce) then
		error("Invalid parameters for ExpandS")
	end

	buffer.copy(WORKSPACE_MSG_66, 0, RhoPrime, 0, 64)

	local Message = WORKSPACE_MSG_66
	local Num = N
	local Modulus = Q

	for I = 0, K - 1 do
		local Offset = I * Num * 4
		local NewNonce = StartNonce + I

		buffer.writeu8(Message, 64, bit32.band(NewNonce, 0xFF))
		buffer.writeu8(Message, 65, bit32.band(bit32.rshift(NewNonce, 8), 0xFF))

		XOF.Reset256()
		XOF.Absorb256(Message)

		local CoeffCount = 0
		local ChunkSize = if Eta == 2 then 136 else 272

		while CoeffCount < Num do
			local TotalOutput = XOF.Squeeze256(ChunkSize)
			local ByteOffset = 0
			local OutputLen = buffer.len(TotalOutput)

			if Eta == 2 then
				while CoeffCount < Num and ByteOffset + 3 < OutputLen do
					local Chunk = buffer.readu32(TotalOutput, ByteOffset)

					local T0 = bit32.band(Chunk, 0x0F)
					local T1 = bit32.band(bit32.rshift(Chunk, 4), 0x0F)
					local T2 = bit32.band(bit32.rshift(Chunk, 8), 0x0F)
					local T3 = bit32.band(bit32.rshift(Chunk, 12), 0x0F)
					local T4 = bit32.band(bit32.rshift(Chunk, 16), 0x0F)
					local T5 = bit32.band(bit32.rshift(Chunk, 20), 0x0F)
					local T6 = bit32.band(bit32.rshift(Chunk, 24), 0x0F)
					local T7 = bit32.band(bit32.rshift(Chunk, 28), 0x0F)

					if T0 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T0 % 5) + (if (T0 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T1 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T1 % 5) + (if (T1 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T2 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T2 % 5) + (if (T2 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T3 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T3 % 5) + (if (T3 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T4 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T4 % 5) + (if (T4 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T5 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T5 % 5) + (if (T5 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T6 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T6 % 5) + (if (T6 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if T7 < 15 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T7 % 5) + (if (T7 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end

					ByteOffset += 4
				end
			else
				while CoeffCount < Num and ByteOffset + 3 < OutputLen do
					local Chunk = buffer.readu32(TotalOutput, ByteOffset)

					local T0 = bit32.band(Chunk, 0x0F)
					local T1 = bit32.band(bit32.rshift(Chunk, 4), 0x0F)
					local T2 = bit32.band(bit32.rshift(Chunk, 8), 0x0F)
					local T3 = bit32.band(bit32.rshift(Chunk, 12), 0x0F)
					local T4 = bit32.band(bit32.rshift(Chunk, 16), 0x0F)
					local T5 = bit32.band(bit32.rshift(Chunk, 20), 0x0F)
					local T6 = bit32.band(bit32.rshift(Chunk, 24), 0x0F)
					local T7 = bit32.band(bit32.rshift(Chunk, 28), 0x0F)

					if T0 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T0 + (if T0 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T1 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T1 + (if T1 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T2 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T2 + (if T2 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T3 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T3 + (if T3 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T4 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T4 + (if T4 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T5 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T5 + (if T5 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T6 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T6 + (if T6 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if T7 < 9 and CoeffCount < Num then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T7 + (if T7 > 4 then Modulus else 0))
						CoeffCount += 1
					end

					ByteOffset += 4
				end
			end

			while CoeffCount < Num and ByteOffset < OutputLen do
				local Byte = buffer.readu8(TotalOutput, ByteOffset)
				local T0 = bit32.band(Byte, 0x0F)
				local T1 = bit32.band(bit32.rshift(Byte, 4), 0x0F)

				if Eta == 2 then
					if T0 < 15 then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T0 % 5) + (if (T0 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
					if CoeffCount < Num and T1 < 15 then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 2 - (T1 % 5) + (if (T1 % 5) > 2 then Modulus else 0))
						CoeffCount += 1
					end
				else
					if T0 < 9 then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T0 + (if T0 > 4 then Modulus else 0))
						CoeffCount += 1
					end
					if CoeffCount < Num and T1 < 9 then
						buffer.writeu32(Vec, Offset + CoeffCount * 4, 4 - T1 + (if T1 > 4 then Modulus else 0))
						CoeffCount += 1
					end
				end

				ByteOffset += 1
			end
		end
	end
end

function Sampling.ExpandMask(Seed: buffer, Nonce: number, Vec: buffer, Gamma1: number, L: number)
	if not Params.CheckGamma1(Gamma1) then
		error("Invalid Gamma1 parameter")
	end

	local Gamma1BitWidth = if Gamma1 == 131072 then 18 else 20
	local BufSize = math.floor((N * Gamma1BitWidth) / 8)

	buffer.copy(WORKSPACE_MSG_66, 0, Seed, 0, 64)

	local Message = WORKSPACE_MSG_66
	local Num = N
	local Modulus = Q
	local G1 = Gamma1

	for I = 0, L - 1 do
		local Offset = I * Num * 4
		local NewNonce = Nonce + I

		buffer.writeu8(Message, 64, bit32.band(NewNonce, 0xFF))
		buffer.writeu8(Message, 65, bit32.band(bit32.rshift(NewNonce, 8), 0xFF))

		XOF.Reset256()
		XOF.Absorb256(Message)

		local Src = XOF.Squeeze256(BufSize)

		if Gamma1BitWidth == 18 then
			for J = 0, 63 do
				local BOffset = J * 9
				local POffset = Offset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)
				local B5 = buffer.readu8(Src, BOffset + 5)
				local B6 = buffer.readu8(Src, BOffset + 6)
				local B7 = buffer.readu8(Src, BOffset + 7)
				local B8 = buffer.readu8(Src, BOffset + 8)

				local V0 = bit32.bor(B0, bit32.lshift(B1, 8), bit32.lshift(bit32.band(B2, 0x3), 16))
				local V1 = bit32.bor(bit32.rshift(B2, 2), bit32.lshift(B3, 6), bit32.lshift(bit32.band(B4, 0xF), 14))
				local V2 = bit32.bor(bit32.rshift(B4, 4), bit32.lshift(B5, 4), bit32.lshift(bit32.band(B6, 0x3F), 12))
				local V3 = bit32.bor(bit32.rshift(B6, 6), bit32.lshift(B7, 2), bit32.lshift(B8, 10))

				local D0 = G1 - V0
				local D1 = G1 - V1
				local D2 = G1 - V2
				local D3 = G1 - V3

				buffer.writeu32(Vec, POffset + 0, if D0 < 0 then D0 + Modulus else D0)
				buffer.writeu32(Vec, POffset + 4, if D1 < 0 then D1 + Modulus else D1)
				buffer.writeu32(Vec, POffset + 8, if D2 < 0 then D2 + Modulus else D2)
				buffer.writeu32(Vec, POffset + 12, if D3 < 0 then D3 + Modulus else D3)
			end
		else
			for J = 0, 63 do
				local BOffset = J * 10
				local POffset = Offset + J * 16

				local B0 = buffer.readu8(Src, BOffset + 0)
				local B1 = buffer.readu8(Src, BOffset + 1)
				local B2 = buffer.readu8(Src, BOffset + 2)
				local B3 = buffer.readu8(Src, BOffset + 3)
				local B4 = buffer.readu8(Src, BOffset + 4)
				local B5 = buffer.readu8(Src, BOffset + 5)
				local B6 = buffer.readu8(Src, BOffset + 6)
				local B7 = buffer.readu8(Src, BOffset + 7)
				local B8 = buffer.readu8(Src, BOffset + 8)
				local B9 = buffer.readu8(Src, BOffset + 9)

				local V0 = bit32.bor(B0, bit32.lshift(B1, 8), bit32.lshift(bit32.band(B2, 0xF), 16))
				local V1 = bit32.bor(bit32.rshift(B2, 4), bit32.lshift(B3, 4), bit32.lshift(B4, 12))
				local V2 = bit32.bor(B5, bit32.lshift(B6, 8), bit32.lshift(bit32.band(B7, 0xF), 16))
				local V3 = bit32.bor(bit32.rshift(B7, 4), bit32.lshift(B8, 4), bit32.lshift(B9, 12))

				local D0 = G1 - V0
				local D1 = G1 - V1
				local D2 = G1 - V2
				local D3 = G1 - V3

				buffer.writeu32(Vec, POffset + 0, if D0 < 0 then D0 + Modulus else D0)
				buffer.writeu32(Vec, POffset + 4, if D1 < 0 then D1 + Modulus else D1)
				buffer.writeu32(Vec, POffset + 8, if D2 < 0 then D2 + Modulus else D2)
				buffer.writeu32(Vec, POffset + 12, if D3 < 0 then D3 + Modulus else D3)
			end
		end
	end
end

function Sampling.SampleInBall(Seed: buffer, Poly: buffer, Tau: number, Lambda: number)
	if not Params.CheckTau(Tau) then
		error("Invalid Tau parameter")
	end

	buffer.fill(Poly, 0, 0, N * 4)

	XOF.Reset256()
	XOF.Absorb256(Seed)

	local TauBits = XOF.Squeeze256(8)
	local TauBitsLow = buffer.readu32(TauBits, 0)
	local TauBitsHigh = buffer.readu32(TauBits, 4)

	local From = N - Tau
	local I = From
	local ChunkSize = 136
	local Modulus = Q

	while I < N do
		local RandomBytes = XOF.Squeeze256(ChunkSize)
		local ByteOffset = 0
		local OutputLen = buffer.len(RandomBytes)

		while I < N and ByteOffset + 3 < OutputLen do
			local Chunk = buffer.readu32(RandomBytes, ByteOffset)

			for B = 0, 3 do
				if I >= N then break end

				local J = bit32.band(bit32.rshift(Chunk, B * 8), 0xFF)

				if J <= I then
					local TauBit = I - From
					local S
					if TauBit < 32 then
						S = bit32.band(bit32.rshift(TauBitsLow, TauBit), 1)
					else
						S = bit32.band(bit32.rshift(TauBitsHigh, TauBit - 32), 1)
					end

					buffer.writeu32(Poly, I * 4, buffer.readu32(Poly, J * 4))
					buffer.writeu32(Poly, J * 4, if S == 0 then 1 else Modulus - 1)

					I += 1
				end
			end

			ByteOffset += 4
		end

		while I < N and ByteOffset < OutputLen do
			local J = buffer.readu8(RandomBytes, ByteOffset)

			if J <= I then
				local TauBit = I - From
				local S
				if TauBit < 32 then
					S = bit32.band(bit32.rshift(TauBitsLow, TauBit), 1)
				else
					S = bit32.band(bit32.rshift(TauBitsHigh, TauBit - 32), 1)
				end

				buffer.writeu32(Poly, I * 4, buffer.readu32(Poly, J * 4))
				buffer.writeu32(Poly, J * 4, if S == 0 then 1 else Modulus - 1)

				I += 1
			end

			ByteOffset += 1
		end
	end
end

return Sampling


--[=[
	Utility functions for ML-DSA key and signature sizes

	 ML-DSA parameters.

	Example usage:
		local Utils = require(script)

		local PubKeySize = Utils.PubKeyLen(4, 13) -- 1312 bytes for ML-DSA-44
		local SecKeySize = Utils.SecKeyLen(4, 4, 2, 13) -- 2560 bytes
		local SigSize = Utils.SigLen(4, 4, 131072, 80, 128) -- Variable
--]=]

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

local Q_BIT_WIDTH = 23

local Utils = {}

local function BitWidth(Value: number): number
	if Value == 0 then
		return 0
	end

	local Width = 0
	local TempValue = Value

	while TempValue > 0 do
		Width += 1
		TempValue = bit32.rshift(TempValue, 1)
	end

	return Width
end

function Utils.PubKeyLen(K: number, D: number): number
	local T1Bw = Q_BIT_WIDTH - D
	local PkLen = 32 + K * 32 * T1Bw

	return PkLen
end

function Utils.SecKeyLen(K: number, L: number, Eta: number, D: number): number
	local EtaBw = BitWidth(2 * Eta)
	local SkLen = 32 + 32 + 64 + 32 * (EtaBw * (K + L) + K * D)

	return SkLen
end

function Utils.SigLen(K: number, L: number, Gamma1: number, Omega: number, Lambda: number): number
	local Gamma1Bw = BitWidth(Gamma1)
	local SigLen = math.floor((2 * Lambda) / 8) + (32 * L * Gamma1Bw) + (Omega + K)

	return SigLen
end

function Utils.BitWidth(Value: number): number
	return BitWidth(Value)
end

return Utils