-- Module for handling all game objects
require(game.ReplicatedFirst.ReadyModule)

local input = require(_G.Modules.InputModule)

-- Note that closures absorb upvalues. I'm trying this as a programming paradigm here.
local modulesByName = {}
local modules = {} -- [Name] = {...module}
local predicates = {}

local objects = {} -- Table of globally considered parts (responsible for considering) (consider = input and background tasks)
local owning = {} -- Table of locally owned parts (responsible for all processing)

-- Get the object from a part and do something to it
local function asObject(part,callback)
	if part.Parent:GetAttribute("PartId") then return callback(part.Parent) -- Since parts can be INSIDE of objects
	elseif part:GetAttribute("PartId") then return callback(part) end
end

local processOwnership

local context = {
	controls = {},
	root = nil -- Where the system is controlling from (Seat)
}
context.message = function(instance,name,...)
	local stuff = {...}
	asObject(instance,function(object)
		local data = objects[object]
		if not data then return end
		if name == "" then name = "default" end
		name = name or "default"
		for _,module in pairs(modules[object.Name] or {}) do
			if module.messages then
				if module.messages[name] then
					if objects[object] then
						module.messages[name](data,instance,context,table.unpack(stuff))
					end
				end
			end
		end
	end)
end
context.connected = function(wire_tag,end_tag,root,callback)
	local searched = {}
	
	local search = {root}
	while #search > 0 do
		local new_search = {}
		for _,search_part in pairs(search) do
			local connected = search_part:GetConnectedParts()
			for _,part in pairs(connected) do
				if not searched[part] then
					searched[part] = true
					if part:HasTag(wire_tag) then 
						table.insert(new_search,part)
						if part:GetAttribute("PartId") and part:HasTag("Glow") then part.Material = Enum.Material.Neon end
					end
					if (not end_tag) or part:HasTag(end_tag) or part.Parent:HasTag(end_tag) then
						asObject(part,function(object)
							callback(part)
						end)
					end
				end
			end
		end
		search = new_search
	end
end
local queues = {}
context.queue = function(this,tag,delta,valid)
	-- Init
	valid = valid or owning
	local queue = queues[tag]
	if queue then queue = {last = tick()} queues[tag] = queue end
	if not queue[this] then queue[this] = this table.insert(queue,1,this) end
	-- Get next in line
	local first = queue[1]
	while not valid[first] do table.remove(queue,1) first = queue[1] end -- Remove removed objects (lol)
	-- Are you ready!?
	if tick() - queue.last > delta then -- Aye aye captain!!
		queue.last = tick() -- Oooo
		table.remove(queue,1)
		table.insert(queue,this)
		return true -- hhhh
	else
		return false
	end
end
context.frequency = function(this,tag,delta)
	if not this[tag] then this[tag] = tick() end
	if tick() - this[tag] > delta then
		this[tag] = tick()
		return true
	else
		return false
	end
end
context.asObject = asObject

if _G.IsServer then
	context.replicate = function(this,object,module,...)
		module.replicate(this,object,context,...)
	end
elseif _G.IsClient then
	context.replicate = function(this,object,module,...)
		if module.replicate then module.replicate(this,object,context,...) end
		if tick() > module.next_replication then
			module.next_replication = tick() + module.replication_delta
			_G.Remotes.ObjectReplicate:FireServer(object,module.name,...)
		end
	end
end

local thisPlayer
if _G.IsServer then
	thisPlayer = nil
elseif _G.IsClient then
	thisPlayer = game.Players.LocalPlayer
end

-- Make object data from an instance (it's shared, so modules can communicate easily)
local function new(object)
	local this = {}
	for module,predicate in pairs(predicates) do
		if predicate(object) then 
			local tab = modules[object.Name]
			if not tab then tab = {} modules[object.Name] = tab end
			tab[module] = module
		end
	end
	for _,module in pairs(modules[object.Name] or {}) do
		_G.Protected(module.new,this,object,context)
	end
	objects[object] = this
	return this
end

local function remove(object)
	local this = objects[object]
	if not this then return end
	for _,module in pairs(modules[object.Name] or {}) do
		if module.remove then _G.Protected(module.remove,this,object,context) end
	end
	objects[object] = nil
	owning[object] = nil
end

local own
-- Add an object to be considered by this client or server
local function add(instance,owner)
	-- If this is not an instance or we already considered it then ignore
	local data = objects[instance]
	if data then return data end
	if not instance:GetAttribute("PartId") then return end
	data = new(instance)
	objects[instance] = data
	instance.Destroying:Connect(function()
		remove(instance)
	end)
	if _G.IsServer then 
		if owner then
			data.owner = owner
			_G.Protected(function()
				if instance:IsA("Model") then
					instance.PrimaryPart:SetNetworkOwner(owner)
					if instance:FindFirstChild("Hinge") then
						instance.Hinge:SetNetworkOwner(owner)
					end
				else
					instance:SetNetworkOwner(owner)
				end
			end)
		else
			own(instance)
		end
	else
		if owner then own(instance) end
	end
	return data
end

local function update(instance)
	local data = objects[instance]
	if data then
		for _,module in pairs(modules[instance.Name] or {}) do
			if module.update then _G.Protected(module.update,data,instance,context) end
		end
	end
end

-- Make the client drop ownership and continue to run idle tasks
local function sleep(object)
	if add(object) then owning[object] = nil end
end

-- Make the client own the object and take responsibility for processing
own = function(object)
	if add(object) then owning[object] = objects[object] end
end

-- Get all parts connected to an instance, determine if the player is still connected in any way
local hrp = {HumanoidRootPart = true}
local function playerInSeat(seat)
	if (seat:IsA("Seat") or seat:IsA("VehicleSeat")) and seat.Occupant then
		return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent)
	else
		local seat = seat:FindFirstChildWhichIsA("Seat") or seat:FindFirstChildWhichIsA("VehicleSeat")
		if seat and seat.Occupant then
			return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent)
		end
	end
end

local vs = {VehicleSeat = true}
local function maxRankSeat(seat1,seat2)
	if not seat1 then return seat2 end
	if not seat2 then return seat1 end
	local priority1 = seat1:GetAttribute("Priority") or 0
	local priority2 = seat2:GetAttribute("Priority") or 0
	-- A seat has priority over the other
	if priority1 == priority2 then
		local class1 = vs[seat1.Name]
		local class2 = vs[seat2.Name]
		-- One seat is a VehicleSeat and the other isn't
		if class1 and not class2 then return seat1
		elseif class2 and not class1 then return seat2
		else
			local owner1 = game.Players:GetPlayerByUserId(seat1:GetAttribute("Owner") or 0) == playerInSeat(seat1)
			local owner2 = game.Players:GetPlayerByUserId(seat2:GetAttribute("Owner") or 0) == playerInSeat(seat2)
			-- One player is seating in a seat that they placed themselves
			if owner1 and not owner2 then return seat1
			elseif owner2 and not owner1 then return seat2
			else return seat1 -- Since it was probably closest
			end
		end
	elseif priority1 > priority2 then return seat1 else return seat2 end
end

local function getParts(instance)
	local searchedParts = {} -- Searched connected instance parts
	local objectsFound = {} -- Found connected objects
	local assemblyRoots = {} -- Found AssemblyRootParts
	local playersSeated = {} -- Players that are connected to this vehicle in some way and their associated seat
	-- Recursive function to look through all the parts
	local function search(part)
		-- DRY
		local function add(instance)
			searchedParts[instance] = true
			local root = instance.AssemblyRootPart
			if not assemblyRoots[root] then assemblyRoots[root] = root end
			asObject(instance,function(object)
				if instance:FindFirstChild("SeatWeld") then
					local hrp = instance.SeatWeld.Part1
					local player = game.Players:GetPlayerFromCharacter(hrp.Parent)
					if player then
						-- Players could be in multiple seats so just pick the highest one
						playersSeated[player] = maxRankSeat(playersSeated[player],object)
					end
				end
				objectsFound[object] = object
			end)
		end
		-- If not found, add it, otherwise return
		if not searchedParts[part] then
			add(part)
		else return end
		-- For all the connected parts do the same thing, but not 'search' since it causes a bunch of excess checking
		for _,part in pairs(part:GetConnectedParts(true)) do
			-- If not found, add it, otherwise continue
			if not searchedParts[part] then 
				add(part)
			else continue end
			-- If the part is connected to a Character
			local joints = part:GetJoints()
			for _,joint in pairs(joints) do
				if joint:IsA("HingeConstraint") or joint:IsA("PrismaticConstraint") then
					-- Also check joints since their network ownership is funky otherwise
					if joint.Attachment0 and joint.Attachment1 then
						search(joint.Attachment0.Parent)
						search(joint.Attachment1.Parent)
					end
				end
			end
		end
	end
	search(instance)
	return objectsFound,playersSeated,assemblyRoots
end

-- Out of a list of player-seats get the highest priority player seated (nil == server)
local function maxPlayerSeated(playersSeated)
	local maxPlayer,maxSeat = nil
	for player,seat in pairs(playersSeated) do
		if maxRankSeat(maxSeat,seat) then maxPlayer = player maxSeat = seat end 
	end
	return maxPlayer
end

local function mark(object)
	if object:FindFirstChild("Mark") then object.Mark:Destroy() end
	local box = Instance.new("SelectionBox")
	box.Parent = object
	box.Adornee = object
	box.Name = "Mark"
	return box
end

-- This sort of runs on every client, so maybe it's far too slow?
processOwnership = function(seat)
	local objectsFound,playersSeated,assemblyRoots = getParts(seat)
	local ownerPlayer = maxPlayerSeated(playersSeated)
	-- The server needs to change the network owner in this case
	if _G.IsServer then
		_G.Remotes.OwnershipReplicate:FireAllClients(seat)
		for _,root in pairs(assemblyRoots) do
			local success,result = pcall(function()
				root:SetNetworkOwner(ownerPlayer)
			end)
			if not success then warn(result) end
		end
		for object in pairs(objectsFound) do
			local data = objects[object]
			if data then data.owner = ownerPlayer end
		end
		if ownerPlayer then
			for object in pairs(objectsFound) do
				sleep(object)
			end
		end
	end
	if ownerPlayer == thisPlayer then
		for _,object in pairs(objectsFound) do -- Connected and Owning
			--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,255,0) end
			own(object)
		end
	elseif playersSeated[thisPlayer] then -- Connected and not Owning
		for _,object in pairs(objects) do
			if objectsFound[object] then
				--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(255,0,0) end
				sleep(object)
			end
		end
	elseif _G.IsClient then -- Disconnected and not Owning
		for object,data in pairs(objects) do
			if objectsFound[object] then
				--if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,0,255) end
				remove(object)
			end
		end
	end
	if _G.IsClient then
		local ourSeat = playersSeated[thisPlayer]
		if ourSeat then
			input.seat = ourSeat
			context.seat = ourSeat
		elseif input.seat == asObject(seat,function(...) return ... end) then
			input.seat = nil
			context.seat = nil
		end
	end
end

local function objectsProcess(delta)
	context.delta = delta
	context.cursor = input.cursor
	for object,data in pairs(owning) do
		for _,module in pairs(modules[object.Name] or {}) do
			if module.run then if not module.run(data,object,context) then remove(object) end end
		end
	end
	for object,data in pairs(objects) do
		for _,module in pairs(modules[object.Name] or {}) do
			if module.idle then if not module.idle(data,object,context) then remove(object) end end
		end
	end
	for _,module in pairs(modulesByName) do
		if module.loop then
			module.loop(context)
		end
	end
end

if _G.IsServer then
	-- Clients and servers will send this to update information between each other (should be handled by values)
	_G.Remotes.ObjectReplicate.OnServerEvent:Connect(function(player,object,name,...)
		local data = objects[object]
		if data and data.owner == player then
			modulesByName[name].replicate(data,object,context,...)
		end
	end)
	game.Players.PlayerRemoving:Connect(function(player)
		for object,data in pairs(objects) do
			if data.owner == player then
				own(object)
			end
		end
	end)
elseif _G.IsClient then
	-- Respond to processing events from the server
	_G.Remotes.OwnershipReplicate.OnClientEvent:Connect(function(seat)
		processOwnership(seat)
	end)
end

for _,instance in pairs(script.Parent.Objects:GetChildren()) do
	local module = require(instance)
	modulesByName[instance.Name] = module
	module.name = instance.Name
	module.next_replication = tick()
	module.replication_delta = module.replication_delta or 0.5
	if module.manifest.predicate then
		predicates[module] = module.manifest.predicate
	else
		for _,name in pairs(module.manifest) do
			modules[name] = modules[name] or {}
			table.insert(modules[name],module)
		end
	end
end

if _G.IsServer then
	for _,instance in pairs(game.Workspace.Objects:GetChildren()) do
		add(instance)
	end
end

spawn(function()
	_G.Run.Heartbeat:Connect(function(delta)
		if _G.IsClient then context.controls = input.values end
		objectsProcess(delta)
	end)
end)

return {processOwnership = processOwnership, add = add, update = update, own = own}