local w=require(script.Parent.Parent.Types)local O=require(script.Parent.Parent.Parent.Shared.Util)local T=require(script.Parent.Aggregate)local I=require(script.Parent.Parent.Constants)local q={}q.__index=q function q.new(w)return setmetatable({rocs=w;_components={};_entities={},_aggregates={}},q)end local function B(w)return function(O)for w,T in ipairs(w)do if O:IsA(T)then return true end end return false,("\073\110\115\116\097\110\099\101 \116\121\112\101 \037\113 \105\115 \110\111\116 \097\108\108\111\119\101\100 \116\111 \104\097\118\101 \116\104\105\115 \099\111\109\112\111\110\101\110\116\033"):format(O.ClassName)end end function q.runEntityCheck(w,O)if w.entityCheck==nil then return true end if type(w.entityCheck)=="\116\097\098\108\101"then w.entityCheck=B(w.entityCheck)end return w.entityCheck(O)end function q.register(I,O)assert(w.ComponentDefinition(O))assert(I._components[O.name]==nil,"\065 \099\111\109\112\111\110\101\110\116 \119\105\116\104 \116\104\105\115 \110\097\109\101 \105\115 \097\108\114\101\097\100\121 \114\101\103\105\115\116\101\114\101\100\033")setmetatable(O,T)O.__index=O O.__tostring=T.__tostring O.rocs=I.rocs O.new=O.new or function()return setmetatable({},O)end I._components[O.name]=O I._components[O]=O return O end function q.construct(T,w,O)local I=w.new()assert(getmetatable(I)==w,"\077\101\116\097\116\097\098\108\101 \111\102 \099\111\110\115\116\114\117\099\116\101\100 \099\111\109\112\111\110\101\110\116 \097\103\103\114\101\103\097\116\101 \109\117\115\116 \098\101 \115\116\097\116\105\099 \099\111\109\112\111\110\101\110\116 \097\103\103\114\101\103\097\116\101")I.components={}I.instance=O T:_dispatchLifecycle(I,"\105\110\105\116\105\097\108\105\122\101")return I end function q.deconstruct(O,w)local T=getmetatable(w)O._entities[w.instance][T]=nil local I=O._aggregates[T]for O,T in ipairs(I)do if T==w then table.remove(I,O)break end end if#I==0 then O._aggregates[T]=nil end if next(O._entities[w.instance])==nil then O._entities[w.instance]=nil end O:removeAllComponents(w)end function q.addComponent(L,w,O,T,B,f)if B==nil then return L:removeComponent(w,O,T),false end assert(q.runEntityCheck(O,w))if L._entities[w]==nil then L._entities[w]={}end if L._aggregates[O]==nil then L._aggregates[O]={}end local a=L._entities[w][O]local c=false if a==nil then c=true a=L:construct(O,w)L._entities[w][O]=a table.insert(L._aggregates[O],a)end a.components[T]=B L:_dispatchComponentChange(a)local D={}if c and O.components then for w,O in pairs(O.components)do local T=L:getStatic(w)local q=L:addComponent(a,T,I.SCOPE_BASE,O)D[q]=true end end if f then for w,O in pairs(f)do local I=L:getStatic(w)local q,B=L:addComponent(a,I,T,O)if B then D[q]=true end end end for w in pairs(D)do L:_dispatchLifecycle(w,I.LIFECYCLE_PARENT_UPDATED)end return a,c end function q.removeAllComponents(O,w)if O._entities[w]==nil then return end for w,T in ipairs(O:getAll(w))do T.components={}O:deconstruct(T)O:_dispatchComponentChange(T)O:_dispatchLifecycle(T,"\100\101\115\116\114\111\121")end end function q.removeComponent(I,w,O,T)if I._entities[w]==nil or I._entities[w][O]==nil then return end local q=I._entities[w][O]q.components[T]=nil I:_dispatchComponentChange(q)local B=next(q.components)==nil if B then I:deconstruct(q)end if B then I:_dispatchLifecycle(q,"\100\101\115\116\114\111\121")end end function q.getComponent(I,w,O,T)if I._entities[w]==nil or I._entities[w][O]==nil then return end return I._entities[w][O].components[T]end function q.getAll(O,w)local T={}if O._entities[w]~=nil then for w,O in pairs(O._entities[w])do table.insert(T,O)end end return T end function q.get(T,w,O)return T._entities[w]and T._entities[w][O]end function q.getStatic(O,w)return O:resolve(w)or error(("\067\097\110\110\111\116 \114\101\115\111\108\118\101 \099\111\109\112\111\110\101\110\116 \037\115"):format(w))end function q.resolve(O,w)return O._components[w]or type(w)=="\116\097\098\108\101"and(getmetatable(w)==T and w)end function q.reduce(O,w)if next(w.components)==nil then return end local T={w.components[I.SCOPE_REMOTE]}table.insert(T,w.components[I.SCOPE_BASE])for w,O in pairs(w.components)do if I.RESERVED_SCOPES[w]==nil then table.insert(T,O)end end local q=getmetatable(w)local B=(q.reducer or O.rocs.reducers.default)(T)local f=B if q.defaults and type(B)=="\116\097\098\108\101"then q.defaults.__index=q.defaults f=setmetatable(B,q.defaults)end if q.check then assert(q.check(f))end return f end function q._dispatchLifecycle(T,w,O)w:dispatch(O)T.rocs:_dispatchLifecycle(w,O)end function q._dispatchComponentChange(O,w)local T=w.data local q=O:reduce(w)w.data=q w.lastData=T if T==nil and q~=nil then O:_dispatchLifecycle(w,I.LIFECYCLE_ADDED)end local B=getmetatable(w)if(B.shouldUpdate or O.rocs.comparators.default)(q,T)then O:_dispatchLifecycle(w,I.LIFECYCLE_UPDATED)local T=O:getAll(w)for w,T in ipairs(T)do O:_dispatchLifecycle(T,I.LIFECYCLE_PARENT_UPDATED)end end if q==nil then O:_dispatchLifecycle(w,I.LIFECYCLE_REMOVED)end w.lastData=nil end return q
local I = require(script.Parent.Parent.Types)
local Util = require(script.Parent.Parent.Parent.Shared.Util)
local Aggregate = require(script.Parent.Aggregate)
local Constants = require(script.Parent.Parent.Constants)

local AggregateCollection = {}
AggregateCollection.__index = AggregateCollection

function AggregateCollection.new(rocs)
	return setmetatable({
		rocs = rocs;
		_components = {};
		_entities = {};
		_aggregates = {};
	}, AggregateCollection)
end

local function makeArrayEntityCheck(array)
	return function(instance)
		for _, className in ipairs(array) do
			if instance:IsA(className) then
				return true
			end
		end

		return
			false,
			("Instance type %q is not allowed to have this component!")
				:format(instance.ClassName)
	end
end

function AggregateCollection.runEntityCheck(staticAggregate, instance)
	if staticAggregate.entityCheck == nil then
		return true
	end

	if type(staticAggregate.entityCheck) == "table" then
		staticAggregate.entityCheck = makeArrayEntityCheck(staticAggregate.entityCheck)
	end

	return staticAggregate.entityCheck(instance)
end

function AggregateCollection:register(componentDefinition)
	assert(I.ComponentDefinition(componentDefinition))
	assert(self._components[componentDefinition.name] == nil, "A component with this name is already registered!")

	setmetatable(componentDefinition, Aggregate)

	componentDefinition.__index = componentDefinition
	componentDefinition.__tostring = Aggregate.__tostring
	componentDefinition.rocs = self.rocs

	componentDefinition.new = componentDefinition.new or function()
		return setmetatable({}, componentDefinition)
	end

	self._components[componentDefinition.name] = componentDefinition
	self._components[componentDefinition] = componentDefinition

	return componentDefinition
end

function AggregateCollection:construct(staticAggregate, instance)
	local aggregate = staticAggregate.new()

	assert(
		getmetatable(aggregate) == staticAggregate,
		"Metatable of constructed component aggregate must be static component aggregate"
	)

	aggregate.components = {}
	aggregate.instance = instance

	self:_dispatchLifecycle(aggregate, "initialize")

	return aggregate
end

function AggregateCollection:deconstruct(aggregate)
	-- destroy is called in removeComponent for correct timing

	local staticAggregate = getmetatable(aggregate)
	self._entities[aggregate.instance][staticAggregate] = nil

	local array = self._aggregates[staticAggregate]

	for i, v in ipairs(array) do
		if v == aggregate then
			table.remove(array, i)
			break
		end
	end

	if #array == 0 then
		self._aggregates[staticAggregate] = nil
	end

	if next(self._entities[aggregate.instance]) == nil then
		self._entities[aggregate.instance] = nil
	end

	self:removeAllComponents(aggregate)
end

function AggregateCollection:addComponent(instance, staticAggregate, scope, data, metacomponents)
	if data == nil then
		return self:removeComponent(instance, staticAggregate, scope), false
	end

	assert(AggregateCollection.runEntityCheck(staticAggregate, instance))

	if self._entities[instance] == nil then
		self._entities[instance] = {}
	end

	if self._aggregates[staticAggregate] == nil then
		self._aggregates[staticAggregate] = {}
	end

	local aggregate = self._entities[instance][staticAggregate]
	local isNew = false

	if aggregate == nil then
		isNew = true
		aggregate = self:construct(staticAggregate, instance)
		self._entities[instance][staticAggregate] = aggregate

		table.insert(self._aggregates[staticAggregate], aggregate)
	end

	aggregate.components[scope] = data

	self:_dispatchComponentChange(aggregate)

	local pendingParentUpdated = {}

	if isNew and staticAggregate.components then
		for componentResolvable, metacomponentData in pairs(staticAggregate.components) do
			local metacomponentStaticAggregate = self:getStatic(componentResolvable)

			local metacomponentAggregate = self:addComponent(
				aggregate,
				metacomponentStaticAggregate,
				Constants.SCOPE_BASE,
				metacomponentData
			)

			pendingParentUpdated[metacomponentAggregate] = true
		end
	end

	if metacomponents then
		for componentResolvable, metacomponentData in pairs(metacomponents) do
			local metacomponentStaticAggregate = self:getStatic(componentResolvable)

			local metacomponentAggregate, wasNew = self:addComponent(
				aggregate,
				metacomponentStaticAggregate,
				scope,
				metacomponentData
			)

			if wasNew then
				pendingParentUpdated[metacomponentAggregate] = true
			end
		end
	end

	-- De-duplicate onParentUpdated calls in case both tables have same
	for metacomponentAggregate in pairs(pendingParentUpdated) do
		self:_dispatchLifecycle(
			metacomponentAggregate,
			Constants.LIFECYCLE_PARENT_UPDATED
		)
	end

	return aggregate, isNew
end

function AggregateCollection:removeAllComponents(instance)
	if self._entities[instance] == nil then
		return
	end

	for _, aggregate in ipairs(self:getAll(instance)) do
		aggregate.components = {}

		self:deconstruct(aggregate)

		self:_dispatchComponentChange(aggregate)

		self:_dispatchLifecycle(aggregate, "destroy")
	end
end

function AggregateCollection:removeComponent(instance, staticAggregate, scope)
	if
		self._entities[instance] == nil
		or self._entities[instance][staticAggregate] == nil
	then
		return
	end

	local aggregate = self._entities[instance][staticAggregate]

	aggregate.components[scope] = nil

	self:_dispatchComponentChange(aggregate)

	local shouldDestroy = next(aggregate.components) == nil
	if shouldDestroy then
		self:deconstruct(aggregate)
	end

	if shouldDestroy then
		self:_dispatchLifecycle(aggregate, "destroy")
	end

	-- TODO: Should destroy be deffered to end-of-frame?
end

function AggregateCollection:getComponent(instance, staticAggregate, scope)
	if
		self._entities[instance] == nil
		or self._entities[instance][staticAggregate] == nil
	then
		return
	end

	return self._entities[instance][staticAggregate].components[scope]
end

function AggregateCollection:getAll(instance)
	local aggregates = {}

	if self._entities[instance] ~= nil then
		for _, aggregate in pairs(self._entities[instance]) do
			table.insert(aggregates, aggregate)
		end
	end

	return aggregates
end

function AggregateCollection:get(instance, staticAggregate)
	return
		self._entities[instance]
		and self._entities[instance][staticAggregate]
end

function AggregateCollection:getStatic(componentResolvable)
	return
		self:resolve(componentResolvable)
		or error(("Cannot resolve component %s"):format(componentResolvable))
end

function AggregateCollection:resolve(componentResolvable)
	return self._components[componentResolvable]
		or (
			type(componentResolvable) == "table"
			and getmetatable(componentResolvable) == Aggregate
			and componentResolvable
		)
end

function AggregateCollection:reduce(aggregate)
	if next(aggregate.components) == nil then
		return
	end

	local values = { aggregate.components[Constants.SCOPE_REMOTE] }
	table.insert(values, aggregate.components[Constants.SCOPE_BASE])

	for name, component in pairs(aggregate.components) do
		if Constants.RESERVED_SCOPES[name] == nil then
			table.insert(values, component)
		end
	end

	local staticAggregate = getmetatable(aggregate)

	local reducedValue = (staticAggregate.reducer or self.rocs.reducers.default)(values)

	local data = reducedValue
	if staticAggregate.defaults and type(reducedValue) == "table" then
		staticAggregate.defaults.__index = staticAggregate.defaults
		data = setmetatable(
			reducedValue,
			staticAggregate.defaults
		)
	end

	if staticAggregate.check then
		assert(staticAggregate.check(data))
	end

	return data
end

function AggregateCollection:_dispatchLifecycle(aggregate, stage)
	aggregate:dispatch(stage)

	self.rocs:_dispatchLifecycle(aggregate, stage)
end

function AggregateCollection:_dispatchComponentChange(aggregate)
	local lastData = aggregate.data
	local newData = self:reduce(aggregate)

	aggregate.data = newData
	aggregate.lastData = lastData

	if lastData == nil and newData ~= nil then
		self:_dispatchLifecycle(aggregate, Constants.LIFECYCLE_ADDED)
	end

	local staticAggregate = getmetatable(aggregate)

	if (staticAggregate.shouldUpdate or self.rocs.comparators.default)(newData, lastData) then
		self:_dispatchLifecycle(aggregate, Constants.LIFECYCLE_UPDATED)

		local childAggregates = self:getAll(aggregate)
		for _, childAggregate in ipairs(childAggregates) do
			self:_dispatchLifecycle(
				childAggregate,
				Constants.LIFECYCLE_PARENT_UPDATED
			)
		end
	end

	if newData == nil then
		self:_dispatchLifecycle(aggregate, Constants.LIFECYCLE_REMOVED)
	end

	aggregate.lastData = nil
end

return AggregateCollection
