-- local Madwork = _G.Madwork
--[[
{Madwork}

-[ReplicaController]---------------------------------------
	(STANDALONE VERSION)
	Lua table replication achieved through write function wrapping
	
	LISTENER WARNING: Making listeners disconnect their own script connections may result in other listeners
		being skipped. Fix pending.
	
	WARNING: Replica update listeners are not cleaned up automatically (e.g. when their value's parent table is set to nil)
		unless the replica is destroyed. Either split one god replica into several replicas or carefully manage listeners
		with :Disconnect() to prevent memory leaks. Does not apply to destroyed replicas.
	
	Notice: Replicas are destroyed client-side when the server stops replicating the replica to the client or the
		server destroys the replica completely. This means that the exact replica that was previously destroyed
		client-side could be created again client-side (as a new object, though).
		
	Replica replication guarantees:
		- When the client receives first data, replica references received will have all nested replicas
			already loaded and accessible through Replica.Children;
		- When the client receives first data or receives selective replication of a top level replica,
			.NewReplicaSignal and .ReplicaOfClassCreated() will be fired for all replicas in the order
			they were created server-side from earliest to latest;
	
	Members:
	
		ReplicaController.NewReplicaSignal            [ScriptSignal](Replica) -- Fired every time an replica is created client-side
		ReplicaController.InitialDataReceivedSignal   [ScriptSignal]() -- Fired once after the client finishes receiving initial replica data from server
		ReplicaController.InitialDataReceived         [bool] -- Set to true after the client finishes receiving initial replica data from server
	
	Functions:
	
		ReplicaController.RequestData() -- Requests the server to start sending replica data
		
		ReplicaController.ReplicaOfClassCreated(replica_class, listener) --> [ScriptConnection] listener(replica)
		
		ReplicaController.GetReplicaById(replica_id) --> replica / nil
		
	Members [Replica]:
	
		Replica.Data       [table] (Read only) Table which is replicated
		
		Replica.Id         [number] Unique identifier
		Replica.Class      [string] Primary Replica identifier
		Replica.Tags       [table] Secondary Replica identifiers
		
		Replica.Parent     [Replica] or nil
		Replica.Children   [table]: {replica, ...}
		
	Methods [Replica]:
		
	-- Dictionary update listening: (listener functions can't yield)
		Replica:ListenToChange(path, listener) --> [ScriptConnection] (new_value, old_value)
		Replica:ListenToNewKey(path, listener) --> [ScriptConnection] (new_value, new_key)
		
		* Notice: When Replica:SetValues(path, values) is used server-side, Replica:ListenToChange() and Replica:ListenToNewKey()
			will only be invoked with changes to the top level keys in the "values" argument passed.
		
	-- (Numeric) Array update listening:
		Replica:ListenToArrayInsert(path, listener) --> [ScriptConnection] (new_index, new_value)
		Replica:ListenToArraySet(path, listener) --> [ScriptConnection] (index, new_value)
		Replica:ListenToArrayRemove(path, listener) --> [ScriptConnection] (old_index, old_value)
		
	-- Write function listening:
		Replica:ListenToWrite(function_name, listener) --> [ScriptConnection] (params...)
		
		* Parameter description for "path":
		
			[string] = "TableMember.TableMember" -- Roblox-style path
			[table] = {"Players", 2312310, "Health"} -- Key array path
			
		Replica:ListenToRaw(listener) --> [ScriptConnection] (action_name, path_array, params...)
			-- ("SetValue", path_array, value)
			-- ("SetValues", path_array, values)
			-- ("ArrayInsert", path_array, value, new_index)
			-- ("ArraySet", path_array, index, value)
			-- ("ArrayRemove", path_array, index, old_value)
		
	-- Signals:
		Replica:ConnectOnClientEvent(listener) --> [ScriptConnection] (params...) -- listener functions can't yield
		Replica:FireServer(params...) -- Fire a signal to server-side listeners for this specific Replica
		
	-- Children:
		Replica:ListenToChildAdded(listener) --> [ScriptConnection] listener(replica)
		
		Replica:FindFirstChildOfClass(replica_class) --> [Replica] or nil
			replica_class   [string]
		
	-- Debug:
		Replica:Identify() --> [string]
		
	-- Cleanup:
	
		Replica:IsActive() --> is_active [bool] -- Returns false if the replica was destroyed
	
		Replica:AddCleanupTask(task) -- Add cleanup task to be performed
		Replica:RemoveCleanupTask(task) -- Remove cleanup task
		
		* Parameter description for "Replica:AddCleanupTask()":
		
			[function] -- Function to be invoked when the Replica is destroyed (function can't yield)
			[RBXScriptConnection] -- Roblox script connection to be :Disconnect()'ed when the Replica is destroyed
			[Object] -- Object with a :Destroy() method to be destroyed when the Replica is destroyed (destruction method can't yield)
			
	-- Write function setters: (Calling outside a write function will throw an error)
	
		Replica:SetValue(path, value)
		Replica:SetValues(path, values)
		Replica:ArrayInsert(path, value) --> new_index
		Replica:ArraySet(path, index, value)
		Replica:ArrayRemove(path, index) --> removed_value
		Replica:Write(function_name, params...) --> return_params...

--]]

local SETTINGS = {
	RequestDataRepeat = 10,
	SetterError = "[ReplicaController]: Replica setters can only be called inside write functions",
}

local Madwork -- Standalone Madwork reference for portable version of ReplicaService/ReplicaController
do
	local RunService = game:GetService("RunService")
	
	local function WaitForDescendant(ancestor, instance_name, warn_name)
		local instance = ancestor:FindFirstChild(instance_name, true) -- Recursive
		if instance == nil then
			local start_time = os.clock()
			local connection
			connection = ancestor.DescendantAdded:Connect(function(descendant)
				if descendant.Name == instance_name then
					instance = descendant
				end
			end)
			while instance == nil do
				if start_time ~= nil and os.clock() - start_time > 1
					and (RunService:IsServer() == true or game:IsLoaded() == true) then
					start_time = nil
					warn("[" .. script.Name .. "]: Missing " .. warn_name .. " \"" .. instance_name
						.. "\" in " .. ancestor:GetFullName() .. "; Please check setup documentation")
				end
				task.wait()
			end
			connection:Disconnect()
			return instance
		else
			return instance
		end
	end
	
	local RemoteEventContainer
	if RunService:IsServer() == true then
		RemoteEventContainer = Instance.new("Folder")
		RemoteEventContainer.Name = "ReplicaRemoteEvents"
		RemoteEventContainer.Parent = game:GetService("ReplicatedStorage")
	else
		RemoteEventContainer = WaitForDescendant(game:GetService("ReplicatedStorage"), "ReplicaRemoteEvents", "folder")
	end
	
	Madwork = {
		GetShared = function(package_name, item_name)
			-- Ignoring package_name as we're working without Madwork framework
			return WaitForDescendant(game:GetService("ReplicatedStorage"), item_name, "module")
		end,
		GetModule = function(package_name, module_name)
			return WaitForDescendant(game:GetService("ServerScriptService"), module_name, "module")
		end,
		SetupRemoteEvent = function(remote_name)
			if RunService:IsServer() == true then
				local remote_event = Instance.new("RemoteEvent")
				remote_event.Name = remote_name
				remote_event.Parent = RemoteEventContainer
				return remote_event
			else
				return WaitForDescendant(RemoteEventContainer, remote_name, "remote event")
			end
		end,
		Shared = {}, -- A Madwork package reference - ReplicaService will try to check this table
	}
	
	local MadworkScriptSignal = require(Madwork.GetShared("Madwork", "MadworkScriptSignal"))
	Madwork.NewScriptSignal = MadworkScriptSignal.NewScriptSignal
	Madwork.NewArrayScriptConnection = MadworkScriptSignal.NewArrayScriptConnection
end