local F=game:GetService("\080\108\097\121\101\114\115")local g=game:GetService("\082\117\110\083\101\114\118\105\099\101")local Y=require(script.Parent.Constants)local function q()local F={}local g={}for Y,q in Y.REQUEST_BUDGETS do F[Y]=q.INITIAL_BUDGET g[Y]={}end F[Enum.DataStoreRequestType.UpdateAsync]=math.min(F[Enum.DataStoreRequestType.GetAsync],F[Enum.DataStoreRequestType.SetIncrementAsync])return{accumulatedSeconds=0,budgets=F,queues=g;maxThrottleQueueSize=Y.MAX_THROTTLE_QUEUE_SIZE}end local k={}k.__index=k function k.new()local F=setmetatable(q(),k)F.manual=false g.PostSimulation:Connect(function(g)F:tick(g)end)return F end function k.manual()local F=setmetatable(q(),k)F.manual=true return F end function k.setMaxThrottleQueueSize(g,F)g.maxThrottleQueueSize=F end function k.hasBudget(g,F)for F,Y in F do if g.budgets[Y]<1 then return false end end return true end function k.consumeBudget(g,F)for F,Y in F do g.budgets[Y]=math.max(g.budgets[Y]-1,0)end g.budgets[Enum.DataStoreRequestType.UpdateAsync]=math.min(g.budgets[Enum.DataStoreRequestType.GetAsync],g.budgets[Enum.DataStoreRequestType.SetIncrementAsync])end function k.updateBudgets(g)local q=#F:GetPlayers()for F,k in Y.REQUEST_BUDGETS do local I=k.RATE+q*k.RATE_PER_PLAYER g.budgets[F]=math.min(g.budgets[F]+I*Y.BUDGET_UPDATE_INTERVAL,k.MAX_BUDGET_FACTOR*I)end g.budgets[Enum.DataStoreRequestType.UpdateAsync]=math.min(g.budgets[Enum.DataStoreRequestType.GetAsync],g.budgets[Enum.DataStoreRequestType.SetIncrementAsync])end function k.updateQueues(F)for g,Y in F.queues do local q={}for g,Y in Y do if F:hasBudget(Y.requestTypes)then F:consumeBudget(Y.requestTypes)coroutine.resume(Y.thread)else table.insert(q,Y)end end F.queues[g]=q end end function k.update(F)F:updateBudgets()F:updateQueues()end function k.tick(g,F)g.accumulatedSeconds+=F while g.accumulatedSeconds>=Y.BUDGET_UPDATE_INTERVAL do g:update()g.accumulatedSeconds-=Y.BUDGET_UPDATE_INTERVAL end end function k.yieldForBudget(g,F)local Y=F[1]if g:hasBudget(F)then g:consumeBudget(F)elseif#g.queues[Y]>=g.maxThrottleQueueSize then error("\082\101\113\117\101\115\116 \119\097\115 \116\104\114\111\116\116\108\101\100 \100\117\101 \116\111 \108\097\099\107 \111\102 \098\117\100\103\101\116 \098\117\116 \116\104\101 \116\104\114\111\116\116\108\101 \113\117\101\117\101 \119\097\115 \102\117\108\108")else warn("\082\101\113\117\101\115\116 \119\097\115 \116\104\114\111\116\116\108\101\100 \100\117\101 \116\111 \108\097\099\107 \111\102 \098\117\100\103\101\116")table.insert(g.queues[Y],{thread=coroutine.running();requestTypes=F})coroutine.yield()end end return k
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local Constants = require(script.Parent.Constants)

local function defaultBudget()
	local budgets = {}
	local queues = {}

	for requestType, options in Constants.REQUEST_BUDGETS do
		budgets[requestType] = options.INITIAL_BUDGET
		queues[requestType] = {}
	end

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

	return {
		accumulatedSeconds = 0,
		budgets = budgets,
		queues = queues,
		maxThrottleQueueSize = Constants.MAX_THROTTLE_QUEUE_SIZE,
	}
end

local Budget = {}
Budget.__index = Budget

function Budget.new()
	local self = setmetatable(defaultBudget(), Budget)

	self.manual = false

	RunService.PostSimulation:Connect(function(deltaSeconds)
		self:tick(deltaSeconds)
	end)

	return self
end

function Budget.manual()
	local self = setmetatable(defaultBudget(), Budget)

	self.manual = true

	return self
end

function Budget:setMaxThrottleQueueSize(size)
	self.maxThrottleQueueSize = size
end

function Budget:hasBudget(requestTypes)
	for _, requestType in requestTypes do
		if self.budgets[requestType] < 1 then
			return false
		end
	end

	return true
end

function Budget:consumeBudget(requestTypes)
	for _, requestType in requestTypes do
		self.budgets[requestType] = math.max(self.budgets[requestType] - 1, 0)
	end

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

function Budget:updateBudgets()
	local playerCount = #Players:GetPlayers()

	for requestType, options in Constants.REQUEST_BUDGETS do
		local rate = options.RATE + playerCount * options.RATE_PER_PLAYER

		self.budgets[requestType] = math.min(
			self.budgets[requestType] + rate * Constants.BUDGET_UPDATE_INTERVAL,
			options.MAX_BUDGET_FACTOR * rate
		)
	end

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

function Budget:updateQueues()
	for index, queue in self.queues do
		local newQueue = {}

		for _, request in queue do
			if self:hasBudget(request.requestTypes) then
				self:consumeBudget(request.requestTypes)
				coroutine.resume(request.thread)
			else
				table.insert(newQueue, request)
			end
		end

		self.queues[index] = newQueue
	end
end

function Budget:update()
	self:updateBudgets()
	self:updateQueues()
end

function Budget:tick(deltaSeconds)
	self.accumulatedSeconds += deltaSeconds

	while self.accumulatedSeconds >= Constants.BUDGET_UPDATE_INTERVAL do
		self:update()
		self.accumulatedSeconds -= Constants.BUDGET_UPDATE_INTERVAL
	end
end

function Budget:yieldForBudget(requestTypes)
	local mainRequestType = requestTypes[1]

	if self:hasBudget(requestTypes) then
		self:consumeBudget(requestTypes)
	elseif #self.queues[mainRequestType] >= self.maxThrottleQueueSize then
		error("Request was throttled due to lack of budget but the throttle queue was full")
	else
		warn("Request was throttled due to lack of budget")

		table.insert(self.queues[mainRequestType], {
			thread = coroutine.running(),
			requestTypes = requestTypes,
		})

		coroutine.yield()
	end
end

return Budget
