local H=game:GetService("\072\116\116\112\083\101\114\118\105\099\101")local k=game:GetService("\080\108\097\121\101\114\115")local F=game:GetService("\082\117\110\083\101\114\118\105\099\101")local u=require(script.Parent.MockDataStoreConstants)local v=require(script.Parent.MockDataStoreUtils)local Z={}local O={[Enum.DataStoreRequestType.GetAsync]=u.BUDGET_GETASYNC;[Enum.DataStoreRequestType.GetSortedAsync]=u.BUDGET_GETSORTEDASYNC,[Enum.DataStoreRequestType.OnUpdate]=u.BUDGET_ONUPDATE;[Enum.DataStoreRequestType.SetIncrementAsync]=u.BUDGET_SETINCREMENTASYNC,[Enum.DataStoreRequestType.SetIncrementSortedAsync]=u.BUDGET_SETINCREMENTSORTEDASYNC}local L={GlobalDataStore={},DataStore={},OrderedDataStore={}}local X={}local t={}local y={[Enum.DataStoreRequestType.GetAsync]={},[Enum.DataStoreRequestType.GetSortedAsync]={};[Enum.DataStoreRequestType.OnUpdate]={},[Enum.DataStoreRequestType.SetIncrementAsync]={},[Enum.DataStoreRequestType.SetIncrementSortedAsync]={}}local function z()for H,k in next,O do t[H]=k.START end t[Enum.DataStoreRequestType.UpdateAsync]=math.min(t[Enum.DataStoreRequestType.GetAsync],t[Enum.DataStoreRequestType.SetIncrementAsync])end local function A(H,k,F,v)if not u.BUDGETING_ENABLED then return end local Z=k.RATE+v*k.RATE_PLR t[H]=math.min(t[H]+F*Z,k.MAX_FACTOR*Z)end local function Q(H)if not u.BUDGETING_ENABLED then return end for H,k in next,H do if t[k]then t[k]=math.max(0,t[k]-1)end end t[Enum.DataStoreRequestType.UpdateAsync]=math.min(t[Enum.DataStoreRequestType.GetAsync],t[Enum.DataStoreRequestType.SetIncrementAsync])end local function W(H)if not u.BUDGETING_ENABLED then return true end for H,k in next,H do if t[k]and t[k]<1 then return false end end return true end local K=false if F:IsServer()then z();(coroutine.wrap(function()local H=os.clock()while v.accurateWait(u.BUDGET_UPDATE_INTERVAL)do local F=os.clock()local v=(F-H)/60 H=F local Z=#k:GetPlayers()if not K then for H,k in next,O do A(H,k,v,Z)end t[Enum.DataStoreRequestType.UpdateAsync]=math.min(t[Enum.DataStoreRequestType.GetAsync],t[Enum.DataStoreRequestType.SetIncrementAsync])end for H,k in next,y do for H=#k,1,-1 do local F=k[H]local v=F.Thread local Z=F.Budget local O=F.Key local L=F.Lock local X=F.Cache if not(L and(L[O]or os.clock()-(X[O]or 0)<u.WRITE_COOLDOWN))and W(Z)then table.remove(k,H)Q(Z)coroutine.resume(v)end end end end end))()game:BindToClose(function()for H,k in next,O do t[H]=math.max(t[H],u.BUDGET_ONCLOSE_BASE*(k.RATE/u.BUDGET_BASE))end t[Enum.DataStoreRequestType.UpdateAsync]=math.min(t[Enum.DataStoreRequestType.GetAsync],t[Enum.DataStoreRequestType.SetIncrementAsync])end)end function Z.GetGlobalData()return L.GlobalDataStore end function Z.GetData(H,k)assert(type(H)=="\115\116\114\105\110\103")assert(type(k)=="\115\116\114\105\110\103")if not L.DataStore[H]then L.DataStore[H]={}end if not L.DataStore[H][k]then L.DataStore[H][k]={}end return L.DataStore[H][k]end function Z.GetOrderedData(H,k)assert(type(H)=="\115\116\114\105\110\103")assert(type(k)=="\115\116\114\105\110\103")if not L.OrderedDataStore[H]then L.OrderedDataStore[H]={}end if not L.OrderedDataStore[H][k]then L.OrderedDataStore[H][k]={}end return L.OrderedDataStore[H][k]end function Z.GetDataInterface(H)return X[H]end function Z.SetDataInterface(H,k)assert(type(H)=="\116\097\098\108\101")assert(type(k)=="\116\097\098\108\101")X[H]=k end function Z.GetBudget(H)if u.BUDGETING_ENABLED then return math.floor(t[H]or 0)else return math.huge end end function Z.SetBudget(H,k)assert(type(k)=="\110\117\109\098\101\114")k=math.max(k,0)if H==Enum.DataStoreRequestType.UpdateAsync then t[Enum.DataStoreRequestType.SetIncrementAsync]=k t[Enum.DataStoreRequestType.GetAsync]=k end if t[H]then t[H]=k end end Z.ResetBudget=z function Z.FreezeBudgetUpdates()K=true end function Z.ThawBudgetUpdates()K=false end function Z.YieldForWriteLockAndBudget(H,k,F,v,Z)assert(type(H)=="\102\117\110\099\116\105\111\110")assert(type(k)=="\115\116\114\105\110\103")assert(type(F)=="\116\097\098\108\101")assert(type(v)=="\116\097\098\108\101")assert(#Z>0)local O=Z[1]if#y[O]>=u.THROTTLE_QUEUE_SIZE then return false end H()table.insert(y[O],1,{Key=k;Lock=F,Cache=v;Thread=coroutine.running();Budget=Z})coroutine.yield()return true end function Z.YieldForBudget(H,k)assert(type(H)=="\102\117\110\099\116\105\111\110")assert(#k>0)local F=k[1]if W(k)then Q(k)elseif#y[F]>=u.THROTTLE_QUEUE_SIZE then return false else H()table.insert(y[F],1,{After=0,Thread=coroutine.running();Budget=k})coroutine.yield()end return true end function Z.ExportToJSON()local k={}if next(L.GlobalDataStore)~=nil then k.GlobalDataStore=L.GlobalDataStore end k.DataStore=v.prepareDataStoresForExport(L.DataStore)k.OrderedDataStore=v.prepareDataStoresForExport(L.OrderedDataStore)return H:JSONEncode(k)end local function b(H,k,F,Z,O,L)for H,t in next,H do if type(H)~="\115\116\114\105\110\103"then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \105\115 \110\111\116 \097 \115\116\114\105\110\103\044 \098\117\116 \097 \037\115\041",Z,O,tostring(H),typeof(H)))elseif type(t)~="\116\097\098\108\101"then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\115\099\111\112\101 \108\105\115\116 \105\115 \110\111\116 \097 \116\097\098\108\101\044 \098\117\116 \097 \037\115\041",Z,O,H,typeof(t)))elseif#H==0 then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \105\115 \097\110 \101\109\112\116\121 \115\116\114\105\110\103\041",Z,O,H))elseif#H>u.MAX_LENGTH_NAME then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \040\110\097\109\101 \101\120\099\101\101\100\115 \037\100 \099\104\097\114\097\099\116\101\114 \108\105\109\105\116\041",Z,O,H,u.MAX_LENGTH_NAME))else for t,y in next,t do if type(t)~="\115\116\114\105\110\103"then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \105\115 \110\111\116 \097 \115\116\114\105\110\103\044 \098\117\116 \097 \037\115\041",Z,O,H,tostring(t),typeof(t)))elseif type(y)~="\116\097\098\108\101"then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\100\097\116\097 \108\105\115\116 \105\115 \110\111\116 \097 \116\097\098\108\101\044 \098\117\116 \097 \037\115\041",Z,O,H,t,typeof(y)))elseif#t==0 then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \105\115 \097\110 \101\109\112\116\121 \115\116\114\105\110\103\041",Z,O,H,t))elseif#t>u.MAX_LENGTH_SCOPE then F(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \037\113 \062 \037\113 \040\115\099\111\112\101 \101\120\099\101\101\100\115 \037\100 \099\104\097\114\097\099\116\101\114 \108\105\109\105\116\041",Z,O,H,t,u.MAX_LENGTH_SCOPE))else if not k[H]then k[H]={}end if not k[H][t]then k[H][t]={}end v.importPairsFromTable(y,k[H][t],X[k[H][t]],F,Z,string.format("\037\115 \062 \037\113 \062 \037\113",O,H,t),L)end end end end end function Z.ImportFromJSON(H,k)assert(type(H)=="\116\097\098\108\101")assert(k==nil or type(k)=="\098\111\111\108\101\097\110")local F=warn if k==false then F=function() end end if type(H.GlobalDataStore)=="\116\097\098\108\101"then v.importPairsFromTable(H.GlobalDataStore,L.GlobalDataStore,X[L.GlobalDataStore],F,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\071\108\111\098\097\108\068\097\116\097\083\116\111\114\101",false)end if type(H.DataStore)=="\116\097\098\108\101"then b(H.DataStore,L.DataStore,F,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\068\097\116\097\083\116\111\114\101",false)end if type(H.OrderedDataStore)=="\116\097\098\108\101"then b(H.OrderedDataStore,L.OrderedDataStore,F,"\073\109\112\111\114\116\070\114\111\109\074\083\079\078","\079\114\100\101\114\101\100\068\097\116\097\083\116\111\114\101",true)end end function Z.ResetData()for H,k in next,X do for H in next,k.__data do k.__data[H]=nil k.__event:Fire(H,nil)end k.__getCache={}k.__writeCache={}k.__writeLock={}if k.__sorted then k.__sorted={}k.__ref={}k.__changed=false end end table.clear(L.GlobalDataStore)for H,k in next,L.DataStore do for H,k in next,k do table.clear(k)end end for H,k in next,L.OrderedDataStore do for H,k in next,k do table.clear(k)end end end return Z
--[[
	MockDataStoreManager.lua
	This module does bookkeeping of data, interfaces and request limits used by MockDataStoreService and its sub-classes.

	This module is licensed under APLv2, refer to the LICENSE file or:
	https://github.com/buildthomas/MockDataStoreService/blob/master/LICENSE
]]

local HttpService = game:GetService("HttpService") -- for json encode/decode
local Players = game:GetService("Players") -- for restoring budgets
local RunService = game:GetService("RunService") -- for checking if running context is on server

local Constants = require(script.Parent.MockDataStoreConstants)
local Utils = require(script.Parent.MockDataStoreUtils)

local MockDataStoreManager = {}

local ConstantsMapping = {
	[Enum.DataStoreRequestType.GetAsync] = Constants.BUDGET_GETASYNC;
	[Enum.DataStoreRequestType.GetSortedAsync] = Constants.BUDGET_GETSORTEDASYNC;
	[Enum.DataStoreRequestType.OnUpdate] = Constants.BUDGET_ONUPDATE;
	[Enum.DataStoreRequestType.SetIncrementAsync] = Constants.BUDGET_SETINCREMENTASYNC;
	[Enum.DataStoreRequestType.SetIncrementSortedAsync] = Constants.BUDGET_SETINCREMENTSORTEDASYNC;
}

-- Bookkeeping of all data:
local Data = {
	GlobalDataStore = {};
	DataStore = {};
	OrderedDataStore = {};
}

-- Bookkeeping of all active GlobalDataStore/OrderedDataStore interfaces indexed by data table:
local Interfaces = {}

-- Request limit bookkeeping:
local Budgets = {}

local budgetRequestQueues = {
	[Enum.DataStoreRequestType.GetAsync] = {};
	[Enum.DataStoreRequestType.GetSortedAsync] = {};
	[Enum.DataStoreRequestType.OnUpdate] = {};
	[Enum.DataStoreRequestType.SetIncrementAsync] = {};
	[Enum.DataStoreRequestType.SetIncrementSortedAsync] = {};
}

local function initBudget()
	for requestType, const in next, ConstantsMapping do
		Budgets[requestType] = const.START
	end

	Budgets[Enum.DataStoreRequestType.UpdateAsync] =
		math.min(Budgets[Enum.DataStoreRequestType.GetAsync], Budgets[Enum.DataStoreRequestType.SetIncrementAsync])
end

local function updateBudget(req, const, dt, n)
	if not Constants.BUDGETING_ENABLED then
		return
	end

	local rate = const.RATE + n * const.RATE_PLR
	Budgets[req] = math.min(Budgets[req] + dt * rate, const.MAX_FACTOR * rate)
end

local function stealBudget(budget)
	if not Constants.BUDGETING_ENABLED then
		return
	end

	for _, requestType in next, budget do
		if Budgets[requestType] then
			Budgets[requestType] = math.max(0, Budgets[requestType] - 1)
		end
	end

	Budgets[Enum.DataStoreRequestType.UpdateAsync] =
		math.min(Budgets[Enum.DataStoreRequestType.GetAsync], Budgets[Enum.DataStoreRequestType.SetIncrementAsync])
end

local function checkBudget(budget)
	if not Constants.BUDGETING_ENABLED then
		return true
	end

	for _, requestType in next, budget do
		if Budgets[requestType] and Budgets[requestType] < 1 then
			return false
		end
	end

	return true
end

local isFrozen = false

if RunService:IsServer() then
	-- Only do budget/throttle updating on server (in case package required on client)
	initBudget()

	coroutine.wrap(function() -- Thread that increases budgets and de-throttles requests periodically
		local lastCheck = os.clock()
		while Utils.accurateWait(Constants.BUDGET_UPDATE_INTERVAL) do
			local now = os.clock()
			local dt = (now - lastCheck) / 60
			lastCheck = now
			local n = #Players:GetPlayers()

			if not isFrozen then
				for requestType, const in next, ConstantsMapping do
					updateBudget(requestType, const, dt, n)
				end

				Budgets[Enum.DataStoreRequestType.UpdateAsync] = math.min(
					Budgets[Enum.DataStoreRequestType.GetAsync],
					Budgets[Enum.DataStoreRequestType.SetIncrementAsync]
				)
			end

			for _, budgetRequestQueue in next, budgetRequestQueues do
				for i = #budgetRequestQueue, 1, -1 do
					local request = budgetRequestQueue[i]

					local thread = request.Thread
					local budget = request.Budget
					local key = request.Key
					local lock = request.Lock
					local cache = request.Cache

					if
						not (lock and (lock[key] or os.clock() - (cache[key] or 0) < Constants.WRITE_COOLDOWN))
						and checkBudget(budget)
					then
						table.remove(budgetRequestQueue, i)
						stealBudget(budget)
						coroutine.resume(thread)
					end
				end
			end
		end
	end)()

	game:BindToClose(function()
		for requestType, const in next, ConstantsMapping do
			Budgets[requestType] =
				math.max(Budgets[requestType], Constants.BUDGET_ONCLOSE_BASE * (const.RATE / Constants.BUDGET_BASE))
		end

		Budgets[Enum.DataStoreRequestType.UpdateAsync] =
			math.min(Budgets[Enum.DataStoreRequestType.GetAsync], Budgets[Enum.DataStoreRequestType.SetIncrementAsync])
	end)
end

function MockDataStoreManager.GetGlobalData()
	return Data.GlobalDataStore
end

function MockDataStoreManager.GetData(name, scope)
	assert(type(name) == "string")
	assert(type(scope) == "string")

	if not Data.DataStore[name] then
		Data.DataStore[name] = {}
	end

	if not Data.DataStore[name][scope] then
		Data.DataStore[name][scope] = {}
	end

	return Data.DataStore[name][scope]
end

function MockDataStoreManager.GetOrderedData(name, scope)
	assert(type(name) == "string")
	assert(type(scope) == "string")

	if not Data.OrderedDataStore[name] then
		Data.OrderedDataStore[name] = {}
	end

	if not Data.OrderedDataStore[name][scope] then
		Data.OrderedDataStore[name][scope] = {}
	end

	return Data.OrderedDataStore[name][scope]
end

function MockDataStoreManager.GetDataInterface(data)
	return Interfaces[data]
end

function MockDataStoreManager.SetDataInterface(data, interface)
	assert(type(data) == "table")
	assert(type(interface) == "table")

	Interfaces[data] = interface
end

function MockDataStoreManager.GetBudget(requestType)
	if Constants.BUDGETING_ENABLED then
		return math.floor(Budgets[requestType] or 0)
	else
		return math.huge
	end
end

function MockDataStoreManager.SetBudget(requestType, budget)
	assert(type(budget) == "number")
	budget = math.max(budget, 0)

	if requestType == Enum.DataStoreRequestType.UpdateAsync then
		Budgets[Enum.DataStoreRequestType.SetIncrementAsync] = budget
		Budgets[Enum.DataStoreRequestType.GetAsync] = budget
	end

	if Budgets[requestType] then
		Budgets[requestType] = budget
	end
end

MockDataStoreManager.ResetBudget = initBudget
function MockDataStoreManager.FreezeBudgetUpdates()
	isFrozen = true
end

function MockDataStoreManager.ThawBudgetUpdates()
	isFrozen = false
end

function MockDataStoreManager.YieldForWriteLockAndBudget(callback, key, writeLock, writeCache, budget)
	assert(type(callback) == "function")
	assert(type(key) == "string")
	assert(type(writeLock) == "table")
	assert(type(writeCache) == "table")
	assert(#budget > 0)

	local mainRequestType = budget[1]
	if #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
		return false -- no room in throttle queue
	end

	callback() -- would i.e. trigger a warning in output
	table.insert(budgetRequestQueues[mainRequestType], 1, {
		Key = key;
		Lock = writeLock;
		Cache = writeCache;
		Thread = coroutine.running();
		Budget = budget;
	})

	coroutine.yield()
	return true
end

function MockDataStoreManager.YieldForBudget(callback, budget)
	assert(type(callback) == "function")
	assert(#budget > 0)

	local mainRequestType = budget[1]

	if checkBudget(budget) then
		stealBudget(budget)
	elseif #budgetRequestQueues[mainRequestType] >= Constants.THROTTLE_QUEUE_SIZE then
		return false -- no room in throttle queue
	else
		callback() -- would i.e. trigger a warning in output
		table.insert(budgetRequestQueues[mainRequestType], 1, {
			After = 0; -- no write lock
			Thread = coroutine.running();
			Budget = budget;
		})

		coroutine.yield()
	end

	return true
end

function MockDataStoreManager.ExportToJSON()
	local export = {}
	if next(Data.GlobalDataStore) ~= nil then -- GlobalDataStore not empty
		export.GlobalDataStore = Data.GlobalDataStore
	end

	export.DataStore = Utils.prepareDataStoresForExport(Data.DataStore) -- can be nil
	export.OrderedDataStore = Utils.prepareDataStoresForExport(Data.OrderedDataStore) -- can be nil

	return HttpService:JSONEncode(export)
end

-- Import into an entire datastore type:
local function importDataStoresFromTable(origin, destination, warnFunc, methodName, prefix, isOrdered)
	for name, scopes in next, origin do
		if type(name) ~= "string" then
			warnFunc(
				string.format(
					"%s: ignored %s > %q (name is not a string, but a %s)",
					methodName,
					prefix,
					tostring(name),
					typeof(name)
				)
			)
		elseif type(scopes) ~= "table" then
			warnFunc(
				string.format(
					"%s: ignored %s > %q (scope list is not a table, but a %s)",
					methodName,
					prefix,
					name,
					typeof(scopes)
				)
			)
		elseif #name == 0 then
			warnFunc(string.format("%s: ignored %s > %q (name is an empty string)", methodName, prefix, name))
		elseif #name > Constants.MAX_LENGTH_NAME then
			warnFunc(
				string.format(
					"%s: ignored %s > %q (name exceeds %d character limit)",
					methodName,
					prefix,
					name,
					Constants.MAX_LENGTH_NAME
				)
			)
		else
			for scope, data in next, scopes do
				if type(scope) ~= "string" then
					warnFunc(
						string.format(
							"%s: ignored %s > %q > %q (scope is not a string, but a %s)",
							methodName,
							prefix,
							name,
							tostring(scope),
							typeof(scope)
						)
					)
				elseif type(data) ~= "table" then
					warnFunc(
						string.format(
							"%s: ignored %s > %q > %q (data list is not a table, but a %s)",
							methodName,
							prefix,
							name,
							scope,
							typeof(data)
						)
					)
				elseif #scope == 0 then
					warnFunc(
						string.format(
							"%s: ignored %s > %q > %q (scope is an empty string)",
							methodName,
							prefix,
							name,
							scope
						)
					)
				elseif #scope > Constants.MAX_LENGTH_SCOPE then
					warnFunc(
						string.format(
							"%s: ignored %s > %q > %q (scope exceeds %d character limit)",
							methodName,
							prefix,
							name,
							scope,
							Constants.MAX_LENGTH_SCOPE
						)
					)
				else
					if not destination[name] then
						destination[name] = {}
					end

					if not destination[name][scope] then
						destination[name][scope] = {}
					end

					Utils.importPairsFromTable(
						data,
						destination[name][scope],
						Interfaces[destination[name][scope]],
						warnFunc,
						methodName,
						string.format("%s > %q > %q", prefix, name, scope),
						isOrdered
					)
				end
			end
		end
	end
end

function MockDataStoreManager.ImportFromJSON(content, verbose)
	assert(type(content) == "table")
	assert(verbose == nil or type(verbose) == "boolean")

	local warnFunc = warn -- assume verbose as default
	if verbose == false then -- intentional formatting
		warnFunc = function() end
	end

	if type(content.GlobalDataStore) == "table" then
		Utils.importPairsFromTable(
			content.GlobalDataStore,
			Data.GlobalDataStore,
			Interfaces[Data.GlobalDataStore],
			warnFunc,
			"ImportFromJSON",
			"GlobalDataStore",
			false
		)
	end

	if type(content.DataStore) == "table" then
		importDataStoresFromTable(content.DataStore, Data.DataStore, warnFunc, "ImportFromJSON", "DataStore", false)
	end

	if type(content.OrderedDataStore) == "table" then
		importDataStoresFromTable(
			content.OrderedDataStore,
			Data.OrderedDataStore,
			warnFunc,
			"ImportFromJSON",
			"OrderedDataStore",
			true
		)
	end
end

function MockDataStoreManager.ResetData()
	for _, interface in next, Interfaces do
		for key in next, interface.__data do
			interface.__data[key] = nil
			interface.__event:Fire(key, nil)
		end

		interface.__getCache = {}
		interface.__writeCache = {}
		interface.__writeLock = {}
		if interface.__sorted then
			interface.__sorted = {}
			interface.__ref = {}
			interface.__changed = false
		end
	end

	table.clear(Data.GlobalDataStore)

	for _, scopes in next, Data.DataStore do
		for _, data in next, scopes do
			table.clear(data)
		end
	end

	for _, scopes in next, Data.OrderedDataStore do
		for _, data in next, scopes do
			table.clear(data)
		end
	end
end

return MockDataStoreManager
