local L="\114\111\111\116"local function G(L)local G=string.split(L,"\047")for L=#G,1,-1 do if G[L]==""then table.remove(G,L)end end return G end local function Y(L,G)local Y=0 for L in pairs(L)do Y+=1 end assert(Y<=G,Y)return Y==G end local function U(L,G)if L.instance==nil then return end L.instance=nil local Y=L.connections if Y then for L,G in ipairs(Y)do G:Disconnect()end table.clear(Y)end if G and L.alias then G[L.alias]=nil end local Z=L.children if Z then for L,Y in pairs(Z)do U(Y,G)end end end local Z={}Z.__index=Z function Z.new(L,Y)local U={}local i={}local B={}local p={}local j={}local D=1 for L,Y in pairs(L)do j[L]=G(Y)D+=1 end return setmetatable({_boundFn=Y,_parsedManifest=j;_manifestSizeTarget=D;_dtorMap=U,_connections=i,_rootInstToRootNode=B,_rootInstToManifest=p},Z)end function Z._startBoundFn(Y,L,G)local U=Y._boundFn local Z=Y._dtorMap local i=Z[L]if i then i()Z[L]=nil end local B=U(G)if B then Z[L]=B end end function Z._stopBoundFn(G,L)local Y=G._dtorMap local U=Y[L]if U then U()Y[L]=nil end end function Z.bindRoot(Z,G)debug.profilebegin("\065\116\111\109\105\099\066\105\110\100\105\110\103\058\066\105\110\100\082\111\111\116")local i=Z._parsedManifest local B=Z._rootInstToRootNode local p=Z._rootInstToManifest local j=Z._manifestSizeTarget assert(p[G]==nil)local D={}p[G]=D debug.profilebegin("\066\117\105\108\100\084\114\101\101")local a={}a.alias=L a.instance=G if next(i)then a.children={}a.connections={}end B[G]=a for L,G in pairs(i)do local Y=a for U,Z in ipairs(G)do local i=U==#G local B=Y.children[Z]or{}if i then if B.alias~=nil then error("\077\117\108\116\105\112\108\101 \097\108\105\097\115\101\115 \097\115\115\105\103\110\101\100 \116\111 \111\110\101 \105\110\115\116\097\110\099\101")end B.alias=L else B.children=B.children or{}B.connections=B.connections or{}end Y.children[Z]=B Y=B end end debug.profileend()local function t(L)local i=assert(L.instance)local B=L.children local p=L.alias local a=not B if p then D[p]=i end if not a then local function Y(L)local G=L.Name local Y=B[G]if not Y or Y.instance~=nil then return end Y.instance=L t(Y)end local function p(L)local p=L.Name local j=B[p]if not j then return end if j.instance~=L then return end Z:_stopBoundFn(G)U(j,D)assert(j.instance==nil)local a=i:FindFirstChild(p)if a then Y(a)end end for L,G in ipairs(i:GetChildren())do Y(G)end table.insert(L.connections,i.ChildAdded:Connect(Y))table.insert(L.connections,i.ChildRemoved:Connect(p))end if a and Y(D,j)then Z:_startBoundFn(G,D)end end debug.profilebegin("\082\101\115\111\108\118\101\084\114\101\101")t(a)debug.profileend()debug.profileend()end function Z.unbindRoot(G,L)local Y=G._rootInstToRootNode local Z=G._rootInstToManifest G:_stopBoundFn(L)local i=Y[L]if i then local G=assert(Z[L])U(i,G)Y[L]=nil end Z[L]=nil end function Z.destroy(L)debug.profilebegin("\065\116\111\109\105\099\066\105\110\100\105\110\103\058\100\101\115\116\114\111\121")for L,G in pairs(L._dtorMap)do G:destroy()end table.clear(L._dtorMap)for L,G in ipairs(L._connections)do G:Disconnect()end table.clear(L._connections)local G=L._rootInstToManifest for L,Y in pairs(L._rootInstToRootNode)do local Z=assert(G[L])U(Y,Z)end table.clear(L._rootInstToManifest)table.clear(L._rootInstToRootNode)debug.profileend()end return Z
--!nonstrict
local ROOT_ALIAS = "root"

local function parsePath(pathStr)
	local pathArray = string.split(pathStr, "/")
	for idx = #pathArray, 1, -1 do
		if pathArray[idx] == "" then
			table.remove(pathArray, idx)
		end
	end
	return pathArray
end

local function isManifestResolved(resolvedManifest, manifestSizeTarget)
	local manifestSize = 0
	for _ in pairs(resolvedManifest) do
		manifestSize += 1
	end

	assert(manifestSize <= manifestSizeTarget, manifestSize)
	return manifestSize == manifestSizeTarget
end

local function unbindNodeDescend(node, resolvedManifest)
	if node.instance == nil then
		return -- Do not try to unbind nodes that are already unbound
	end

	node.instance = nil

	local connections = node.connections
	if connections then
		for _, conn in ipairs(connections) do
			conn:Disconnect()
		end
		table.clear(connections)
	end

	if resolvedManifest and node.alias then
		resolvedManifest[node.alias] = nil
	end

	local children = node.children
	if children then
		for _, childNode in pairs(children) do
			unbindNodeDescend(childNode, resolvedManifest)
		end
	end
end

local AtomicBinding = {}
AtomicBinding.__index = AtomicBinding

function AtomicBinding.new(manifest, boundFn)
	local dtorMap = {} -- { [root] -> dtor }
	local connections = {} -- { Connection, ... }
	local rootInstToRootNode = {} -- { [root] -> rootNode }
	local rootInstToManifest = {} -- { [root] -> { [alias] -> instance } }

	local parsedManifest = {} -- { [alias] = {Name, ...} }
	local manifestSizeTarget = 1 -- Add 1 because root isn't explicitly on the manifest	
	
	for alias, rawPath in pairs(manifest) do
		parsedManifest[alias] = parsePath(rawPath)
		manifestSizeTarget += 1
	end

	return setmetatable({
		_boundFn = boundFn,
		_parsedManifest = parsedManifest,
		_manifestSizeTarget = manifestSizeTarget,
		
		_dtorMap = dtorMap,
		_connections = connections,
		_rootInstToRootNode = rootInstToRootNode,
		_rootInstToManifest = rootInstToManifest,
	}, AtomicBinding)
end

function AtomicBinding:_startBoundFn(root, resolvedManifest)
	local boundFn = self._boundFn
	local dtorMap = self._dtorMap
	
	local oldDtor = dtorMap[root]
	if oldDtor then
		oldDtor()
		dtorMap[root] = nil
	end

	local dtor = boundFn(resolvedManifest)
	if dtor then
		dtorMap[root] = dtor
	end
end

function AtomicBinding:_stopBoundFn(root)
	local dtorMap = self._dtorMap
	
	local dtor = dtorMap[root]
	if dtor then
		dtor()
		dtorMap[root] = nil
	end
end

function AtomicBinding:bindRoot(root)
	debug.profilebegin("AtomicBinding:BindRoot")
	
	local parsedManifest = self._parsedManifest
	local rootInstToRootNode = self._rootInstToRootNode
	local rootInstToManifest = self._rootInstToManifest
	local manifestSizeTarget = self._manifestSizeTarget
	
	assert(rootInstToManifest[root] == nil)

	local resolvedManifest = {}
	rootInstToManifest[root] = resolvedManifest

	debug.profilebegin("BuildTree")

	local rootNode = {}
	rootNode.alias = ROOT_ALIAS
	rootNode.instance = root
	if next(parsedManifest) then
		-- No need to assign child data if there are no children
		rootNode.children = {}
		rootNode.connections = {}
	end

	rootInstToRootNode[root] = rootNode

	for alias, parsedPath in pairs(parsedManifest) do
		local parentNode = rootNode

		for idx, childName in ipairs(parsedPath) do
			local leaf = idx == #parsedPath
			local childNode = parentNode.children[childName] or {}

			if leaf then
				if childNode.alias ~= nil then
					error("Multiple aliases assigned to one instance")
				end

				childNode.alias = alias

			else
				childNode.children = childNode.children or {}
				childNode.connections = childNode.connections or {}
			end

			parentNode.children[childName] = childNode
			parentNode = childNode
		end
	end

	debug.profileend() -- BuildTree

	-- Recursively descend into the tree, resolving each node.
	-- Nodes start out as empty and instance-less; the resolving process discovers instances to map to nodes.
	local function processNode(node)
		local instance = assert(node.instance)

		local children = node.children
		local alias = node.alias
		local isLeaf = not children

		if alias then
			resolvedManifest[alias] = instance
		end

		if not isLeaf then
			local function processAddChild(childInstance)
				local childName = childInstance.Name
				local childNode = children[childName]
				if not childNode or childNode.instance ~= nil then
					return
				end

				childNode.instance = childInstance
				processNode(childNode)
			end

			local function processDeleteChild(childInstance)
				-- Instance deletion - Parent A detects that child B is being removed
				--    1. A removes B from `children`
				--    2. A traverses down from B,
				--       i.  Disconnecting inputs
				--       ii. Removing nodes from the resolved manifest
				--    3. stopBoundFn is called because we know the tree is no longer complete, or at least has to be refreshed
				-- 	  4. We search A for a replacement for B, and attempt to re-resolve using that replacement if it exists.
				-- To support the above sanely, processAddChild needs to avoid resolving nodes that are already resolved.

				local childName = childInstance.Name
				local childNode = children[childName]

				if not childNode then
					return -- There's no child node corresponding to the deleted instance, ignore
				end

				if childNode.instance ~= childInstance then
					return -- A child was removed with the same name as a node instance, ignore
				end

				self:_stopBoundFn(root) -- Happens before the tree is unbound so the manifest is still valid in the destructor.
				unbindNodeDescend(childNode, resolvedManifest) -- Unbind the tree

				assert(childNode.instance == nil) -- If this triggers, unbindNodeDescend failed

				-- Search for a replacement
				local replacementChild = instance:FindFirstChild(childName)
				if replacementChild then
					processAddChild(replacementChild)
				end
			end

			for _, child in ipairs(instance:GetChildren()) do
				processAddChild(child)
			end

			table.insert(node.connections, instance.ChildAdded:Connect(processAddChild))
			table.insert(node.connections, instance.ChildRemoved:Connect(processDeleteChild))
		end

		if isLeaf and isManifestResolved(resolvedManifest, manifestSizeTarget) then
			self:_startBoundFn(root, resolvedManifest)
		end
	end

	debug.profilebegin("ResolveTree")
	processNode(rootNode)
	debug.profileend() -- ResolveTree
	
	debug.profileend() -- AtomicBinding:BindRoot
end

function AtomicBinding:unbindRoot(root)
	local rootInstToRootNode = self._rootInstToRootNode
	local rootInstToManifest = self._rootInstToManifest
	
	self:_stopBoundFn(root)

	local rootNode = rootInstToRootNode[root]
	if rootNode then
		local resolvedManifest = assert(rootInstToManifest[root])
		unbindNodeDescend(rootNode, resolvedManifest)
		rootInstToRootNode[root] = nil
	end

	rootInstToManifest[root] = nil
end

function AtomicBinding:destroy()
	debug.profilebegin("AtomicBinding:destroy")

	for _, dtor in pairs(self._dtorMap) do
		dtor:destroy()
	end
	table.clear(self._dtorMap)

	for _, conn in ipairs(self._connections) do
		conn:Disconnect()
	end
	table.clear(self._connections)

	local rootInstToManifest = self._rootInstToManifest
	for rootInst, rootNode in pairs(self._rootInstToRootNode) do
		local resolvedManifest = assert(rootInstToManifest[rootInst])
		unbindNodeDescend(rootNode, resolvedManifest)
	end
	table.clear(self._rootInstToManifest)
	table.clear(self._rootInstToRootNode)

	debug.profileend()
end

return AtomicBinding
