-- CONSTANTS
local PI2 = math.pi*2
local PHI = (1 + math.sqrt(5)) / 2
local RIGHT 	= Vector3.new(1, 0, 0)
local UP 		= Vector3.new(0, 1, 0)
local BACK 		= Vector3.new(0, 0, 1)
local LEFT 		= Vector3.new(-1, 0, 0)
local DOWN 		= Vector3.new(0, -1, 0)
local FORWARD 	= Vector3.new(0, 0, -1)
local CORNERS = {
	Vector3.new(1, 1, 1);
	Vector3.new(-1, 1, 1);
	Vector3.new(-1, 1, -1);
	Vector3.new(1, 1, -1);
	Vector3.new(1, -1, 1);
	Vector3.new(-1, -1, 1);
	Vector3.new(-1, -1, -1);
	Vector3.new(1, -1, -1);
}
-- VERTICE INDEX ARRAYS
local BLOCK = {1, 2, 3, 4, 5, 6, 7, 8}
local WEDGE = {1, 2, 5, 6, 7, 8}
local CORNERWEDGE = {4, 5, 6, 7, 8}
-- VERTICE FUNCTIONS
local function fromIndexArray(array)
	local output = {}
	for i = 1, #array do
		output[i] = CORNERS[array[i]]
	end
	return output
end
local function cylinder(n)
	local output = {}
	local arc = PI2 / n
	for i = 1, n do
		local vi = CFrame.fromAxisAngle(RIGHT, i*arc) * UP
		output[i] = RIGHT + vi
		output[n + i] = LEFT + vi
	end
	return output
end
local function icoSphere(n)
	local verts = {
		Vector3.new(-1,  PHI, 0),
		Vector3.new(1,  PHI, 0),
		Vector3.new(-1, -PHI, 0),
		Vector3.new(1, -PHI, 0),

		Vector3.new(0, -1,  PHI),
		Vector3.new(0,  1,  PHI),
		Vector3.new(0, -1, -PHI),
		Vector3.new(0,  1, -PHI),

		Vector3.new(PHI, 0, -1),
		Vector3.new(PHI, 0,  1),
		Vector3.new(-PHI, 0, -1),
		Vector3.new(-PHI, 0,  1)
	}

	local indices = {
		1, 12, 6,
		1, 6, 2,
		1, 2, 8,
		1, 8, 11,
		1, 11, 12,

		2, 6, 10,
		6, 12, 5,
		12, 11, 3,
		11, 8, 7,
		8, 2, 9,

		4, 10, 5,
		4, 5, 3,
		4, 3, 7,
		4, 7, 9,
		4, 9, 10,

		5, 10, 6,
		3, 5, 12,
		7, 3, 11,
		9, 7, 8,
		10, 9, 2
	}

	local splits = {}

	local function split(i, j)
		local key = i < j and (i .. "," .. j) or (j .. "," .. i)

		if (not splits[key]) then
			verts[#verts+1] = (verts[i] + verts[j]) / 2
			splits[key] = #verts
		end

		return splits[key]
	end

	for _ = 1, n do
		for  i = #indices, 1, -3 do
			local v1, v2, v3 = indices[i - 2], indices[i - 1], indices[i]
			local a = split(v1, v2)
			local b = split(v2, v3)
			local c = split(v3, v1)

			indices[#indices+1] = v1
			indices[#indices+1] = a
			indices[#indices+1] = c

			indices[#indices+1] = v2
			indices[#indices+1] = b
			indices[#indices+1] = a

			indices[#indices+1] = v3
			indices[#indices+1] = c
			indices[#indices+1] = b

			indices[#indices+1] = a
			indices[#indices+1] = b
			indices[#indices+1] = c

			table.remove(indices, i)
			table.remove(indices, i - 1)
			table.remove(indices, i - 2)
		end
	end

	-- normalize
	for i = 1, #verts do
		verts[i] = verts[i].Unit
	end

	return verts
end
-- Useful functions
local function vertShape(cf, size2, array)
	local output = {}
	for i = 1, #array do
		output[i] = cf:PointToWorldSpace(array[i] * size2)
	end
	return output
end
local function getCentroidFromSet(set)
	local sum = set[1]
	for i = 2, #set do
		sum = sum + set[2]
	end
	return sum / #set
end
local function classify(part)
	if (part.ClassName == "Part") then
		if (part.Shape == Enum.PartType.Block) then
			return "Block"
		elseif (part.Shape == Enum.PartType.Cylinder) then
			return "Cylinder"
		elseif (part.Shape == Enum.PartType.Ball) then
			return "Ball"
		end;
	elseif (part.ClassName == "WedgePart") then
		return "Wedge"
	elseif (part.ClassName == "CornerWedgePart") then
		return "CornerWedge"
	elseif (part:IsA("BasePart")) then -- mesh, CSG, truss, etc... just use block
		return "Block"
	end
end
--
local BLOCK_ARRAY = fromIndexArray(BLOCK)
local WEDGE_ARRAY = fromIndexArray(WEDGE)
local CORNERWEDGE_ARRAY = fromIndexArray(CORNERWEDGE)
local CYLINDER_ARRAY = cylinder(20)
local SPHERE_ARRAY = icoSphere(2)
return {
	Block = function(cf, size2) return vertShape(cf, size2, BLOCK_ARRAY) end;
	Wedge = function(cf, size2) return vertShape(cf, size2, WEDGE_ARRAY) end;
	CornerWedge = function(cf, size2) return vertShape(cf, size2, CORNERWEDGE_ARRAY) end;
	Cylinder = function(cf, size2) return vertShape(cf, size2, CYLINDER_ARRAY) end;
	Ball = function(cf, size2) return vertShape(cf, size2, SPHERE_ARRAY) end;

	GetCentroid = getCentroidFromSet;
	Classify = classify;
}


-- TAKEN FROM https://www.roblox.com/library/3686163417/Rotated-Region3-GJK
-- PACKAGE UPDATED SINCE MAY 02, 2020 12:15:45 PM

--[[
This is a Rotated Region3 Class that behaves much the same as the standard Region3 class expect that it allows
for both rotated regions and also a varying array of shapes.
API:
Constructors:
	RotatedRegion3.new(CFrame cframe, Vector3 size)
		> Creates a region from a cframe which acts as the center of the region and size which extends to
		> the corners like a block part.
	RotatedRegion3.Block(CFrame cframe, Vector3 size)
		> This is the exact same as the region.new constructor, but has a different name.
	RotatedRegion3.Wedge(CFrame cframe, Vector3 size)
		> Creates a region from a cframe which acts as the center of the region and size which extends to
		> the corners like a wedge part.
	RotatedRegion3.CornerWedge(CFrame cframe, Vector3 size)
		> Creates a region from a cframe which acts as the center of the region and size which extends to
		> the corners like a cornerWedge part.
	RotatedRegion3.Cylinder(CFrame cframe, Vector3 size)
		> Creates a region from a cframe which acts as the center of the region and size which extends to
		> the corners like a cylinder part.
	RotatedRegion3.Ball(CFrame cframe, Vector3 size)
		> Creates a region from a cframe which acts as the center of the region and size which extends to
		> the corners like a ball part.
	RotatedRegion3.FromPart(part)
		> Creates a region from a part in the game. It can be used on any base part, but the region
		> will treat unknown shapes (meshes, unions, etc) as block shapes.
Methods:
	RotatedRegion3:CastPoint(Vector3 point)
		> returns true or false if the point is within the RotatedRegion3 object
	RotatedRegion3:CastPart(BasePart part)
		> returns true or false if the part is withing the RotatedRegion3 object
	RotatedRegion3:FindPartsInRegion3(Instance ignore, Integer maxParts)
		> returns array of parts in the RotatedRegion3 object
		> will return a maximum number of parts in array [maxParts] the default is 20
		> parts that either are descendants of or actually are the [ignore] instance will be ignored
	RotatedRegion3:FindPartsInRegion3WithIgnoreList(Instance Array ignore, Integer maxParts)
		> returns array of parts in the RotatedRegion3 object
		> will return a maximum number of parts in array [maxParts] the default is 20
		> parts that either are descendants of the [ignore array] or actually are the [ignore array] instances will be ignored
	RotatedRegion3:FindPartsInRegion3WithWhiteList(Instance Array whiteList, Integer maxParts)
		> returns array of parts in the RotatedRegion3 object
		> will return a maximum number of parts in array [maxParts] the default is 20
		> parts that either are descendants of the [whiteList array] or actually are the [whiteList array] instances are all that will be checked
	RotatedRegion3:Cast(Instance or Instance Array ignore, Integer maxParts)
		> Same as the `:FindPartsInRegion3WithIgnoreList` method, but will check if the ignore argument is an array or single instance
Properties:
	RotatedRegion3.CFrame
		> cframe that represents the center of the region
	RotatedRegion3.Size
		> vector3 that represents the size of the region
	RotatedRegion3.Shape
		> string that represents the shape type of the RotatedRegion3 object
	RotatedRegion3.Set
		> array of vector3 that are passed to the support function
	RotatedRegion3.Support
		> function that is used for support in the GJK algorithm
	RotatedRegion3.Centroid
		> vector3 that represents the center of the set, again used for the GJK algorithm
	RotatedRegion3.AlignedRegion3
		> standard region3 that represents the world bounding box of the RotatedRegion3 object
Note: I haven't actually done anything to enforce this, but you should treat all these properties as read only
Enjoy!
- EgoMoose
--]]
--
local GJK = require(script:WaitForChild("GJK"))
local Supports = require(script:WaitForChild("Supports"))
local Vertices = require(script:WaitForChild("Vertices"))
-- Class
local RotatedRegion3 = {}
RotatedRegion3.__index = RotatedRegion3
-- Private functions
local function getCorners(cf, s2)
	return {
		cf:PointToWorldSpace(Vector3.new(-s2.x, s2.y, s2.z));
		cf:PointToWorldSpace(Vector3.new(-s2.x, -s2.y, s2.z));
		cf:PointToWorldSpace(Vector3.new(-s2.x, -s2.y, -s2.z));
		cf:PointToWorldSpace(Vector3.new(s2.x, -s2.y, -s2.z));
		cf:PointToWorldSpace(Vector3.new(s2.x, s2.y, -s2.z));
		cf:PointToWorldSpace(Vector3.new(s2.x, s2.y, s2.z));
		cf:PointToWorldSpace(Vector3.new(s2.x, -s2.y, s2.z));
		cf:PointToWorldSpace(Vector3.new(-s2.x, s2.y, -s2.z));
	}
end
local function worldBoundingBox(set)
	local x, y, z = {}, {}, {}
	for i = 1, #set do x[i], y[i], z[i] = set[i].x, set[i].y, set[i].z end
	local min = Vector3.new(math.min(unpack(x)), math.min(unpack(y)), math.min(unpack(z)))
	local max = Vector3.new(math.max(unpack(x)), math.max(unpack(y)), math.max(unpack(z)))
	return min, max
end
-- Public Constructors
function RotatedRegion3.new(cframe, size)
	local self = setmetatable({}, RotatedRegion3)

	self.CFrame = cframe
	self.Size = size
	self.Shape = "Block"

	self.Set = Vertices.Block(cframe, size/2)
	self.Support = Supports.PointCloud
	self.Centroid = cframe.p

	self.AlignedRegion3 = Region3.new(worldBoundingBox(self.Set))
	return self
end
RotatedRegion3.Block = RotatedRegion3.new
function RotatedRegion3.Wedge(cframe, size)
	local self = setmetatable({}, RotatedRegion3)
	self.CFrame = cframe
	self.Size = size
	self.Shape = "Wedge"

	self.Set = Vertices.Wedge(cframe, size/2)
	self.Support = Supports.PointCloud
	self.Centroid = Vertices.GetCentroid(self.Set)

	self.AlignedRegion3 = Region3.new(worldBoundingBox(self.Set))
	return self
end
function RotatedRegion3.CornerWedge(cframe, size)
	local self = setmetatable({}, RotatedRegion3)
	self.CFrame = cframe
	self.Size = size
	self.Shape = "CornerWedge"

	self.Set = Vertices.CornerWedge(cframe, size/2)
	self.Support = Supports.PointCloud
	self.Centroid = Vertices.GetCentroid(self.Set)

	self.AlignedRegion3 = Region3.new(worldBoundingBox(self.Set))
	return self
end
function RotatedRegion3.Cylinder(cframe, size)
	local self = setmetatable({}, RotatedRegion3)
	self.CFrame = cframe
	self.Size = size
	self.Shape = "Cylinder"

	self.Set = {cframe, size/2}
	self.Support = Supports.Cylinder
	self.Centroid = cframe.p

	self.AlignedRegion3 = Region3.new(worldBoundingBox(getCorners(unpack(self.Set))))
	return self
end
function RotatedRegion3.Ball(cframe, size)
	local self = setmetatable({}, RotatedRegion3)
	self.CFrame = cframe
	self.Size = size
	self.Shape = "Ball"

	self.Set = {cframe, size/2}
	self.Support = Supports.Ellipsoid
	self.Centroid = cframe.p

	self.AlignedRegion3 = Region3.new(worldBoundingBox(getCorners(unpack(self.Set))))
	return self
end
function RotatedRegion3.FromPart(part)
	return RotatedRegion3[Vertices.Classify(part)](part.CFrame, part.Size)
end
-- Public Constructors
function RotatedRegion3:CastPoint(point)
	local gjk = GJK.new(self.Set, {point}, self.Centroid, point, self.Support, Supports.PointCloud)
	return gjk:IsColliding()
end
function RotatedRegion3:CastPart(part)
	local r3 = RotatedRegion3.FromPart(part)
	local gjk = GJK.new(self.Set, r3.Set, self.Centroid, r3.Centroid, self.Support, r3.Support)
	return gjk:IsColliding()
end
function RotatedRegion3:FindPartsInRegion3(ignore, maxParts)
	local found = {}
	local parts = game.Workspace:FindPartsInRegion3(self.AlignedRegion3, ignore, maxParts)
	for i = 1, #parts do
		if (self:CastPart(parts[i])) then
			table.insert(found, parts[i])
		end
	end
	return found
end
function RotatedRegion3:FindPartsInRegion3WithIgnoreList(ignore, maxParts)
	ignore = ignore or {}
	local found = {}
	local parts = game.Workspace:FindPartsInRegion3WithIgnoreList(self.AlignedRegion3, ignore, maxParts)
	for i = 1, #parts do
		if (self:CastPart(parts[i])) then
			table.insert(found, parts[i])
		end
	end
	return found
end
function RotatedRegion3:FindPartsInRegion3WithWhiteList(whiteList, maxParts)
	whiteList = whiteList or {}
	local found = {}
	local parts = game.Workspace:FindPartsInRegion3WithWhiteList(self.AlignedRegion3, whiteList, maxParts)
	for i = 1, #parts do
		if (self:CastPart(parts[i])) then
			table.insert(found, parts[i])
		end
	end
	return found
end
function RotatedRegion3:Cast(ignore, maxParts)
	ignore = type(ignore) == "table" and ignore or {ignore}
	return self:FindPartsInRegion3WithIgnoreList(ignore, maxParts)
end
--
return RotatedRegion3


local function commify(self, number)
	assert(type(number) == 'number', 'Attempt to commify a non-number value')

	local formatted = tostring(number)
	while true do
		local newFormatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
		formatted = newFormatted
		if k == 0 then
			break
		end
	end
	return formatted
end

return {
	commify = commify,
}


local DEFAULT_SUFFIX_TABLE = {
	"k",
	"M",
	"B",
	"T",
	"Qd",
	"Qn",
	"Sx",
	"Sp",
	"O",
	"N",
	"De",
	"Ud",
	"DD",
	"tdD",
	"QnD",
	"SxD",
	"SpD",
	"OcD",
	"NvD",
	"VgN",
	"UvG",
	"DvG",
	"TvG",
	"QtV",
	"QnV",
	"SeV",
	"SpG",
	"OvG",
	"NvG",
	"TgN",
	"UtG",
	"DtG",
	"TsTg",
	"QtTg",
	"QnTg",
	"SsTg",
	"SpTg",
	"OcTg",
	"NoTg",
	"QdDr",
	"UnAg",
	"DuAg",
	"TeAg",
	"QdAg",
	"QnAG",
	"SxAg",
	"SpAg",
	"OcAg",
	"NvAg",
	"CT",
}

local setSetting = require(script.setSetting).setSetting
local numberToString = require(script.numberToString).numberToString
local numbersToSortedString = require(script.numbersToSortedString).numbersToSortedString
local stringToNumber = require(script.stringToNumber).stringToNumber
local commify = require(script.commify).commify

return {
	new = function()
		return {
			_suffixTable = DEFAULT_SUFFIX_TABLE,
			_decimalPlaces = 2,
			_stripTrailingZeroes = false,

			setSetting = setSetting,
			numberToString = numberToString,
			numbersToSortedString = numbersToSortedString,
			stringToNumber = stringToNumber,
			commify = commify,
		}
	end,
}


local function round(number, decimalPlaces, roundDown)
	number = number * 10 ^ decimalPlaces
	if roundDown then
		return math.floor(number) / 10 ^ decimalPlaces
	else
		number = tonumber(string.format("%.14g", number))
		-- cast to string and back to number to give some epsilon for floating point numbers
		--[[ e.g.:
			local number = 1005 / 10^3 * 10^2 + 0.5 -- 101
			print(number, math.floor(number))
			> 101 100

			local number = 1005 / 10^3 * 10^2 + 0.5 -- 101
			number = tonumber(tostring(number))
			print(number, math.floor(number))
			> 101 101
		]]
		return math.floor(number + 0.5) / 10 ^ decimalPlaces
	end
end

local function numberToString(self, number, roundDown)
	if type(number) ~= "number" then
		error(`numberToString invalid parameter #1, expected number, got "${type(number)}"`, 2)
	end

	if roundDown == nil then
		roundDown = true
	end

	if number < 1000 and number > -1000 then
		-- special case: we must manually abbreviate numbers between -1000 and 1000
		local rounded = round(number, self._decimalPlaces, roundDown)
		if math.abs(rounded) < 1000 then
			if not self._stripTrailingZeroes then
				return ("%." .. self._decimalPlaces .. "f"):format(rounded)
			else
				return tostring(rounded)
			end
		else
			number = rounded
		end
	end

	local negative = number < 0
	number = math.abs(math.floor(number))

	for index = #self._suffixTable, 1, -1 do
		local unit = self._suffixTable[index]
		local size = 10 ^ (index * 3)

		if size <= number then
			number = round(number / size, self._decimalPlaces, roundDown)

			if number == 1000 and index < #self._suffixTable then
				number = 1
				unit = self._suffixTable[index + 1]
			end

			if not self._stripTrailingZeroes then
				number = ("%." .. self._decimalPlaces .. "f"):format(number)
			end

			number = number .. unit
			break
		end
	end

	if negative then
		return "-" .. number
	else
		return tostring(number)
	end
end

return {
	numberToString = numberToString,
}


-- Credits to Coreccii for this method
local hexToBin = {
	["0"] = "0000",
	["1"] = "0001",
	["2"] = "0010",
	["3"] = "0011",
	["4"] = "0100",
	["5"] = "0101",
	["6"] = "0110",
	["7"] = "0111",
	["8"] = "1000",
	["9"] = "1001",
	["A"] = "1010",
	["B"] = "1011",
	["C"] = "1100",
	["D"] = "1101",
	["E"] = "1110",
	["F"] = "1111",
}

local binToUnicodeAscending = table.create(16)
local binToUnicodeDescending = table.create(16)

for hex, bin in pairs(hexToBin) do
	binToUnicodeAscending[hex] = bin.gsub(bin, ".", {["0"] = "\u{200B}", ["1"] = "\u{2060}"})
	binToUnicodeDescending[hex] = bin.gsub(bin, ".", {["1"] = "\u{200B}", ["0"] = "\u{2060}"})
end

-- Usage: getSortablePrefix(sortOrder: number, maximumSortOrder: number, descending?: false): string
local function getSortablePrefix(num, maxNum, descending)
	local maxSize = math.ceil(math.log(maxNum) / math.log(16))
	local maxSizeString = string.format("%d", maxSize)

	local numHex = string.format("%0" .. maxSizeString .. "X", num)
	return (string.gsub(numHex, ".", descending and binToUnicodeDescending or binToUnicodeAscending))
end

local function round(number, decimalPlaces, roundDown)
	number = number * 10^decimalPlaces
	if roundDown then
		return math.floor(number) / 10^decimalPlaces
	else
		number = tonumber(tostring(number))
		-- cast to string and back to number to prevent floating point errors
		return math.floor(number + 0.5) / 10^decimalPlaces
	end
end

local function numbersToSortedString(self, numbers)
	if not (numbers and type(numbers) == 'table') then
		error('numbersToSortedString had invalid parameters.\nP1 - numbers: Array<number>', 2)
	end

	local numbersSize = #numbers

	local sortedNumbers = table.create(numbersSize)

	-- Validate and create the numbers
	for index, number in ipairs(numbers) do
		if type(number) ~= 'number' then
			error('numbersToSortedString had invalid parameters.\nP1 - numbers: Array<number>', 2)
		end

		table.insert(sortedNumbers, {
			initialIndex = index,
			value = number
		})
	end

	-- Sort
	table.sort(sortedNumbers, function(a, b)
		return a.value < b.value
	end)

	-- Make return result
	local returnResult = table.create(numbersSize)
	for sortedIndex, numberData in ipairs(sortedNumbers) do
		if numberData.value < 1000 then
			if not self._stripTrailingZeroes then
				returnResult[numberData.initialIndex] = string.format('%.'..self._decimalPlaces..'f', numberData.value)
			else
				returnResult[numberData.initialIndex] = tostring(round(numberData.value, self._decimalPlaces))
			end
		end

		for index = #self._suffixTable, 1, -1 do
			local shortenedNumber = 10 ^ (index * 3)

			if numberData.value >= shortenedNumber then
				local suffix = self._suffixTable[index]

				local prefixed

				if not self._stripTrailingZeroes then
					prefixed = string.format('%.'..self._decimalPlaces..'f', numberData.value / shortenedNumber)
				else
					prefixed = tostring(round(numberData.value / shortenedNumber, self._decimalPlaces))
				end

				returnResult[numberData.initialIndex] = getSortablePrefix(sortedIndex, numbersSize, false)..prefixed..suffix
				break
			end
		end

	end

	return returnResult
end

return {
	numbersToSortedString = numbersToSortedString,
}


-- Disable ANSI colors from Jest
_G.NOCOLOR = true

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")

local runCLI = require(ReplicatedStorage.DevPackages.Jest).runCLI

local processServiceExists, ProcessService = pcall(function()
	return game:GetService("ProcessService")
end)

local story = {
	summary = "Run Jest tests",
	controls = nil,
	render = function()
		_G.NOCOLOR = false

		task.spawn(function()
			local status, result = runCLI(ServerScriptService.abbreviate, {
				verbose = false,
				ci = false,
			}, { ServerScriptService.abbreviate }):awaitStatus()

			if status == "Rejected" then
				print(result)
			end

			if
				status == "Resolved"
				and result.results.numFailedTestSuites == 0
				and result.results.numFailedTests == 0
			then
				if processServiceExists then
					ProcessService:ExitAsync(0)
				end
			end

			if processServiceExists then
				ProcessService:ExitAsync(1)
			end
		end)

		return function() end
	end,
}

return story


local function setSetting(self, settingName, settingValue)
	if not (settingName and settingValue ~= nil and type(settingName) == 'string') then
		error('setSetting had invalid parameters.\nP1 - settingName: string\nP2 - settingValue: unknown', 2)
	end
	local realSetting = '_' .. settingName

	if self[realSetting] ~= nil and type(self[realSetting]) ~= 'function' then
		self[realSetting] = settingValue
	else
		if self[realSetting] ~= nil then
			error('Attempt to index setting ' ..settingName..' which is not an editable field.');
		else
			error('Attempt to index setting ' ..settingName..' which is not a valid setting!');
		end
	end
end

return {
	setSetting = setSetting,
}


local function stringToNumber(self, str)
	if not (str and type(str) == 'string') then
		error('stringToNumber had invalid parameters.\nP1 - string: string', 2)
	end

	local totalMagnitude = 1

	for key, suffix in pairs(self._suffixTable) do
		str = string.gsub(str, suffix, function()
			totalMagnitude = totalMagnitude * (10 ^ (key * 3))
			return ''
		end)
	end

	-- validate that user passed an actual string that we can convert to number
	if not tonumber(str) then
		error('stringToNumber invalid parameter #1: Expected a string which could be converted to a number, got "'..str..'"', 2)
	end

	return totalMagnitude * str
end

return {
	stringToNumber = stringToNumber,
}


local RunService = game:GetService("RunService")

local Signal = require(script.Parent.Signal)

local noop = function() end

local BaseMotor = {}
BaseMotor.__index = BaseMotor

function BaseMotor.new()
	return setmetatable({
		_onStep = Signal.new(),
		_onComplete = Signal.new()
	}, BaseMotor)
end

function BaseMotor:onStep(handler)
	return self._onStep:connect(handler)
end

function BaseMotor:onComplete(handler)
	return self._onComplete:connect(handler)
end

function BaseMotor:start()
	if not self._connection then
		self._connection = RunService.RenderStepped:Connect(function(deltaTime)
			self:step(deltaTime)
		end)
	end
end

function BaseMotor:stop()
	if self._connection then
		self._connection:Disconnect()
		self._connection = nil
	end
end

BaseMotor.destroy = BaseMotor.stop

BaseMotor.step = noop
BaseMotor.getValue = noop
BaseMotor.setGoal = noop

function BaseMotor:__tostring()
	return "Motor"
end

return BaseMotor


local BaseMotor = require(script.Parent.BaseMotor)
local SingleMotor = require(script.Parent.SingleMotor)

local isMotor = require(script.Parent.isMotor)

local GroupMotor = setmetatable({}, BaseMotor)
GroupMotor.__index = GroupMotor

local function toMotor(value)
	if isMotor(value) then
		return value
	end

	local valueType = typeof(value)

	if valueType == "number" then
		return SingleMotor.new(value, false)
	elseif valueType == "table" then
		return GroupMotor.new(value, false)
	end

	error(("Unable to convert %q to motor; type %s is unsupported"):format(value, valueType), 2)
end

function GroupMotor.new(initialValues, shouldStartImplicitly)
	assert(initialValues, "Missing argument #1: initialValues")
	assert(typeof(initialValues) == "table", "initialValues must be a table!")

	local self = setmetatable(BaseMotor.new(), GroupMotor)

	if shouldStartImplicitly ~= nil then
		self._shouldStartImplicitly = shouldStartImplicitly
	else
		self._shouldStartImplicitly = true
	end

	self._complete = true
	self._motors = {}

	for key, value in pairs(initialValues) do
		self._motors[key] = toMotor(value)
	end

	return self
end

function GroupMotor:step(deltaTime)
	if self._complete then
		return true
	end

	local allMotorsComplete = true

	for _, motor in pairs(self._motors) do
		local complete = motor:step(deltaTime)
		if not complete then
			-- If any of the sub-motors are incomplete, the group motor will not be complete either
			allMotorsComplete = false
		end
	end

	self._onStep:fire(self:getValue())

	if allMotorsComplete then
		self._complete = true
		self._onComplete:fire()
	end

	return allMotorsComplete
end

function GroupMotor:setGoal(goals)
	self._complete = false

	for key, goal in pairs(goals) do
		local motor = assert(self._motors[key], ("Unknown motor for key %s"):format(key))
		motor:setGoal(goal)
	end

	if self._shouldStartImplicitly then
		self:start()
	end
end

function GroupMotor:getValue()
	local values = {}

	for key, motor in pairs(self._motors) do
		values[key] = motor:getValue()
	end

	return values
end

function GroupMotor:__tostring()
	return "Motor(Group)"
end

return GroupMotor


local Instant = {}
Instant.__index = Instant

function Instant.new(targetValue)
	return setmetatable({
		_targetValue = targetValue
	}, Instant)
end

function Instant:step()
	return {
		complete = true,
		value = self._targetValue
	}
end

return Instant


local Connection = {}
Connection.__index = Connection

function Connection.new(signal, handler)
	return setmetatable({
		signal = signal,
		connected = true,
		_handler = handler
	}, Connection)
end

function Connection:disconnect()
	if self.connected then
		self.connected = false

		for index, connection in pairs(self.signal._connections) do
			if connection == self then
				table.remove(self.signal._connections, index)
				return
			end
		end
	end
end

local Signal = {}
Signal.__index = Signal

function Signal.new()
	return setmetatable({
		_connections = {},
		_threads = {}
	}, Signal)
end

function Signal:fire(...)
	for _, connection in pairs(self._connections) do
		connection._handler(...)
	end

	for _, thread in pairs(self._threads) do
		coroutine.resume(thread, ...)
	end

	self._threads = {}
end

function Signal:connect(handler)
	local connection = Connection.new(self, handler)
	table.insert(self._connections, connection)
	return connection
end

function Signal:wait()
	table.insert(self._threads, coroutine.running())
	return coroutine.yield()
end

return Signal


local BaseMotor = require(script.Parent.BaseMotor)

local SingleMotor = setmetatable({}, BaseMotor)
SingleMotor.__index = SingleMotor

function SingleMotor.new(initialValue, shouldStartImplicitly)
	assert(initialValue, "Missing argument #1: initialValue")
	assert(typeof(initialValue) == "number", "initialValue must be a number!")

	local self = setmetatable(BaseMotor.new(), SingleMotor)

	if shouldStartImplicitly ~= nil then
		self._shouldStartImplicitly = shouldStartImplicitly
	else
		self._shouldStartImplicitly = true
	end

	self._goal = nil
	self._state = {
		complete = true,
		value = initialValue
	}

	return self
end

function SingleMotor:step(deltaTime)
	if self._state.complete then
		return true
	end

	local newState = self._goal:step(self._state, deltaTime)

	self._state = newState
	self._onStep:fire(newState.value)

	if newState.complete then
		self._onComplete:fire()
	end

	return newState.complete
end

function SingleMotor:getValue()
	return self._state.value
end

function SingleMotor:setGoal(goal)
	self._state.complete = false
	self._goal = goal

	if self._shouldStartImplicitly then
		self:start()
	end
end

function SingleMotor:__tostring()
	return "Motor(Single)"
end

return SingleMotor


local Flipper = {
	SingleMotor = require(script.SingleMotor),
	GroupMotor = require(script.GroupMotor),

	Instant = require(script.Instant),
	Spring = require(script.Spring),

	isMotor = require(script.isMotor)
}

return Flipper


local function isMotor(value)
	local motorType = tostring(value):match("^Motor%((.+)%)$")

	if motorType then
		return true, motorType
	else
		return false
	end
end

return isMotor


local Promise = require(script.Parent.Parent.Promise)
local t = require(script.Parent.Parent.t)
local Document = require(script.Parent.Document)
local stackSkipAssert = require(script.Parent.stackSkipAssert).stackSkipAssert
local getTime = require(script.Parent.getTime).getTime

local DOCUMENT_COOLDOWN = 7

local Collection = {}
Collection.__index = Collection

function Collection.new(name, options)
	options = options or {}

	stackSkipAssert(options.schema ~= nil, "You must provide a schema in options")

	local runSchema = t.strictInterface(options.schema)

	local defaultDataOk, defaultDataError = runSchema(options.defaultData)

	if not defaultDataOk then
		error(("The default data you provided for collection %q does not pass your schema requirements.\n\n%s\n"):format(
			name,
			defaultDataError
		), 2)
	end

	return setmetatable({
		name = name;
		schema = options.schema;
		runSchema = runSchema;
		defaultData = options.defaultData;
		_migrations = options.migrations or {};
		_activeDocuments = {};
		_justClosedDocuments = {};
	}, Collection)
end

function Collection:getDocument(name)
	name = tostring(name)

	if self._justClosedDocuments[name] then
		local waitTime = DOCUMENT_COOLDOWN - (getTime() - self._justClosedDocuments[name])

		if waitTime > 0 then
			warn(("Document %q in %q was recently closed. Your getDocument call will be delayed by %.1f seconds."):format(
				name,
				self.name,
				waitTime
			))
			Promise.delay(waitTime):await() -- todo:  yields
		end
	end

	if self._activeDocuments[name] == nil then
		self._activeDocuments[name] = Document.new(self, name)
	end

	local promise = self._activeDocuments[name]:readyPromise()

	promise:catch(function()
		self._activeDocuments[name] = nil
	end)

	return promise
end

function Collection:getLatestMigrationVersion()
	return #self._migrations
end

function Collection:validateData(data)
	return self.runSchema(data)
end

function Collection:keyExists(key)
	return self.schema[key] ~= nil
end

function Collection:validateKey(key, value)
	if self:keyExists(key) then
		return self.schema[key](value)
	else
		return false, ("Key %q is not present in %q's schema."):format(key, self.name)
	end
end

function Collection:_removeDocument(name)
	self._justClosedDocuments[name] = getTime()
	self._activeDocuments[name] = nil

	Promise.delay(DOCUMENT_COOLDOWN):andThen(function()
		self._justClosedDocuments[name] = nil
	end)
end

return Collection


local Promise = require(script.Parent.Parent.Promise)
local AccessLayer = require(script.Parent.Layers.AccessLayer)
local DocumentData = require(script.Parent.DocumentData)
local Error = require(script.Parent.Error)
local stackSkipAssert = require(script.Parent.stackSkipAssert).stackSkipAssert

local Document = {}
Document.__index = Document

function Document.new(collection, name)
	return setmetatable({
		collection = collection;
		name = name;
		_data = nil;
	}, Document)
end

function Document:readyPromise()
	if self._readyPromise == nil then
		self._readyPromise = Promise.new(function(resolve, reject)
			self._data = DocumentData.new({
				lockSession = AccessLayer.acquireLockSession(self.collection.name, self.name, self.collection._migrations);
				collection = self.collection;
			})

			local schemaOk, schemaError = self.collection:validateData(self._data:read())

			if not schemaOk then
				reject(Error.new({
					kind = Error.Kind.SchemaValidationFailed,
					error = schemaError,
					context = ("Schema validation failed when loading data in collection %q key %q"):format(
						self.collection.name,
						self.name
					)
				}))
			end

			resolve(self)
		end)
	end

	-- Wrap in Promise.resolve to track unique consumers
	return Promise.resolve(self._readyPromise)
end

function Document:get(key)
	key = tostring(key)

	stackSkipAssert(self.collection:keyExists(key), ("Key %q does not appear in %q's schema."):format(
		key,
		self.collection.name
	))

	return self._data:read()[key]
end

function Document:set(key, value)
	stackSkipAssert(self._data:isClosed() == false, "Attempt to call :set() on a closed Document")

	key = tostring(key)

	stackSkipAssert(self.collection:validateKey(key, value))

	local current = self._data:read()
	current[key] = value
	self._data:write(current)
end

function Document:save()
	stackSkipAssert(self._data:isClosed() == false, "Attempt to call :save() on a closed Document")

	return Promise.new(function(resolve)
		self._data:save()
		resolve()
	end)
end

function Document:close()
	stackSkipAssert(self._data:isClosed() == false, "Attempt to call :close() on a closed Document")

	return Promise.new(function(resolve)
		self._data:close()
		resolve()
	end):finally(function()
		self.collection:_removeDocument(self.name)
	end)
end

function Document:isClosed()
	return self._data:isClosed()
end

return Document


local DocumentData = {}
DocumentData.__index = DocumentData

-- TODO: Backups

function DocumentData.new(options)
	return setmetatable({
		_lockSession = options.lockSession;
		_readOnlyData = options.readOnlyData;
		_collection = options.collection;
		_currentData = nil;
		_dataLoaded = false;
		_closed = false;
	}, DocumentData)
end

function DocumentData:isClosed()
	return self._closed
end

function DocumentData:_load()
	if self._lockSession then
		return self._lockSession:read()
	else
		return self._readOnlyData
	end
end

function DocumentData:read()
	if self._dataLoaded == false then
		local newData = self:_load()

		if newData == nil then
			newData = self._collection.defaultData or {}
		end

		assert(self._collection:validateData(newData))

		self._currentData = newData
		self._dataLoaded = true
	end

	return self._currentData
end

function DocumentData:write(value)
	if self._lockSession == nil then
		error("Can't write to a readonly DocumentData")
	end

	self._currentData = value
end

function DocumentData:save()
	if self._lockSession == nil then
		error("Can't save on a readonly DocumentData")
	end

	self._lockSession:write(self._currentData)
end

function DocumentData:close()
	self._closed = true

	if self._lockSession then
		self._lockSession:unlockWithFinalData(self._currentData)
	end
end

return DocumentData


local makeEnum = require(script.Parent.makeEnum).makeEnum
local getTime = require(script.Parent.getTime).getTime

local Error = {
	Kind = makeEnum("Quicksave.Error.Kind", {
		"DataStoreError",
		"CouldNotAcquireLock",
		"LockConsistencyViolation",
		"SchemaValidationFailed"
	}),
}
Error.__index = Error

function Error.new(options, parent)
	options = options or {}
	return setmetatable({
		error = tostring(options.error) or "[This error has no error text.]",
		trace = options.trace,
		context = options.context,
		kind = options.kind,
		parent = parent,
		createdTick = getTime(),
		createdTrace = debug.traceback(),
	}, Error)
end

function Error.is(anything)
	if type(anything) == "table" then
		local metatable = getmetatable(anything)

		if type(metatable) == "table" then
			return rawget(anything, "error") ~= nil and type(rawget(metatable, "extend")) == "function"
		end
	end

	return false
end

function Error.isKind(anything, kind)
	assert(kind ~= nil, "Argument #2 to Quicksave.Error.isKind must not be nil")

	return Error.is(anything) and anything.kind == kind
end

function Error:extend(options)
	options = options or {}

	options.kind = options.kind or self.kind

	return Error.new(options, self)
end

function Error:getErrorChain()
	local runtimeErrors = { self }

	while runtimeErrors[#runtimeErrors].parent do
		table.insert(runtimeErrors, runtimeErrors[#runtimeErrors].parent)
	end

	return runtimeErrors
end

function Error:__tostring()
	local errorStrings = {
		string.format("-- Quicksave.Error(%s) --", self.kind or "?"),
	}

	for _, runtimeError in ipairs(self:getErrorChain()) do
		table.insert(errorStrings, table.concat({
			runtimeError.trace or runtimeError.error,
			runtimeError.context,
		}, "\n"))
	end

	return table.concat(errorStrings, "\n")
end

return Error


local MigrationLayer = require(script.Parent.Parent.MigrationLayer)
local Error = require(script.Parent.Parent.Parent.Error)
local Promise = require(script.Parent.Parent.Parent.Parent.Promise)
local getTime = require(script.Parent.Parent.Parent.getTime).getTime

local HttpService = game:GetService("HttpService")

local LOCK_EXPIRE = 60 * 5
local WRITE_MAX_INTERVAL = 7

local function consistencyError()
	return Error.new({
		kind = Error.Kind.LockConsistencyViolation,
		error = "Lock was changed after we acquired it. Aborting operation."
	})
end

local LockSession = {}
LockSession.__index = LockSession

function LockSession.new(collection, key, migrations)
	assert(migrations ~= nil, "migrations is nil")

	return setmetatable({
		collection = collection;
		key = key;
		_lockId = HttpService:GenerateGUID();
		_locked = false;
		_value = nil;
		_lastWrite = -1;
		_pendingData = nil;
		_pendingPromise = nil;
		_pendingClose = false;
		_migrations = migrations;
	}, LockSession)
end

function LockSession:lock()
	if self._locked then
		error("Attempt to lock when already locked")
	end

	local success

	self._value = MigrationLayer.update(self.collection, self.key, function(value)
		value = value or {
			createdAt = os.time();
			updatedAt = os.time();
		}

		if type(value.lockedAt) == "number" and os.time() - value.lockedAt < LOCK_EXPIRE then
			success = false
			return nil
		end

		value.lockId = self._lockId
		value.lockedAt = os.time()

		success = true

		return value
	end, self._migrations)

	if success and self._value.lockId == self._lockId then
		self._locked = true
		self._lastWrite = getTime()
		return self
	end

	error(Error.new({
		kind = Error.Kind.CouldNotAcquireLock,
		error = "Could not acquire lock"
	}))
end

function LockSession:_checkConsistency(value)
	return value.lockId and value.lockId == self._lockId
end

function LockSession:_ensureLocked()
	if not self._locked then
		error("Attempt to access LockSession without a lock")
	end
end

function LockSession:write(data)
	self:_ensureLocked()

	if self._pendingClose then
		error("Attempt to write to a LockSession that is pending to be closed.")
	end

	if data == nil then
		warn("[Debug, remove for release] LockSession:write data is nil!")
	end

	if getTime() - self._lastWrite < WRITE_MAX_INTERVAL then
		warn("Queueing key =", self.collection, self.key)
		self._pendingData = data

		if not self._pendingPromise then
			self._pendingPromise = Promise.delay(WRITE_MAX_INTERVAL - (getTime() - self._lastWrite)):andThen(function()
				self._pendingPromise = nil

				if self._pendingClose then
					self._pendingClose = false
					self:unlock()
				else
					local pendingData = self._pendingData
					self._pendingData = nil
					self:write(pendingData)
				end
			end)
		end

		return self._pendingPromise:expect()
	end

	local errorVal
	self._value = MigrationLayer.update(self.collection, self.key, function(value)
		if not self:_checkConsistency(value) then
			errorVal = consistencyError()
			return nil
		end

		value.updatedAt = os.time()
		value.lockedAt = os.time()
		value.data = data

		return value
	end, self._migrations)

	if errorVal then
		error(errorVal)
	end

	self._lastWrite = getTime()
end

function LockSession:read()
	self:_ensureLocked()

	return self._value.data
end

function LockSession:getUpdatedTimestamp()
	self:_ensureLocked()
	return self._value.updatedAt
end

function LockSession:getCreatedTimestamp()
	self:_ensureLocked()
	return self._value.createdAt
end

function LockSession:unlockWithFinalData(data)
	self._pendingData = data

	return self:unlock()
end

function LockSession:unlock()
	if getTime() - self._lastWrite < WRITE_MAX_INTERVAL or self._pendingPromise then
		self._pendingClose = true

		if self._pendingPromise == nil then
			self._pendingPromise = Promise.delay(WRITE_MAX_INTERVAL - (getTime() - self._lastWrite)):andThen(function()
				self._pendingPromise = nil
				self:unlock()
			end)
		end

		return self._pendingPromise:expect()
	end

	self._locked = false
	self._value = nil

	MigrationLayer.update(self.collection, self.key, function(value)
		if not self:_checkConsistency(value) then
			return nil
		end

		if self._pendingData then
			value.data = self._pendingData
			self._pendingData = nil
		end

		value.lockedAt = nil
		value.lockId = nil

		return value
	end, self._migrations)
end

return LockSession


local LockSession = require(script.LockSession)
local MigrationLayer = require(script.Parent.MigrationLayer)

local AccessLayer = {}

function AccessLayer.acquireLockSession(collection, key, migrations)
	local lockSession = LockSession.new(collection, key, migrations)

	return lockSession:lock()
end

function AccessLayer.readWithoutLock(collection, key, migrations)
	return MigrationLayer.read(collection, key, migrations)
end

return AccessLayer


local dictionary, length = {}, 0
for i = 32, 127 do
	if i ~= 34 and i ~= 92 then
		local c = string.char(i)
		dictionary[c], dictionary[length] = length, c
		length = length + 1
	end
end

local escapemap = {}
for i = 1, 34 do
	i = ({34, 92, 127})[i-31] or i
	local c, e = string.char(i), string.char(i + 31)
	escapemap[c], escapemap[e] = e, c
end
local function escape(s)
	return (s:gsub("[%c\"\\]", function(c)
		return "\127"..escapemap[c]
	end))
end

local function unescape(s)
	return (s:gsub("\127(.)", function(c)
		return escapemap[c]
	end))
end

local function copy(t)
	local new = {}
	for k, v in pairs(t) do
		new[k] = v
	end
	return new
end

local function tobase93(n)
	local value = ""
	repeat
		local remainder = n%93
		value = dictionary[remainder]..value
		n = (n - remainder)/93
	until n == 0
	return value
end

local function tobase10(value)
	local n = 0
	for i = 1, #value do
		n = n + 93^(i-1)*dictionary[value:sub(-i, -i)]
	end
	return n
end

local function compress(text)
	local dictionary = copy(dictionary)
	local key, sequence, size = "", {}, #dictionary
	local width, spans, span = 1, {}, 0
	local function listkey(key)
		local value = tobase93(dictionary[key])
		if #value > width then
			width, span, spans[width] = #value, 0, span
		end
		sequence[#sequence+1] = (" "):rep(width - #value)..value
		span = span + 1
	end
	text = escape(text)
	for i = 1, #text do
		local c = text:sub(i, i)
		local new = key..c
		if dictionary[new] then
			key = new
		else
			listkey(key)
			key, size = c, size+1
			dictionary[new], dictionary[size] = size, new
		end
	end
    listkey(key)
	spans[width] = span
    return table.concat(spans, ",").."|"..table.concat(sequence)
end

local function decompress(text)
	local dictionary = copy(dictionary)
	local sequence, spans, content = {}, text:match("(.-)|(.*)")
	local groups, start = {}, 1
	for span in spans:gmatch("%d+") do
		local width = #groups+1
		groups[width] = content:sub(start, start + span*width - 1)
		start = start + span*width
	end
	local previous;
	for width = 1, #groups do
		for value in groups[width]:gmatch(('.'):rep(width)) do
			local entry = dictionary[tobase10(value)]
			if previous then
				if entry then
					sequence[#sequence+1] = entry
					dictionary[#dictionary+1] = previous..entry:sub(1, 1)
				else
					entry = previous..previous:sub(1, 1)
					sequence[#sequence+1] = entry
					dictionary[#dictionary+1] = entry
				end
			else
				sequence[1] = entry
			end
			previous = entry
		end
	end
	return unescape(table.concat(sequence))
end

return {
	compress = compress;
	decompress = decompress;
}


local LZW = require(script.Parent.Parent.LZW)

return {
	["compressed/1"] = {
		pack = function(value)
			return LZW.compress(value)
		end;
		unpack = function(value)
			return LZW.decompress(value);
		end;
	}
}


return {
	["raw/1"] = {
		pack = function(value)
			return value
		end;
		unpack = function(value)
			return value;
		end;
	}
}


local RetryLayer = require(script.Parent.RetryLayer)

local HttpService = game:GetService("HttpService")

local RawSchemes = require(script.Schemes.raw)
local CompressedSchemes = require(script.Schemes.compressed)

local MINIMUM_LENGTH_TO_COMPRESS = 1000

local DataLayer = {
	schemes = {
		["raw/1"] = RawSchemes["raw/1"];
		["compressed/1"] = CompressedSchemes["compressed/1"];
	}
}

function DataLayer._unpack(value)
	if value == nil then
		return nil
	end

	local scheme = value.scheme

	if not DataLayer.schemes[scheme] then
		error(("Unknown scheme: %q"):format(scheme))
	end

	return HttpService:JSONDecode(DataLayer.schemes[scheme].unpack(value.data))
end

function DataLayer._pack(value)
	value = HttpService:JSONEncode(value)

	local scheme
	if #value > MINIMUM_LENGTH_TO_COMPRESS then
		scheme = "compressed/1"
	else
		scheme = "raw/1"
	end

	return {
		scheme = scheme;
		data = DataLayer.schemes[scheme].pack(value)
	}
end

-- Todo: Prevent unpacking twice
function DataLayer.update(collection, key, callback)
	return DataLayer._unpack(RetryLayer.update(collection, key, function(value)
		return DataLayer._pack(callback(DataLayer._unpack(value)))
	end))
end

function DataLayer.read(collection, key)
	return DataLayer._unpack(RetryLayer.read(collection, key))
end

return DataLayer


local DataStoreService = require(script.Parent.Parent.Parent.MockDataStoreService)

local DataStoreLayer = {
    _dataStores = {};
}

function DataStoreLayer._getDataStore(collectionName)
    if DataStoreLayer._dataStores[collectionName] == nil then
        DataStoreLayer._dataStores[collectionName] = DataStoreService:GetDataStore(collectionName, "_package/eryn.io/quicksave")
    end

    return DataStoreLayer._dataStores[collectionName]
end

function DataStoreLayer.perform(methodName, collectionName, ...)
    local dataStore = DataStoreLayer._getDataStore(collectionName)

    return dataStore[methodName](dataStore, ...)
end

return DataStoreLayer


local DataLayer = require(script.Parent.DataLayer)

local MigrationLayer = {}

function MigrationLayer._unpack(value, migrations)
	value = value or {
		generation = #migrations;
		data = nil;
	}

	local data = value.data
	local generation = value.generation

	if generation < #migrations then
		for i = generation + 1, #migrations do
			data.data = migrations[i](data.data)
		end
	end

	return data
end

function MigrationLayer._pack(value, migrations)
	return {
		data = value;
		generation = #migrations;
	}
end

-- Todo: Prevent unpacking twice
function MigrationLayer.update(collection, key, callback, migrations)
	return MigrationLayer._unpack(DataLayer.update(collection, key, function(value)
		return MigrationLayer._pack(callback(MigrationLayer._unpack(value, migrations)), migrations)
	end), migrations)
end

function MigrationLayer.read(collection, key, migrations)
	return MigrationLayer._unpack(DataLayer.read(collection, key), migrations)
end

return MigrationLayer


local ThrottleLayer = require(script.Parent.ThrottleLayer)
local Error = require(script.Parent.Parent.Error)

local RetryLayer = {}

function RetryLayer._retry(callback, ...)
	local attempts = 0

	while attempts < 5 do
		attempts = attempts + 1

		local ok, value = pcall(callback, ...)

		if ok then
			return value
		end

		if attempts < 5 then
			warn(("[Quicksave] DataStore operation failed. Retrying...\nError:\n%s"):format(
				tostring(value)
			))
		else
			error(Error.new({
				kind = Error.Kind.DataStoreError,
				error = value,
				context = "Failed after 5 retries."
			}))
		end
	end

end

function RetryLayer.update(...)
	return RetryLayer._retry(function(...)
		return ThrottleLayer.update(...)
	end, ...)
end

function RetryLayer.read(...)
	return RetryLayer._retry(function(...)
		return ThrottleLayer.read(...)
	end, ...)
end

return RetryLayer


local DataStoreService = require(script.Parent.Parent.Parent.MockDataStoreService)
local RunService = game:GetService("RunService")

local DataStoreLayer = require(script.Parent.DataStoreLayer)

local METHOD_RESOURCE_MAP = {
	GetAsync = Enum.DataStoreRequestType.GetAsync;
	UpdateAsync = Enum.DataStoreRequestType.UpdateAsync,
	RemoveAsync = Enum.DataStoreRequestType.SetIncrementAsync,
	SetAsync = Enum.DataStoreRequestType.SetIncrementAsync,
}

local ThrottleLayer = {
	_queue = {};
	_dataStores = {};
}

function ThrottleLayer._perform(methodName, collectionName, ...)
	local resource = METHOD_RESOURCE_MAP[methodName]

	if DataStoreService:GetRequestBudgetForRequestType(resource) > 0 then
		return DataStoreLayer.perform(methodName, collectionName, ...)
	end

	if ThrottleLayer._queue[resource] == nil then
		ThrottleLayer._queue[resource] = {}

		local connection
		connection = RunService.Heartbeat:Connect(function()
			for _, thread in ipairs(ThrottleLayer._queue[resource]) do
				if DataStoreService:GetRequestBudgetForRequestType(resource) > 0 then
					DataStoreLayer.perform(unpack(thread))
				else
					break
				end
			end

			if #ThrottleLayer._queue[resource] == 0 then
				connection:Disconnect()
				ThrottleLayer._queue[resource] = nil
			end
		end)
	end

	table.insert(ThrottleLayer._queue[resource], { methodName, ... })
end

function ThrottleLayer.update(collection, key, callback)
	return ThrottleLayer._perform("UpdateAsync", collection, key, callback)
end

function ThrottleLayer.read(collection, key)
	return ThrottleLayer._perform("GetAsync", collection, key)
end

function ThrottleLayer.write(collection, key, value)
	return ThrottleLayer._perform("SetAsync", collection, key, value)
end

function ThrottleLayer.remove(collection, key)
	return ThrottleLayer._perform("RemoveAsync", collection, key)
end

return ThrottleLayer


local t = require(script.Parent.t)
local Promise = require(script.Parent.Promise)
local Collection = require(script.Collection)
local Error = require(script.Error)

local Quicksave = {
	t = t;
	Promise = Promise;
	Error = Error;

	_collections = {};
}

function Quicksave.createCollection(name, options)
	if Quicksave._collections[name] then
		error(("Collection %q already exists"):format(name))
	end

	Quicksave._collections[name] = Collection.new(name, options)

	return Quicksave._collections[name]
end

function Quicksave.getCollection(name)
	return Quicksave._collections[name] or error(("Collection %q hasn't been created yet!"):format(name))
end

return Quicksave


local function makeEnum(enumName, members)
	local enum = {}

	for _, memberName in ipairs(members) do
		enum[memberName] = memberName
	end

	return setmetatable(enum, {
		__index = function(_, k)
			error(string.format("%s is not in %s!", k, enumName), 2)
		end,
		__newindex = function()
			error(string.format("Creating new members in %s is not allowed!", enumName), 2)
		end,
	})
end

return {
	makeEnum = makeEnum;
}


local function stackSkipAssert(condition, text)
	if not condition then
		error(text, 3)
	end
end

return {
	stackSkipAssert = stackSkipAssert;
}


-- t: a runtime typechecker for Roblox

-- regular lua compatibility
local typeof = typeof or type

local function primitive(typeName)
	return function(value)
		local valueType = typeof(value)
		if valueType == typeName then
			return true
		else
			return false, string.format("%s expected, got %s", typeName, valueType)
		end
	end
end

local t = {}

--[[**
	matches any type except nil

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.any(value)
	if value ~= nil then
		return true
	else
		return false, "any expected, got nil"
	end
end

--Lua primitives

--[[**
	ensures Lua primitive boolean type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.boolean = primitive("boolean")

--[[**
	ensures Lua primitive thread type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.thread = primitive("thread")

--[[**
	ensures Lua primitive callback type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.callback = primitive("function")
t["function"] = t.callback

--[[**
	ensures Lua primitive none type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.none = primitive("nil")
t["nil"] = t.none

--[[**
	ensures Lua primitive string type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.string = primitive("string")

--[[**
	ensures Lua primitive table type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.table = primitive("table")

--[[**
	ensures Lua primitive userdata type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.userdata = primitive("userdata")

--[[**
	ensures value is a number and non-NaN

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.number(value)
	local valueType = typeof(value)
	if valueType == "number" then
		if value == value then
			return true
		else
			return false, "unexpected NaN value"
		end
	else
		return false, string.format("number expected, got %s", valueType)
	end
end

--[[**
	ensures value is NaN

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.nan(value)
	if value ~= value then
		return true
	else
		return false, "unexpected non-NaN value"
	end
end

-- roblox types

--[[**
	ensures Roblox Axes type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Axes = primitive("Axes")

--[[**
	ensures Roblox BrickColor type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.BrickColor = primitive("BrickColor")

--[[**
	ensures Roblox CFrame type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.CFrame = primitive("CFrame")

--[[**
	ensures Roblox Color3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Color3 = primitive("Color3")

--[[**
	ensures Roblox ColorSequence type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.ColorSequence = primitive("ColorSequence")

--[[**
	ensures Roblox ColorSequenceKeypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.ColorSequenceKeypoint = primitive("ColorSequenceKeypoint")

--[[**
	ensures Roblox DockWidgetPluginGuiInfo type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.DockWidgetPluginGuiInfo = primitive("DockWidgetPluginGuiInfo")

--[[**
	ensures Roblox Faces type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Faces = primitive("Faces")

--[[**
	ensures Roblox Instance type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Instance = primitive("Instance")

--[[**
	ensures Roblox NumberRange type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberRange = primitive("NumberRange")

--[[**
	ensures Roblox NumberSequence type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberSequence = primitive("NumberSequence")

--[[**
	ensures Roblox NumberSequenceKeypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.NumberSequenceKeypoint = primitive("NumberSequenceKeypoint")

--[[**
	ensures Roblox PathWaypoint type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.PathWaypoint = primitive("PathWaypoint")

--[[**
	ensures Roblox PhysicalProperties type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.PhysicalProperties = primitive("PhysicalProperties")

--[[**
	ensures Roblox Random type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Random = primitive("Random")

--[[**
	ensures Roblox Ray type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Ray = primitive("Ray")

--[[**
	ensures Roblox Rect type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Rect = primitive("Rect")

--[[**
	ensures Roblox Region3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Region3 = primitive("Region3")

--[[**
	ensures Roblox Region3int16 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Region3int16 = primitive("Region3int16")

--[[**
	ensures Roblox TweenInfo type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.TweenInfo = primitive("TweenInfo")

--[[**
	ensures Roblox UDim type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.UDim = primitive("UDim")

--[[**
	ensures Roblox UDim2 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.UDim2 = primitive("UDim2")

--[[**
	ensures Roblox Vector2 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector2 = primitive("Vector2")

--[[**
	ensures Roblox Vector3 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector3 = primitive("Vector3")

--[[**
	ensures Roblox Vector3int16 type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Vector3int16 = primitive("Vector3int16")

-- roblox enum types

--[[**
	ensures Roblox Enum type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.Enum = primitive("Enum")

--[[**
	ensures Roblox EnumItem type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.EnumItem = primitive("EnumItem")

--[[**
	ensures Roblox RBXScriptSignal type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RBXScriptSignal = primitive("RBXScriptSignal")

--[[**
	ensures Roblox RBXScriptConnection type

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
t.RBXScriptConnection = primitive("RBXScriptConnection")

--[[**
	ensures value is a given literal value

	@param literal The literal to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.literal(...)
	local size = select("#", ...)
	if size == 1 then
		local literal = ...
		return function(value)
			if value ~= literal then
				return false, string.format("expected %s, got %s", tostring(literal), tostring(value))
			end

			return true
		end
	else
		local literals = {}
		for i = 1, size do
			local value = select(i, ...)
			literals[i] = t.literal(value)
		end

		return t.union(table.unpack(literals, 1, size))
	end
end

--[[**
	DEPRECATED
	Please use t.literal
**--]]
t.exactly = t.literal

--[[**
	Returns a t.union of each key in the table as a t.literal

	@param keyTable The table to get keys from

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.keyOf(keyTable)
	local keys = {}
	local length = 0
	for key in pairs(keyTable) do
		length = length + 1
		keys[length] = key
	end

	return t.literal(table.unpack(keys, 1, length))
end

--[[**
	Returns a t.union of each value in the table as a t.literal

	@param valueTable The table to get values from

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.valueOf(valueTable)
	local values = {}
	local length = 0
	for _, value in pairs(valueTable) do
		length = length + 1
		values[length] = value
	end

	return t.literal(table.unpack(values, 1, length))
end

--[[**
	ensures value is an integer

	@param value The value to check against

	@returns True iff the condition is satisfied, false otherwise
**--]]
function t.integer(value)
	local success, errMsg = t.number(value)
	if not success then
		return false, errMsg or ""
	end

	if value % 1 == 0 then
		return true
	else
		return false, string.format("integer expected, got %s", value)
	end
end

--[[**
	ensures value is a number where min <= value

	@param min The minimum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMin(min)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if value >= min then
			return true
		else
			return false, string.format("number >= %s expected, got %s", min, value)
		end
	end
end

--[[**
	ensures value is a number where value <= max

	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMax(max)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg
		end

		if value <= max then
			return true
		else
			return false, string.format("number <= %s expected, got %s", max, value)
		end
	end
end

--[[**
	ensures value is a number where min < value

	@param min The minimum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMinExclusive(min)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if min < value then
			return true
		else
			return false, string.format("number > %s expected, got %s", min, value)
		end
	end
end

--[[**
	ensures value is a number where value < max

	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberMaxExclusive(max)
	return function(value)
		local success, errMsg = t.number(value)
		if not success then
			return false, errMsg or ""
		end

		if value < max then
			return true
		else
			return false, string.format("number < %s expected, got %s", max, value)
		end
	end
end

--[[**
	ensures value is a number where value > 0

	@returns A function that will return true iff the condition is passed
**--]]
t.numberPositive = t.numberMinExclusive(0)

--[[**
	ensures value is a number where value < 0

	@returns A function that will return true iff the condition is passed
**--]]
t.numberNegative = t.numberMaxExclusive(0)

--[[**
	ensures value is a number where min <= value <= max

	@param min The minimum to use
	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberConstrained(min, max)
	assert(t.number(min) and t.number(max))
	local minCheck = t.numberMin(min)
	local maxCheck = t.numberMax(max)

	return function(value)
		local minSuccess, minErrMsg = minCheck(value)
		if not minSuccess then
			return false, minErrMsg or ""
		end

		local maxSuccess, maxErrMsg = maxCheck(value)
		if not maxSuccess then
			return false, maxErrMsg or ""
		end

		return true
	end
end

--[[**
	ensures value is a number where min < value < max

	@param min The minimum to use
	@param max The maximum to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.numberConstrainedExclusive(min, max)
	assert(t.number(min) and t.number(max))
	local minCheck = t.numberMinExclusive(min)
	local maxCheck = t.numberMaxExclusive(max)

	return function(value)
		local minSuccess, minErrMsg = minCheck(value)
		if not minSuccess then
			return false, minErrMsg or ""
		end

		local maxSuccess, maxErrMsg = maxCheck(value)
		if not maxSuccess then
			return false, maxErrMsg or ""
		end

		return true
	end
end

--[[**
	ensures value matches string pattern

	@param string pattern to check against

	@returns A function that will return true iff the condition is passed
**--]]
function t.match(pattern)
	assert(t.string(pattern))
	return function(value)
		local stringSuccess, stringErrMsg = t.string(value)
		if not stringSuccess then
			return false, stringErrMsg
		end

		if string.match(value, pattern) == nil then
			return false, string.format("%q failed to match pattern %q", value, pattern)
		end

		return true
	end
end

--[[**
	ensures value is either nil or passes check

	@param check The check to use

	@returns A function that will return true iff the condition is passed
**--]]
function t.optional(check)
	assert(t.callback(check))
	return function(value)
		if value == nil then
			return true
		end

		local success, errMsg = check(value)
		if success then
			return true
		else
			return false, string.format("(optional) %s", errMsg or "")
		end
	end
end

--[[**
	matches given tuple against tuple type definition

	@param ... The type definition for the tuples

	@returns A function that will return true iff the condition is passed
**--]]
function t.tuple(...)
	local checks = {...}
	return function(...)
		local args = {...}
		for i, check in ipairs(checks) do
			local success, errMsg = check(args[i])
			if success == false then
				return false, string.format("Bad tuple index #%s:\n\t%s", i, errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures all keys in given table pass check

	@param check The function to use to check the keys

	@returns A function that will return true iff the condition is passed
**--]]
function t.keys(check)
	assert(t.callback(check))
	return function(value)
		local tableSuccess, tableErrMsg = t.table(value)
		if tableSuccess == false then
			return false, tableErrMsg or ""
		end

		for key in pairs(value) do
			local success, errMsg = check(key)
			if success == false then
				return false, string.format("bad key %s:\n\t%s", tostring(key), errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures all values in given table pass check

	@param check The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.values(check)
	assert(t.callback(check))
	return function(value)
		local tableSuccess, tableErrMsg = t.table(value)
		if tableSuccess == false then
			return false, tableErrMsg or ""
		end

		for key, val in pairs(value) do
			local success, errMsg = check(val)
			if success == false then
				return false, string.format("bad value for key %s:\n\t%s", tostring(key), errMsg or "")
			end
		end

		return true
	end
end

--[[**
	ensures value is a table and all keys pass keyCheck and all values pass valueCheck

	@param keyCheck The function to use to check the keys
	@param valueCheck The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.map(keyCheck, valueCheck)
	assert(t.callback(keyCheck), t.callback(valueCheck))
	local keyChecker = t.keys(keyCheck)
	local valueChecker = t.values(valueCheck)

	return function(value)
		local keySuccess, keyErr = keyChecker(value)
		if not keySuccess then
			return false, keyErr or ""
		end

		local valueSuccess, valueErr = valueChecker(value)
		if not valueSuccess then
			return false, valueErr or ""
		end

		return true
	end
end

--[[**
	ensures value is a table and all keys pass valueCheck and all values are true

	@param valueCheck The function to use to check the values

	@returns A function that will return true iff the condition is passed
**--]]
function t.set(valueCheck)
	return t.map(valueCheck, t.literal(true))
end

do
	local arrayKeysCheck = t.keys(t.integer)
	--[[**
		ensures value is an array and all values of the array match check

		@param check The check to compare all values with

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.array(check)
		assert(t.callback(check))
		local valuesCheck = t.values(check)

		return function(value)
			local keySuccess, keyErrMsg = arrayKeysCheck(value)
			if keySuccess == false then
				return false, string.format("[array] %s", keyErrMsg or "")
			end

			-- # is unreliable for sparse arrays
			-- Count upwards using ipairs to avoid false positives from the behavior of #
			local arraySize = 0

			for _ in ipairs(value) do
				arraySize = arraySize + 1
			end

			for key in pairs(value) do
				if key < 1 or key > arraySize then
					return false, string.format("[array] key %s must be sequential", tostring(key))
				end
			end

			local valueSuccess, valueErrMsg = valuesCheck(value)
			if not valueSuccess then
				return false, string.format("[array] %s", valueErrMsg or "")
			end

			return true
		end
	end

	--[[**
		ensures value is an array of a strict makeup and size

		@param check The check to compare all values with

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.strictArray(...)
		local valueTypes = { ... }
		assert(t.array(t.callback)(valueTypes))

		return function(value)
			local keySuccess, keyErrMsg = arrayKeysCheck(value)
			if keySuccess == false then
				return false, string.format("[strictArray] %s", keyErrMsg or "")
			end

			-- If there's more than the set array size, disallow
			if #valueTypes < #value then
				return false, string.format("[strictArray] Array size exceeds limit of %d", #valueTypes)
			end

			for idx, typeFn in pairs(valueTypes) do
				local typeSuccess, typeErrMsg = typeFn(value[idx])
				if not typeSuccess then
					return false, string.format("[strictArray] Array index #%d - %s", idx, typeErrMsg)
				end
			end

			return true
		end
	end
end

do
	local callbackArray = t.array(t.callback)
	--[[**
		creates a union type

		@param ... The checks to union

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.union(...)
		local checks = {...}
		assert(callbackArray(checks))

		return function(value)
			for _, check in ipairs(checks) do
				if check(value) then
					return true
				end
			end

			return false, "bad type for union"
		end
	end

	--[[**
		Alias for t.union
	**--]]
	t.some = t.union

	--[[**
		creates an intersection type

		@param ... The checks to intersect

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.intersection(...)
		local checks = {...}
		assert(callbackArray(checks))

		return function(value)
			for _, check in ipairs(checks) do
				local success, errMsg = check(value)
				if not success then
					return false, errMsg or ""
				end
			end

			return true
		end
	end

	--[[**
		Alias for t.intersection
	**--]]
	t.every = t.intersection
end

do
	local checkInterface = t.map(t.any, t.callback)
	--[[**
		ensures value matches given interface definition

		@param checkTable The interface definition

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.interface(checkTable)
		assert(checkInterface(checkTable))
		return function(value)
			local tableSuccess, tableErrMsg = t.table(value)
			if tableSuccess == false then
				return false, tableErrMsg or ""
			end

			for key, check in pairs(checkTable) do
				local success, errMsg = check(value[key])
				if success == false then
					return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "")
				end
			end

			return true
		end
	end

	--[[**
		ensures value matches given interface definition strictly

		@param checkTable The interface definition

		@returns A function that will return true iff the condition is passed
	**--]]
	function t.strictInterface(checkTable)
		assert(checkInterface(checkTable))
		return function(value)
			local tableSuccess, tableErrMsg = t.table(value)
			if tableSuccess == false then
				return false, tableErrMsg or ""
			end

			for key, check in pairs(checkTable) do
				local success, errMsg = check(value[key])
				if success == false then
					return false, string.format("[interface] bad value for %s:\n\t%s", tostring(key), errMsg or "")
				end
			end

			for key in pairs(value) do
				if not checkTable[key] then
					return false, string.format("[interface] unexpected field %q", tostring(key))
				end
			end

			return true
		end
	end
end

--[[**
	ensure value is an Instance and it's ClassName matches the given ClassName

	@param className The class name to check for

	@returns A function that will return true iff the condition is passed
**--]]
function t.instanceOf(className, childTable)
	assert(t.string(className))

	local childrenCheck
	if childTable ~= nil then
		childrenCheck = t.children(childTable)
	end

	return function(value)
		local instanceSuccess, instanceErrMsg = t.Instance(value)
		if not instanceSuccess then
			return false, instanceErrMsg or ""
		end

		if value.ClassName ~= className then
			return false, string.format("%s expected, got %s", className, value.ClassName)
		end

		if childrenCheck then
			local childrenSuccess, childrenErrMsg = childrenCheck(value)
			if not childrenSuccess then
				return false, childrenErrMsg
			end
		end

		return true
	end
end

t.instance = t.instanceOf

--[[**
	ensure value is an Instance and it's ClassName matches the given ClassName by an IsA comparison

	@param className The class name to check for

	@returns A function that will return true iff the condition is passed
**--]]
function t.instanceIsA(className, childTable)
	assert(t.string(className))

	local childrenCheck
	if childTable ~= nil then
		childrenCheck = t.children(childTable)
	end

	return function(value)
		local instanceSuccess, instanceErrMsg = t.Instance(value)
		if not instanceSuccess then
			return false, instanceErrMsg or ""
		end

		if not value:IsA(className) then
			return false, string.format("%s expected, got %s", className, value.ClassName)
		end

		if childrenCheck then
			local childrenSuccess, childrenErrMsg = childrenCheck(value)
			if not childrenSuccess then
				return false, childrenErrMsg
			end
		end

		return true
	end
end

--[[**
	ensures value is an enum of the correct type

	@param enum The enum to check

	@returns A function that will return true iff the condition is passed
**--]]
function t.enum(enum)
	assert(t.Enum(enum))
	return function(value)
		local enumItemSuccess, enumItemErrMsg = t.EnumItem(value)
		if not enumItemSuccess then
			return false, enumItemErrMsg
		end

		if value.EnumType == enum then
			return true
		else
			return false, string.format("enum of %s expected, got enum of %s", tostring(enum), tostring(value.EnumType))
		end
	end
end

do
	local checkWrap = t.tuple(t.callback, t.callback)

	--[[**
		wraps a callback in an assert with checkArgs

		@param callback The function to wrap
		@param checkArgs The functon to use to check arguments in the assert

		@returns A function that first asserts using checkArgs and then calls callback
	**--]]
	function t.wrap(callback, checkArgs)
		assert(checkWrap(callback, checkArgs))
		return function(...)
			assert(checkArgs(...))
			return callback(...)
		end
	end
end

--[[**
	asserts a given check

	@param check The function to wrap with an assert

	@returns A function that simply wraps the given check in an assert
**--]]
function t.strict(check)
	return function(...)
		assert(check(...))
	end
end

do
	local checkChildren = t.map(t.string, t.callback)

	--[[**
		Takes a table where keys are child names and values are functions to check the children against.
		Pass an instance tree into the function.
		If at least one child passes each check, the overall check passes.

		Warning! If you pass in a tree with more than one child of the same name, this function will always return false

		@param checkTable The table to check against

		@returns A function that checks an instance tree
	**--]]
	function t.children(checkTable)
		assert(checkChildren(checkTable))

		return function(value)
			local instanceSuccess, instanceErrMsg = t.Instance(value)
			if not instanceSuccess then
				return false, instanceErrMsg or ""
			end

			local childrenByName = {}
			for _, child in ipairs(value:GetChildren()) do
				local name = child.Name
				if checkTable[name] then
					if childrenByName[name] then
						return false, string.format("Cannot process multiple children with the same name %q", name)
					end

					childrenByName[name] = child
				end
			end

			for name, check in pairs(checkTable) do
				local success, errMsg = check(childrenByName[name])
				if not success then
					return false, string.format("[%s.%s] %s", value:GetFullName(), name, errMsg or "")
				end
			end

			return true
		end
	end
end

return t


--!strict
local React = require(script.Parent.Parent.Parent.react)
--OverHash deviation: local Redux = require(script.Parent.Parent.Parent.Redux)
local Subscription = require(script.Parent.Parent.utils.Subscription)

export type ReactReduxContextValue<SS = any, A = any> = {
	store: any, --OverHash deviation: use any instead of typeof(Redux.Store.new()),
	subscription: Subscription.Subscription,
	getServerState: (() -> SS)?,
}

local ReactReduxContext = React.createContext(nil :: ReactReduxContextValue?)

export type ReactReduxContextInstance = typeof(ReactReduxContext)

--selene: allow(global_usage)
if _G.__DEV__ then
	ReactReduxContext.displayName = "ReactRedux"
end

return ReactReduxContext


--!strict

local React = require(script.Parent.Parent.Parent.react)
local ReactReduxContext = require(script.Parent.Context)
local Subscription = require(script.Parent.Parent.utils.Subscription)
-- OverHash deviation: local Redux = require(script.Parent.Parent.Parent.Redux)

export type ProviderProps<A = any, S = unknown> = {
	-- The single Redux store in your application.
	store: any, --OverHash deviation: use `any` instead of typeof(Redux.Store.new()),

	--[[
    * Optional context to be used internally in react-redux. Use React.createContext() to create a context to be used.
    * If this is used, you'll need to customize `connect` by supplying the same context provided to the Provider.
    * Initial value doesn't matter, as it is overwritten with the internal state of Provider.
    ]]
	context: React.Context<ReactReduxContext.ReactReduxContextValue<S, A>>?,
	children: React.ReactNode,
}

local function Provider<A, S>(props: ProviderProps<A, S>)
	local store = props.store
	local context = props.context
	local children = props.children

	local contextValue = React.useMemo(function()
		local subscription = Subscription.createSubscription(props.store)
		return {
			store = props.store,
			subscription = subscription,
		}
	end, { store })

	local previousState = React.useMemo(function()
		return store:getState()
	end, { store })

	React.useLayoutEffect(function()
		local subscription = contextValue.subscription
		subscription.onStateChange = subscription.notifyNestedSubs
		subscription.trySubscribe()

		if previousState ~= store:getState() then
			subscription.notifyNestedSubs()
		end
		return function()
			subscription.tryUnsubscribe()
			subscription.onStateChange = nil
		end
	end, { contextValue, previousState })

	local Context = context or ReactReduxContext

	return React.createElement(Context.Provider, {
		value = contextValue,
	}, children)
end

return Provider


--!strict

local React = require(script.Parent.Parent.Parent.react)
local ReactReduxContext = require(script.Parent.Parent.components.Context)
local useDefaultStore = require(script.Parent.useStore).useStore
local createStoreHook = require(script.Parent.useStore).createStoreHook

local exports = {}

--[[
 * Hook factory, which creates a `useDispatch` hook bound to a given context.
 *
 * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
 * @returns {Function} A `useDispatch` hook bound to the specified context.
 ]]
exports.createDispatchHook = function<S, A>(context: React.Context<ReactReduxContext.ReactReduxContextValue<S, A>>?)
	context = context or ReactReduxContext
	local useStore = if context == ReactReduxContext then useDefaultStore else createStoreHook(context)

	return function()
		local store = useStore()
		return function(action: { [string]: any })
			return store:dispatch(action)
		end
	end
end

--[[
 * A hook to access the redux `dispatch` function.
 *
 * @returns {any|function} redux store's `dispatch` function
 *
 * @example
 *
 * import React, { useCallback } from 'react'
 * import { useDispatch } from 'react-redux'
 *
 * export const CounterComponent = ({ value }) => {
 *   const dispatch = useDispatch()
 *   const increaseCounter = useCallback(() => dispatch({ type: 'increase-counter' }), [])
 *   return (
 *

 *       {value}
 *       <button onClick={increaseCounter}>Increase counter</button>
 *

 *   )
 * }
]]
exports.useDispatch = exports.createDispatchHook()

return exports


--!strict
local React = require(script.Parent.Parent.Parent.react)
local ReactReduxContext = require(script.Parent.Parent.components.Context)

local exports = {}

--[[
 * A hook to access the value of the `ReactReduxContext`. This is a low-level
 * hook that you should usually not need to call directly.
 *
 * @returns {any} the value of the `ReactReduxContext`
 *
 * @example
 *
 * import React from 'react'
 * import { useReduxContext } from 'react-redux'
 *
 * export const CounterComponent = () -> {
 *   const { store } = useReduxContext()
 *   return <div>{store.getState()}</div>
 * }
]]
exports.useReduxContext = function(): ReactReduxContext.ReactReduxContextValue?
	local contextValue = React.useContext(ReactReduxContext)

	--selene: allow(global_usage)
	if _G.__DEV__ and not contextValue then
		error("could not find react-redux context value; please ensure the component is wrapped in a <Provider>")
	end

	return contextValue
end

return exports


--!strict
local React = require(script.Parent.Parent.Parent.react)

local useDefaultReduxContext = require(script.Parent.useReduxContext).useReduxContext
local ReactReduxContext = require(script.Parent.Parent.components.Context)
local types = require(script.Parent.Parent.types)
local useSyncExternalStore = require(script.Parent.Parent.utils.useSyncExternalStore)

local notInitialized = useSyncExternalStore.notInitialized
type uSESWS = useSyncExternalStore.uSESWS

local exports = {}

-- Luau doens't think notInitialized can be casted into uSESWS
local useSyncExternalStoreWithSelectorUntyped: any = notInitialized
local useSyncExternalStoreWithSelector: uSESWS = useSyncExternalStoreWithSelectorUntyped
exports.initializeUseSelector = function(fn: uSESWS)
	useSyncExternalStoreWithSelector = fn
end

local refEquality: types.EqualityFn<any> = function(a, b)
	return a == b
end

--[[
 * Hook factory, which creates a `useSelector` hook bound to a given context.
 *
 * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
 * @returns {Function} A `useSelector` hook bound to the specified context.
]]
exports.createSelectorHook = function(context: ReactReduxContext.ReactReduxContextInstance?): <TState, Selected>(
	selector: (state: TState) -> Selected,
	equalityFn: types.EqualityFn<Selected>?
) -> Selected
	context = context or ReactReduxContext
	local useReduxContext: typeof(useDefaultReduxContext) = if context == ReactReduxContext
		then useDefaultReduxContext
		else function()
			return React.useContext(context)
		end

	return function<TState, Selected>(selector: (state: TState) -> Selected, equalityFn: types.EqualityFn<any>?): Selected
		equalityFn = equalityFn or refEquality

		--selene: allow(global_usage)
		if _G.__DEV__ then
			if not selector then
				error(`You must pass a selector to useSelector`)
			end
			if typeof(selector) ~= "function" then
				error(`You must pass a function as a selector to useSelector`)
			end
			if typeof(equalityFn) ~= "function" then
				error(`You must pass a function as an equality function to useSelector`)
			end
		end

		local reduxContext = useReduxContext()
		assert(reduxContext, "Error occured trying to find redux context. This is likely a bug.")

		local store = reduxContext.store
		local subscription = reduxContext.subscription
		local getServerState = reduxContext.getServerState

		local getState = React.useCallback(function()
			return store:getState()
		end, { store })

		local selectedState = useSyncExternalStoreWithSelector(
			subscription.addNestedSub,
			getState,
			getServerState or getState,
			selector,
			equalityFn
		)

		React.useDebugValue(selectedState)

		return selectedState
	end
end

--[[
 * A hook to access the redux store's state. This hook takes a selector function
 * as an argument. The selector is called with the store state.
 *
 * This hook takes an optional equality comparison function as the second parameter
 * that allows you to customize the way the selected state is compared to determine
 * whether the component needs to be re-rendered.
 *
 * @param {Function} selector the selector function
 * @param {Function=} equalityFn the function that will be used to determine equality
 *
 * @returns {any} the selected state
 *
 * @example
 *
 * import React from 'react'
 * import { useSelector } from 'react-redux'
 *
 * export const CounterComponent = () -> {
 *   const counter = useSelector(state -> state.counter)
 *   return <div>{counter}</div>
 * }
 ]]
exports.useSelector = exports.createSelectorHook()

return exports


--!strict
local React = require(script.Parent.Parent.Parent.react)
-- OverHash deviation: local Redux = require(script.Parent.Parent.Parent.Redux)
local Context = require(script.Parent.Parent.components.Context)
local useDefaultReduxContext = require(script.Parent.useReduxContext).useReduxContext

local exports = {}

--[[
 * Hook factory, which creates a `useStore` hook bound to a given context.
 *
 * @param {React.Context} [context=ReactReduxContext] Context passed to your `<Provider>`.
 * @returns {Function} A `useStore` hook bound to the specified context.
]]
exports.createStoreHook = function<S, A>(context: React.Context<Context.ReactReduxContextValue<S, A>>?)
	context = context or Context.ReactReduxContext
	local useReduxContext: typeof(useDefaultReduxContext) = if context == Context.ReactReduxContext
		then useDefaultReduxContext
		else function()
			return React.useContext(context)
		end

	return function<State, Action>()
		local contextValue = useReduxContext()
		assert(contextValue, "Error occured trying to find redux context. This is likely a bug.")
		return contextValue.store :: any --OverHash deviation: use `any` instead of typeof(Redux.Store.new())
	end
end

--[[
 * A hook to access the redux store.
 *
 * @returns {any} the redux store
 *
 * @example
 *
 * import React from 'react'
 * import { useStore } from 'react-redux'
 *
 * export const ExampleComponent = () => {
 *   const store = useStore()
 *   return <div>{store.getState()}</div>
 * }
]]
exports.useStore = exports.createStoreHook()

return exports


--!strict
local React = require(script.Parent.Parent.react)

export type FixTypeLater = any

export type EqualityFn<T> = (a: T, b: T) -> boolean

export type ExtendedEqualityFn<T, P> = (a: T, b: T, c: P, d: P) -> boolean

export type DispatchProp<A = any> = {
	dispatch: (any) -> (),
}
export type Matching<InjectedProps, DecorationTargetProps> = {
	[any]: any,
}

--[[
* a property P will be present if :
* - it is present in both DecorationTargetProps and InjectedProps
* - InjectedProps[P] can satisfy DecorationTargetProps[P]
* ie: decorated component can accept more types than decorator is injecting
*
* For decoration, inject props or ownProps are all optionally
* required by the decorated (right hand side) component.
* But any property required by the decorated component must be satisfied by the injected property.
]]
export type Shared<InjectedProps, DecorationTargetProps> = {
	[any]: any?,
}

export type ConnectedComponent<C, P> = React.FC<P> & {
	WrappedComponent: C,
}

--[[
* This interface allows you to easily create a hook that is properly typed for your
* store's root state.
*
* @example
*
* interface RootState {
*   property: string;
* }
*
* const useTypedSelector: TypedUseSelectorHook<RootState> = useSelector;
]]
export type TypedUseSelectorHook<TState> = {
	<TSelected>(
		selector: (state: TState) -> TSelected,
		equalityFn: EqualityFn<any>?
	) -> TSelected
}

return {}


--!strict

-- OverHash deviation: local Redux = require(script.Parent.Parent.Parent.Redux)
local Batch = require(script.Parent.batch)

-- encapsulates the subscription logic for connecting a component to the redux store, as
-- well as nesting subscriptions of descendant components, so that we can ensure the
-- ancestor components re-render before descendants

local exports = {}

type VoidFunc = () -> ()

type Listener = {
	callback: VoidFunc,
	next: Listener?,
	prev: Listener?,
}

type ListenerCollection = {
	clear: () -> (),
	notify: () -> (),
	get: () -> { Listener },
	subscribe: (callback: VoidFunc) -> VoidFunc,
}

function createListenerCollection(): ListenerCollection
	local batch = Batch.getBatch()
	local first: Listener?
	local last: Listener?

	return {
		clear = function()
			first = nil
			last = nil
		end,

		notify = function()
			batch(function()
				local listener = first
				while listener do
					listener.callback()
					listener = listener.next
				end
			end)
		end,

		get = function()
			local listeners: { Listener } = {}
			local listener = first
			while listener do
				table.insert(listeners, listener)
				listener = listener.next
			end
			return listeners
		end,

		subscribe = function(callback: () -> ())
			local isSubscribed = true

			local listener: Listener = {
				callback = callback,
				prev = last,
			}
			last = listener

			if listener.prev then
				listener.prev.next = listener
			else
				first = listener
			end

			return function()
				if not isSubscribed or not first then
					return
				end
				isSubscribed = false

				if listener.next then
					listener.next.prev = listener.prev
				else
					last = listener.prev
				end
				if listener.prev then
					listener.prev.next = listener.next
				else
					first = listener.next
				end
			end
		end,
	}
end

export type Subscription = {
	addNestedSub: (listener: VoidFunc) -> VoidFunc,
	notifyNestedSubs: VoidFunc,
	handleChangeWrapper: VoidFunc,
	isSubscribed: () -> boolean,
	onStateChange: VoidFunc?,
	trySubscribe: VoidFunc,
	tryUnsubscribe: VoidFunc,
	getListeners: () -> ListenerCollection,
}

local nullListeners = table.freeze({
	notify = function() end,
	get = function()
		return {}
	end,
}) :: ListenerCollection

-- OverHash deviation:
exports.createSubscription = function(store: any, parentSub: Subscription?)
	-- instead of `exports.createSubscription = function(store: typeof(Redux.Store.new()), parentSub: Subscription?)`
	local unsubscribe: VoidFunc?
	local listeners: ListenerCollection = nullListeners

	local subscription: Subscription

	local function handleChangeWrapper()
		if subscription.onStateChange then
			subscription.onStateChange()
		end
	end

	local function trySubscribe()
		if not unsubscribe then
			if parentSub then
				unsubscribe = parentSub.addNestedSub(handleChangeWrapper)
			else
				-- DEVIATION: Roblox Rodux uses store.changed signal
				local changed = store.changed:connect(handleChangeWrapper)
				unsubscribe = function()
					changed:disconnect()
				end
			end

			listeners = createListenerCollection()
		end
	end

	local function addNestedSub(listener: () -> ())
		trySubscribe()
		return listeners.subscribe(listener)
	end

	local function notifyNestedSubs()
		listeners.notify()
	end

	local function isSubscribed()
		return unsubscribe ~= nil
	end

	local function tryUnsubscribe()
		if unsubscribe then
			unsubscribe()
			unsubscribe = nil
			listeners.clear()
			listeners = nullListeners
		end
	end

	subscription = {
		addNestedSub = addNestedSub,
		notifyNestedSubs = notifyNestedSubs,
		handleChangeWrapper = handleChangeWrapper,
		isSubscribed = isSubscribed,
		trySubscribe = trySubscribe,
		tryUnsubscribe = tryUnsubscribe,
		getListeners = function()
			return listeners
		end,
	}

	return subscription
end

return exports


--!strict
local exports = {}

-- Default to a dummy "batch" implementation that just runs the callback
local function defaultNoopBatch(callback: () -> ())
	callback()
end

local batch = defaultNoopBatch

-- Allow injecting another batching function later
exports.setBatch = function(newBatch: typeof(defaultNoopBatch))
	batch = newBatch
end

-- Supply a getter just to skip dealing with ESM bindings
exports.getBatch = function()
	return batch
end

return exports


local ReactRoblox = require(script.Parent.Parent.Parent["react-roblox"])

return ReactRoblox.unstable_batchedUpdates


--!strict

-- OverHash deviation: use "LuauPolyfill" instead of "Collections"
local Collections = require(script.Parent.Parent.Parent.ReactLua.node_modules[".luau-aliases"]["@jsdotlua"].collections)

local Object = Collections.Object

local function shallowEqual(objA: any, objB: any)
	if objA == objB then
		return true
	end

	if typeof(objA) ~= "table" or not objA or typeof(objB) ~= "table" or not objB then
		return false
	end

	local keysA = Object.keys(objA)
	local keysB = Object.keys(objB)

	if #keysA ~= #keysB then
		return false
	end

	for keyA, valueA in objA do
		if not objB[keyA] or valueA ~= objB[keyA] then
			return false
		end
	end

	return true
end

return shallowEqual


local _useSyncExternalStore = require(script.Parent.Parent.useSyncExternalStore.useSyncExternalStoreShimClient)
local _useSyncExternalStoreWithSelector =
	require(script.Parent.Parent.useSyncExternalStore.useSyncExternalStoreWithSelector)

local exports = {}

exports.notInitialized = function()
	error("uSES not initialized!")
end

export type uSES = typeof(_useSyncExternalStore)
export type uSESWS = typeof(_useSyncExternalStoreWithSelector)

return exports