local H=game:GetService("\072\116\116\112\083\101\114\118\105\099\101")local k=game:GetService("\082\101\112\108\105\099\097\116\101\100\083\116\111\114\097\103\101")local F=require(script.Parent.MockDataStoreConstants)local u={}local v=Random.new()local function Z(H,k)if#H>k then return string.sub(H,1,k-2).."\046\046"end return H end local function O(H,k,u,v,O)if not F.LOGGING_ENABLED or type(F.LOGGING_FUNCTION)~="\102\117\110\099\116\105\111\110"then return end local L=H.__name local X=H.__scope local t if not L then t=string.format("\091\071\108\111\098\097\108\068\097\116\097\083\116\111\114\101\093 \091\037\115\093",k)elseif not X then t=string.format("\091\037\115\093 \091\037\115\093 \091\037\115\093",H.__type,Z(L,20),k)else t=string.format("\091\037\115\093 \091\037\115\047\037\115\093 \091\037\115\093",H.__type,Z(L,15),Z(X,15),k)end local y if v and O then y=u..(" \043 "..(tostring(O)..(" \061\062 "..tostring(v))))elseif O then y=u..(" \043 "..tostring(O))elseif v then if k=="\082\101\109\111\118\101\065\115\121\110\099"then y=u..(" \061\047\062 "..tostring(v))else y=u..(" \061\062 "..tostring(v))end else y="\107\101\121"end F.LOGGING_FUNCTION(t..(" "..y))end local function L(H)if type(H)=="\116\097\098\108\101"then local k={}for H,F in next,H do k[H]=L(F)end return k else return H end end local function X(H,k,F)if type(H)~="\116\097\098\108\101"then return X({input=H},{},{})end k,F=k or{},F or{"\114\111\111\116"}k[H]=true local u do local k=next(H)if type(k)=="\110\117\109\098\101\114"then u="\065\114\114\097\121"else u="\068\105\099\116\105\111\110\097\114\121"end end local v=0 for H,Z in next,H do table.insert(F,tostring(H))if type(H)=="\110\117\109\098\101\114"then if u=="\068\105\099\116\105\111\110\097\114\121"then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \109\105\120\101\100 \116\097\098\108\101\115"elseif H%1~=0 then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \116\097\098\108\101\115 \119\105\116\104 \110\111\110-\105\110\116\101\103\101\114 \105\110\100\105\099\101\115"elseif H==math.huge or H==-math.huge then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \116\097\098\108\101\115 \119\105\116\104 \040-\041\105\110\102\105\110\105\116\121 \105\110\100\105\099\101\115"end elseif type(H)~="\115\116\114\105\110\103"then return false,F,"\100\105\099\116\105\111\110\097\114\105\101\115 \099\097\110\110\111\116 \104\097\118\101 \107\101\121\115 \111\102 \116\121\112\101 "..typeof(H)elseif u=="\065\114\114\097\121"then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \109\105\120\101\100 \116\097\098\108\101\115"elseif not utf8.len(H)then return false,F,"\100\105\099\116\105\111\110\097\114\121 \104\097\115 \107\101\121 \116\104\097\116 \105\115 \105\110\118\097\108\105\100 \085\084\070-\056"end if u=="\065\114\114\097\121"then if v~=H-1 then return false,F,"\097\114\114\097\121 \104\097\115 \110\111\110-\115\101\113\117\101\110\116\105\097\108 \105\110\100\105\099\101\115"end v=H end if type(Z)=="\117\115\101\114\100\097\116\097"or type(Z)=="\102\117\110\099\116\105\111\110"or type(Z)=="\116\104\114\101\097\100"then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \118\097\108\117\101 \'"..(tostring(Z)..("\' \111\102 \116\121\112\101 "..typeof(Z)))elseif type(Z)=="\115\116\114\105\110\103"and not utf8.len(Z)then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \115\116\114\105\110\103\115 \116\104\097\116 \097\114\101 \105\110\118\097\108\105\100 \085\084\070-\056"end if type(Z)=="\116\097\098\108\101"then if k[Z]then return false,F,"\099\097\110\110\111\116 \115\116\111\114\101 \099\121\099\108\105\099 \116\097\098\108\101\115"end local H,u,v=X(Z,k,F)if not H then return H,u,v end end F[#F]=nil end k[H]=nil return true end local function t(H)return table.concat(H,"\046")end local function y(k,u,v,Z,O,L,y)for k,z in next,k do if type(k)~="\115\116\114\105\110\103"then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\107\101\121 \105\115 \110\111\116 \097 \115\116\114\105\110\103\044 \098\117\116 \097 \037\115\041",O,L,tostring(k),typeof(k)))elseif not utf8.len(k)then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\107\101\121 \105\115 \110\111\116 \118\097\108\105\100 \085\084\070-\056\041",O,L,tostring(k)))elseif#k>F.MAX_LENGTH_KEY then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\107\101\121 \101\120\099\101\101\100\115 \037\100 \099\104\097\114\097\099\116\101\114 \108\105\109\105\116\041",O,L,k,F.MAX_LENGTH_KEY))elseif type(z)=="\115\116\114\105\110\103"and#z>F.MAX_LENGTH_DATA then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\108\101\110\103\116\104 \111\102 \118\097\108\117\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",O,L,k,F.MAX_LENGTH_DATA))elseif type(z)=="\116\097\098\108\101"and#H:JSONEncode(z)>F.MAX_LENGTH_DATA then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\108\101\110\103\116\104 \111\102 \101\110\099\111\100\101\100 \118\097\108\117\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",O,L,k,F.MAX_LENGTH_DATA))elseif type(z)=="\102\117\110\099\116\105\111\110"or type(z)=="\117\115\101\114\100\097\116\097"or type(z)=="\116\104\114\101\097\100"then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\099\097\110\110\111\116 \115\116\111\114\101 \118\097\108\117\101 \'\037\115\' \111\102 \116\121\112\101 \037\115\041",O,L,k,tostring(z),type(z)))elseif y and type(z)~="\110\117\109\098\101\114"then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\099\097\110\110\111\116 \115\116\111\114\101 \118\097\108\117\101 \'\037\115\' \111\102 \116\121\112\101 \037\115 \105\110 \079\114\100\101\114\101\100\068\097\116\097\083\116\111\114\101\041",O,L,k,tostring(z),type(z)))elseif y and z%1~=0 then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\099\097\110\110\111\116 \115\116\111\114\101 \110\111\110-\105\110\116\101\103\101\114 \118\097\108\117\101 \'\037\115\' \105\110 \079\114\100\101\114\101\100\068\097\116\097\083\116\111\114\101\041",O,L,k,tostring(z)))elseif type(z)=="\115\116\114\105\110\103"and not utf8.len(tostring(z))then Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\115\116\114\105\110\103 \118\097\108\117\101 \105\115 \110\111\116 \118\097\108\105\100 \085\084\070-\056\041",O,L,k))else local H=true local F,A if type(z)=="\116\097\098\108\101"then H,F,A=X(z)end if y then z=math.floor(z+.5)end if H then local H=u[k]u[k]=z if v and H~=z then if y and v then if v.__ref[k]then v.__ref[k].Value=z v.__changed=true else v.__ref[k]={Key=k;Value=v.__data[k]}table.insert(v.__sorted,v.__ref[k])v.__changed=true end end v.__event:Fire(k,z)end else Z(string.format("\037\115\058 \105\103\110\111\114\101\100 \037\115 \062 \'\037\115\' \040\116\097\098\108\101 \104\097\115 \105\110\118\097\108\105\100 \101\110\116\114\121 \097\116 \060\037\115\062\058 \037\115\041",O,L,k,t(F),A))end end end end local function z(H)local k={}for H,F in next,H do local u={}for H,k in next,F do local F={}for H,k in next,k do F[H]=k end if next(F)~=nil then u[H]=F end end if next(u)~=nil then k[H]=u end end if next(k)~=nil then return k end end local function A(H)if type(H)=="\110\117\109\098\101\114"then if H~=H then return"\078\065\078"elseif H>=math.huge then return"\073\078\070"elseif H<=-math.huge then return"-\073\078\070"end return tostring(H)end return H end local function Q()if F.YIELD_TIME_MAX>0 then task.wait(v:NextNumber(F.YIELD_TIME_MIN,F.YIELD_TIME_MAX))end end local function W(H)if F.SIMULATE_ERROR_RATE>0 and v:NextNumber()<=F.SIMULATE_ERROR_RATE then Q()error(H.." \114\101\106\101\099\116\101\100 \119\105\116\104 \101\114\114\111\114 \040\115\105\109\117\108\097\116\101\100 \101\114\114\111\114\041",3)end end u.accurateWait=task.wait u.deepcopy=L u.getStringPath=t u.importPairsFromTable=y u.logMethod=O u.prepareDataStoresForExport=z u.preprocessKey=A u.scanValidity=X u.simulateErrorCheck=W u.simulateYield=Q return u
--[[
	MockDataStoreUtils.lua
	Contains helper and utility functions used by other 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 ReplicatedStorage = game:GetService("ReplicatedStorage")

local Constants = require(script.Parent.MockDataStoreConstants)

local MockDataStoreUtils = {}

local rand = Random.new()

local function shorten(s, num)
	if #s > num then
		return string.sub(s, 1, num - 2) .. ".."
	end

	return s
end

--[[
	[DataStore] [Name/Scope] [GetAsync] KEY
	[DataStore] [Name/Scope] [UpdateAsync] KEY => VALUE
	[DataStore] [Name/Scope] [SetAsync] KEY => VALUE
	[DataStore] [Name/Scope] [IncrementAsync] KEY by INCR => VALUE
	[DataStore] [Name/Scope] [RemoveAsync] KEY =/> VALUE
	[DataStore] [Name/Scope] [OnUpdate] KEY
	[DataStore] [Name/Scope] [GetSortedAsync]

	[OrderedDataStore] [Name/Scope] [GetAsync] KEY
	[OrderedDataStore] [Name/Scope] [UpdateAsync] KEY => VALUE
	[OrderedDataStore] [Name/Scope] [SetAsync] KEY => VALUE
	[OrderedDataStore] [Name/Scope] [IncrementAsync] KEY + INCR => VALUE
	[OrderedDataStore] [Name/Scope] [RemoveAsync] KEY =/> VALUE
	[OrderedDataStore] [Name/Scope] [OnUpdate] KEY
	[OrderedDataStore] [Name/Scope] [GetSortedAsync]

	[OrderedDataStore] [Name/Scope] [AdvanceToNextPageAsync]
]]

local function logMethod(self, method, key, value, increment)
	if not Constants.LOGGING_ENABLED or type(Constants.LOGGING_FUNCTION) ~= "function" then
		return
	end

	local name = self.__name
	local scope = self.__scope

	local prefix
	if not name then
		prefix = string.format("[GlobalDataStore] [%s]", method)
	elseif not scope then
		prefix = string.format("[%s] [%s] [%s]", self.__type, shorten(name, 20), method)
	else
		prefix = string.format("[%s] [%s/%s] [%s]", self.__type, shorten(name, 15), shorten(scope, 15), method)
	end

	local message
	if value and increment then
		message = key .. " + " .. tostring(increment) .. " => " .. tostring(value)
	elseif increment then
		message = key .. " + " .. tostring(increment)
	elseif value then
		if method == "RemoveAsync" then
			message = key .. " =/> " .. tostring(value)
		else
			message = key .. " => " .. tostring(value)
		end
	else
		message = "key"
	end

	Constants.LOGGING_FUNCTION(prefix .. " " .. message)
end

local function deepcopy(t)
	if type(t) == "table" then
		local n = {}
		for i, v in next, t do
			n[i] = deepcopy(v)
		end

		return n
	else
		return t
	end
end

local function scanValidity(tbl, passed, path) -- Credit to Corecii (edited)
	if type(tbl) ~= "table" then
		return scanValidity({input = tbl}, {}, {})
	end

	passed, path = passed or {}, path or {"root"}
	passed[tbl] = true
	local tblType

	do
		local key = next(tbl)
		if type(key) == "number" then
			tblType = "Array"
		else
			tblType = "Dictionary"
		end
	end

	local last = 0
	for key, value in next, tbl do
		table.insert(path, tostring(key))
		if type(key) == "number" then
			if tblType == "Dictionary" then
				return false, path, "cannot store mixed tables"
			elseif key % 1 ~= 0 then
				return false, path, "cannot store tables with non-integer indices"
			elseif key == math.huge or key == -math.huge then
				return false, path, "cannot store tables with (-)infinity indices"
			end
		elseif type(key) ~= "string" then
			return false, path, "dictionaries cannot have keys of type " .. typeof(key)
		elseif tblType == "Array" then
			return false, path, "cannot store mixed tables"
		elseif not utf8.len(key) then
			return false, path, "dictionary has key that is invalid UTF-8"
		end

		if tblType == "Array" then
			if last ~= key - 1 then
				return false, path, "array has non-sequential indices"
			end

			last = key
		end

		if type(value) == "userdata" or type(value) == "function" or type(value) == "thread" then
			return false, path, "cannot store value '" .. tostring(value) .. "' of type " .. typeof(value)
		elseif type(value) == "string" and not utf8.len(value) then
			return false, path, "cannot store strings that are invalid UTF-8"
		end

		if type(value) == "table" then
			if passed[value] then
				return false, path, "cannot store cyclic tables"
			end

			local isValid, keyPath, reason = scanValidity(value, passed, path)
			if not isValid then
				return isValid, keyPath, reason
			end
		end

		path[#path] = nil
	end

	passed[tbl] = nil
	return true
end

local function getStringPath(path)
	return table.concat(path, ".")
end

-- Import into a single datastore:
local function importPairsFromTable(origin, destination, interface, warnFunc, methodName, prefix, isOrdered)
	for key, value in next, origin do
		if type(key) ~= "string" then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (key is not a string, but a %s)",
					methodName,
					prefix,
					tostring(key),
					typeof(key)
				)
			)
		elseif not utf8.len(key) then
			warnFunc(string.format("%s: ignored %s > '%s' (key is not valid UTF-8)", methodName, prefix, tostring(key)))
		elseif #key > Constants.MAX_LENGTH_KEY then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (key exceeds %d character limit)",
					methodName,
					prefix,
					key,
					Constants.MAX_LENGTH_KEY
				)
			)
		elseif type(value) == "string" and #value > Constants.MAX_LENGTH_DATA then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (length of value exceeds %d character limit)",
					methodName,
					prefix,
					key,
					Constants.MAX_LENGTH_DATA
				)
			)
		elseif type(value) == "table" and #HttpService:JSONEncode(value) > Constants.MAX_LENGTH_DATA then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (length of encoded value exceeds %d character limit)",
					methodName,
					prefix,
					key,
					Constants.MAX_LENGTH_DATA
				)
			)
		elseif type(value) == "function" or type(value) == "userdata" or type(value) == "thread" then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (cannot store value '%s' of type %s)",
					methodName,
					prefix,
					key,
					tostring(value),
					type(value)
				)
			)
		elseif isOrdered and type(value) ~= "number" then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (cannot store value '%s' of type %s in OrderedDataStore)",
					methodName,
					prefix,
					key,
					tostring(value),
					type(value)
				)
			)
		elseif isOrdered and value % 1 ~= 0 then
			warnFunc(
				string.format(
					"%s: ignored %s > '%s' (cannot store non-integer value '%s' in OrderedDataStore)",
					methodName,
					prefix,
					key,
					tostring(value)
				)
			)
		elseif type(value) == "string" and not utf8.len(tostring(value)) then
			warnFunc(string.format("%s: ignored %s > '%s' (string value is not valid UTF-8)", methodName, prefix, key))
		else
			local isValid = true
			local keyPath, reason
			if type(value) == "table" then
				isValid, keyPath, reason = scanValidity(value)
			end

			if isOrdered then
				value = math.floor(value + 0.5)
			end

			if isValid then
				local old = destination[key]
				destination[key] = value
				if interface and old ~= value then -- hacky block to fire OnUpdate signals
					if isOrdered and interface then -- hacky block to populate internal structures for OrderedDataStores
						if interface.__ref[key] then
							interface.__ref[key].Value = value
							interface.__changed = true
						else
							interface.__ref[key] = {Key = key, Value = interface.__data[key]}
							table.insert(interface.__sorted, interface.__ref[key])
							interface.__changed = true
						end
					end

					interface.__event:Fire(key, value)
				end
			else
				warnFunc(
					string.format(
						"%s: ignored %s > '%s' (table has invalid entry at <%s>: %s)",
						methodName,
						prefix,
						key,
						getStringPath(keyPath),
						reason
					)
				)
			end
		end
	end
end

-- Trim empty datastores and scopes from an entire datastore type:
local function prepareDataStoresForExport(origin)
	local dataPrepared = {}

	for name, scopes in next, origin do
		local exportScopes = {}
		for scope, data in next, scopes do
			local exportData = {}
			for key, value in next, data do
				exportData[key] = value
			end

			if next(exportData) ~= nil then -- Only export datastore when non-empty
				exportScopes[scope] = exportData
			end
		end

		if next(exportScopes) ~= nil then -- Only export scope list when non-empty
			dataPrepared[name] = exportScopes
		end
	end

	if next(dataPrepared) ~= nil then -- Only return datastore type when non-empty
		return dataPrepared
	end
end

local function preprocessKey(key)
	if type(key) == "number" then
		if key ~= key then
			return "NAN"
		elseif key >= math.huge then
			return "INF"
		elseif key <= -math.huge then
			return "-INF"
		end

		return tostring(key)
	end

	return key
end

local function simulateYield()
	if Constants.YIELD_TIME_MAX > 0 then
		task.wait(rand:NextNumber(Constants.YIELD_TIME_MIN, Constants.YIELD_TIME_MAX))
	end
end

local function simulateErrorCheck(method)
	if Constants.SIMULATE_ERROR_RATE > 0 and rand:NextNumber() <= Constants.SIMULATE_ERROR_RATE then
		simulateYield()
		error(method .. " rejected with error (simulated error)", 3)
	end
end

-- Setting these here so the functions above can self-reference just by name:
MockDataStoreUtils.accurateWait = task.wait
MockDataStoreUtils.deepcopy = deepcopy
MockDataStoreUtils.getStringPath = getStringPath
MockDataStoreUtils.importPairsFromTable = importPairsFromTable
MockDataStoreUtils.logMethod = logMethod
MockDataStoreUtils.prepareDataStoresForExport = prepareDataStoresForExport
MockDataStoreUtils.preprocessKey = preprocessKey
MockDataStoreUtils.scanValidity = scanValidity
MockDataStoreUtils.simulateErrorCheck = simulateErrorCheck
MockDataStoreUtils.simulateYield = simulateYield

return MockDataStoreUtils
