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

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

local MockDataStoreUtils = {}

local Constants = require(script.Parent.MockDataStoreConstants)
local HttpService = game:GetService("HttpService") -- for json encode/decode
local RunService = game:GetService("RunService")

local rand = Random.new()

local function shorten(s, num)
	if #s > num then
		return s:sub(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 = ("[GlobalDataStore] [%s]"):format(method)
	elseif not scope then
		prefix = ("[%s] [%s] [%s]"):format(self.__type, shorten(name, 20), method)
	else
		prefix = ("[%s] [%s/%s] [%s]"):format(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 pairs(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
		path[#path + 1] = 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 pairs(origin) do
		if type(key) ~= "string" then
			warnFunc(("%s: ignored %s > '%s' (key is not a string, but a %s)")
				:format(methodName, prefix, tostring(key), typeof(key)))
		elseif not utf8.len(key) then
			warnFunc(("%s: ignored %s > '%s' (key is not valid UTF-8)")
				:format(methodName, prefix, tostring(key)))
		elseif #key > Constants.MAX_LENGTH_KEY then
			warnFunc(("%s: ignored %s > '%s' (key exceeds %d character limit)")
				:format(methodName, prefix, key, Constants.MAX_LENGTH_KEY))
		elseif type(value) == "string" and #value > Constants.MAX_LENGTH_DATA then
			warnFunc(("%s: ignored %s > '%s' (length of value exceeds %d character limit)")
				:format(methodName, prefix, key, Constants.MAX_LENGTH_DATA))
		elseif type(value) == "table" and #HttpService:JSONEncode(value) > Constants.MAX_LENGTH_DATA then
			warnFunc(("%s: ignored %s > '%s' (length of encoded value exceeds %d character limit)")
				:format(methodName, prefix, key, Constants.MAX_LENGTH_DATA))
		elseif type(value) == "function" or type(value) == "userdata" or type(value) == "thread" then
			warnFunc(("%s: ignored %s > '%s' (cannot store value '%s' of type %s)")
				:format(methodName, prefix, key, tostring(value), type(value)))
		elseif isOrdered and type(value) ~= "number" then
			warnFunc(("%s: ignored %s > '%s' (cannot store value '%s' of type %s in OrderedDataStore)")
				:format(methodName, prefix, key, tostring(value), type(value)))
		elseif isOrdered and value % 1 ~= 0 then
			warnFunc(("%s: ignored %s > '%s' (cannot store non-integer value '%s' in OrderedDataStore)")
				:format(methodName, prefix, key, tostring(value)))
		elseif type(value) == "string" and not utf8.len(value) then
			warnFunc(("%s: ignored %s > '%s' (string value is not valid UTF-8)")
				:format(methodName, prefix, key, tostring(value), type(value)))
		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 + .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(("%s: ignored %s > '%s' (table has invalid entry at <%s>: %s)")
					:format(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 pairs(origin) do
		local exportScopes = {}
		for scope, data in pairs(scopes) do
			local exportData = table.clone(data)
			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.logMethod = logMethod
MockDataStoreUtils.deepcopy = deepcopy
MockDataStoreUtils.scanValidity = scanValidity
MockDataStoreUtils.getStringPath = getStringPath
MockDataStoreUtils.importPairsFromTable = importPairsFromTable
MockDataStoreUtils.prepareDataStoresForExport = prepareDataStoresForExport
MockDataStoreUtils.preprocessKey = preprocessKey
MockDataStoreUtils.simulateYield = simulateYield
MockDataStoreUtils.simulateErrorCheck = simulateErrorCheck

return MockDataStoreUtils
