-- Regular :WaitForChild had issues with order (remoteevents were going through before waitforchild resumed)

function WaitForChild(parent, name)
	local remote = parent:FindFirstChild(name)

	if not remote then
		local thread = coroutine.running()
		local con

		con = parent.ChildAdded:Connect(function(child)
			if child.Name == name then
				con:Disconnect()
				remote = child
				ResumeThread(thread)
			end
		end)

		YieldThread()
	end

	return remote
end

function GetEventHandler(name)
	local handler = EventHandlers[name]
	if handler then
		return handler
	end
	
	local handler = {
		Name = name,
		Folder = EventsFolder,
		
		Callbacks = {},
		IncomingQueueErrored = nil
	}
	
	EventHandlers[name] = handler
	
	if IsServer then
		local remote = Instance.new("RemoteEvent")
		remote.Name = handler.Name
		remote.Parent = handler.Folder

		handler.Remote = remote
	else
		FastSpawn(function()
			handler.Queue = {}

			local remote = WaitForChild(handler.Folder, handler.Name)
			handler.Remote = remote
			
			if #handler.Callbacks == 0 then
				handler.IncomingQueue = {}
			end
				
			remote.OnClientEvent:Connect(function(...)
				if handler.IncomingQueue then
					if #handler.IncomingQueue >= 2048 then
						if not handler.IncomingQueueErrored then
							handler.IncomingQueueErrored = true
							FastSpawn(error, string.format("Exhausted remote invocation queue for %s", remote:GetFullName()), -1)
							
							delay(1, function()
								handler.IncomingQueueErrored = nil
							end)
						end
						
						if #handler.IncomingQueue >= 8172 then
							table.remove(handler.IncomingQueue, 1)
						end
					end
					
					ReceiveCounter += 1
					table.insert(handler.IncomingQueue, table.pack(ReceiveCounter, handler, ...))
					return
				end
				
				SafeFireEvent(handler, ...)
			end)
			
			if not IsStudio then
				remote.Name = ""
			end
			
			for _,fn in pairs(handler.Queue) do
				fn()
			end
			
			handler.Queue = nil
		end)
	end
	
	return handler
end

function GetFunctionHandler(name)
	local handler = FunctionHandlers[name]
	if handler then
		return handler
	end
	
	local handler = {
		Name = name,
		Folder = FunctionsFolder,
		
		Callback = nil,
		IncomingQueueErrored = nil
	}
	
	FunctionHandlers[name] = handler
	
	if IsServer then
		local remote = Instance.new("RemoteFunction")
		remote.Name = handler.Name
		remote.Parent = handler.Folder
		
		handler.Remote = remote
	else
		FastSpawn(function()
			handler.Queue = {}
			
			local remote = WaitForChild(handler.Folder, handler.Name)
			handler.Remote = remote
			
			handler.IncomingQueue = {}
			handler.OnClientInvoke = function(...)
				if not handler.Callback then
					if #handler.IncomingQueue >= 2048 then
						if not handler.IncomingQueueErrored then
							handler.IncomingQueueErrored = true
							FastSpawn(error, string.format("Exhausted remote invocation queue for %s", remote:GetFullName()), -1)
							
							delay(1, function()
								handler.IncomingQueueErrored = nil
							end)
						end
						
						if #handler.IncomingQueue >= 8172 then
							table.remove(handler.IncomingQueue, 1)
						end
					end
				
					ReceiveCounter += 1
					local params = table.pack(ReceiveCounter, handler, coroutine.running())
					
					table.insert(handler.IncomingQueue, params)
					YieldThread()
				end
				
				return SafeInvokeCallback(handler, ...)
			end
			
			if not IsStudio then
				remote.Name = ""
			end
			
			for _,fn in pairs(handler.Queue) do
				fn()
			end
			
			handler.Queue = nil
		end)
	end
	
	return handler
end

function AddToQueue(handler, fn, doWarn)
	if handler.Remote then
		return fn()
	end
	
	handler.Queue[#handler.Queue + 1] = fn
	
	if doWarn then
		delay(5, function()
			if not handler.Remote then
				warn(debug.traceback(("Infinite yield possible on '%s:WaitForChild(\"%s\")'"):format(handler.Folder:GetFullName(), handler.Name)))
			end
		end)
	end
end

function ExecuteDeferredHandlers()
	local handlers = DeferredHandlers
	local queue = {}
	
	DeferredHandlers = {}
	
	for handler in pairs(handlers) do
		local incoming = handler.IncomingQueue
		handler.IncomingQueue = nil

		table.move(incoming, 1, #incoming, #queue + 1, queue)
	end
	
	table.sort(queue, function(a, b) return a[1] < b[1] end)
	
	for _,v in ipairs(queue) do
		local handler = v[2]
		
		if handler.Callbacks then
			SafeFireEvent(handler, unpack(v, 3))
		else
			ResumeThread(v[3])
		end
	end
end

local Middleware = {
	MatchParams = function(name, paramTypes)
		paramTypes = { unpack(paramTypes) }
		local paramStart = 1
		
		for i,v in pairs(paramTypes) do
			local list = type(v) == "string" and string.split(v, "|") or v
			
			local dict = {}
			local typeListString = ""
			
			for _,v in pairs(list) do
				local typeString = v:gsub("^%s+", ""):gsub("%s+$", "")
				
				typeListString ..= (#typeListString > 0 and " or " or "") .. typeString
				dict[typeString:lower()] = true
			end
			
			dict._string = typeListString
			paramTypes[i] = dict
		end
		
		if IsServer then
			paramStart = 2
			table.insert(paramTypes, 1, false)
		end
		
		local function MatchParams(fn, ...)
			local params = table.pack(...)
			
			if params.n > #paramTypes then
				if IsStudio then
					warn(("[Network] Invalid number of parameters to %s (%s expected, got %s)"):format(name, #paramTypes - paramStart + 1, params.n - paramStart + 1))
				end
				return
			end
			
			for i = paramStart, #paramTypes do
				local argType = typeof(params[i])
				local argExpected = paramTypes[i]
				
				if not argExpected[argType:lower()] and not argExpected.any then
					if IsStudio then
						warn(("[Network] Invalid parameter %d to %s (%s expected, got %s)"):format(i - paramStart + 1, name, argExpected._string, argType))
					end
					return
				end
			end
			
			return fn(...)
		end
		
		return MatchParams
	end
}

function combineFn(handler, final, ...)
	local middleware = { ... }
	
	if typeof(final) == "table" then
		local info = final
		final = final[1]
		
		if info.MatchParams then
			table.insert(middleware, Middleware.MatchParams(handler.Name, info.MatchParams))
		end
	end
	
	local function NetworkHandler(...)
		if LoggingNetwork then
			local client = ...
			table.insert(LoggingNetwork[client][handler.Remote].dataIn, GetParamString(select(2, ...)))
		end
		
		local currentIndex = 1
		
		local function runMiddleware(index, ...)
			if index ~= currentIndex then
				return
			end
			
			currentIndex += 1
			
			if index <= #middleware then
				return middleware[index](function(...) return runMiddleware(index + 1, ...) end, ...)
			end
			
			return final(...)
		end
		
		return runMiddleware(1, ...)
	end
	
	return NetworkHandler
end

function Network:BindEvents(pre, callbacks)
	if typeof(pre) == "table" then
		pre, callbacks = nil, pre
	end
	
	for name,fn in pairs(callbacks) do
		local handler = GetEventHandler(name)
		if not handler then
			error(("Tried to bind callback to non-existing RemoteEvent %q"):format(name))
		end
		
		handler.Callbacks[#handler.Callbacks + 1] = combineFn(handler, fn, pre)
		
		if IsServer then
			handler.Remote.OnServerEvent:Connect(function(...)
				SafeFireEvent(handler, ...)
			end)
		else
			if handler.IncomingQueue then
				DeferredHandlers[handler] = true
			end
		end
	end
	
	ExecuteDeferredHandlers()
end

function Network:BindFunctions(pre, callbacks)
	if typeof(pre) == "table" then
		pre, callbacks = nil, pre
	end
	
	for name,fn in pairs(callbacks) do
		local handler = GetFunctionHandler(name)
		if not handler then
			error(("Tried to bind callback to non-existing RemoteFunction %q"):format(name))
		end
		
		if handler.Callback then
			error(("Tried to bind multiple callbacks to the same RemoteFunction (%s)"):format(handler.Remote:GetFullName()))
		end
		
		handler.Callback = combineFn(handler, fn, pre)
		
		if IsServer then
			handler.Remote.OnServerInvoke = function(...)
				return SafeInvokeCallback(handler, ...)
			end
		else
			if handler.IncomingQueue then
				DeferredHandlers[handler] = true
			end
		end
	end
	
	ExecuteDeferredHandlers()
end
