local w=require(script.Parent.Selectors.AllSelector)local O=require(script.Parent.Selectors.ComponentSelector)local T=require(script.Parent.Selectors.Util)local I=(game:GetService("\082\117\110\083\101\114\118\105\099\101")).Stepped local q=setmetatable({},w)q.__index=q function q.new(I,B,...)local f={...}local L if#f==1 then if T.inheritsBase(f[1])then L=f[1]elseif type(f[1])=="\115\116\114\105\110\103"then L=O.new(I,f[1])end end local a=setmetatable(L or w.new(I,...),q)a._entities={}a._events={}a._intervals={}a._intervalConnection=nil a._hooks.onComponentAdded={}a._hooks.onComponentRemoved={}a._hooks.onComponentUpdated={}a._hooks.onComponentParentUpdated={}a._scope=B return a end function q._stop(w)for w,O in pairs(w._events)do if O.connection then O.connection:Disconnect()O.connection=nil end end if w._intervalConnection then w._intervalConnection:Disconnect()w._intervalConnection=nil end end function q._start(w)for w,O in pairs(w.events)do if not O.connection then O.connection=O.event:Connect(O.hook)end end if not w._intervalConnection then w._intervalConnection=I:Connect(function()for w,O in pairs(w._intervals)do local T=O.lastInvoke local I=tick()if not T then O.lastInvoke=I O.hook(0)elseif I-T>=O.interval then O.lastInvoke=I O.hook(I-T)end end end)end end function q._listen(w)for O,T in pairs(w._selectors)do T:onAdded(function(O)local I=O.instance if w:check(I,T)then if not w._lookup[I]then local O=w._rocs:getEntity(I,w._scope)table.insert(w._entities,O)w._lookup[I]=O if#w._entities==1 then w:_start()end w:_trigger("\111\110\065\100\100\101\100",O)end w:_trigger("\111\110\067\111\109\112\111\110\101\110\116\065\100\100\101\100",O)end end)T:onRemoved(function(O)local T=O.instance if w._lookup[T]then if not w:check(T)then local O=w._lookup[T]w._lookup[T]=nil for T,I in pairs(w._entities)do if I==O then table.remove(w._entities,T)break end end if#w._entities==0 then w:_stop()end w:_trigger("\111\110\082\101\109\111\118\101\100",O)end w:_trigger("\111\110\067\111\109\112\111\110\101\110\116\082\101\109\111\118\101\100",O)end end)T:onUpdated(function(O)local T=w._lookup[O.instance]if T then w:_trigger("\111\110\085\112\100\097\116\101\100",T)w:_trigger("\111\110\067\111\109\112\111\110\101\110\116\085\112\100\097\116\101\100",O)end end)T:onParentUpdated(function(O)local T=w._lookup[O.instance]if T then w:_trigger("\111\110\080\097\114\101\110\116\085\112\100\097\116\101\100",T)w:_trigger("\111\110\067\111\109\112\111\110\101\110\116\080\097\114\101\110\116\085\112\100\097\116\101\100",O)end end)end end function q.setup(w)if w._ready then return end w._ready=true w:_listen()for w,O in pairs(w._selectors)do O:setup()end for O,T in pairs(w:instances())do local I=w._rocs:getEntity(T,w._scope)table.insert(w._entities,I)w._lookup[T]=I end if#w._entities>0 then w:_start()end return w end function q.destroy(w)if not w._ready then return end w._ready=nil for O,T in pairs(w._hooks)do w._hooks[O]={}end for w,O in pairs(w._selectors)do O:destroy()end w._entities={}w._lookup={}w:_stop()return w end function q.catchup(w)for O,T in pairs(w._entities)do w:_trigger("\111\110\065\100\100\101\100",T)end return w end function q.onInterval(T,w,O)table.insert(T._intervals,{interval=w;hook=O})if#T._entities>0 then T:_start()end return T end function q.onEvent(T,w,O)table.insert(T._events,{event=w;hook=O})if#T._entities>0 then T:_start()end return T end function q.get(w)return w._entities end function q.onComponentAdded(O,w)table.insert(O._hooks.onComponentAdded,w)return O end function q.onComponentRemoved(O,w)table.insert(O._hooks.onComponentRemoved,w)return O end function q.onComponentUpdated(O,w)table.insert(O._hooks.onComponentUpdated,w)return O end function q.onComponentParentUpdated(O,w)table.insert(O._hooks.onComponentParentUpdated,w)return O end return q
local AllSelector = require(script.Parent.Selectors.AllSelector)
local ComponentSelector = require(script.Parent.Selectors.ComponentSelector)
local Util = require(script.Parent.Selectors.Util)

local intervalSignal = game:GetService("RunService").Stepped

local System = setmetatable({}, AllSelector)
System.__index = System

-- TODO: look into logic of aggregate/component events

function System.new(rocs, scope, ...)
	local args = {...}

	local base
	if #args == 1 then
		if Util.inheritsBase(args[1]) then
			base = args[1]
		elseif type(args[1]) == "string" then
			base = ComponentSelector.new(rocs, args[1])
		end
	end

	local self = setmetatable(base or AllSelector.new(rocs, ...), System)

	self._entities = {} -- keeping track of what entities are in system
						-- self._lookup: [instance] = entity

	self._events = {} -- {Event = RbxScriptSignal, Hook = function, Connection = nil/RbxScriptConnection}
	self._intervals = {} -- {Interval = num, Hook = function, LastInvoke = tick()}
	self._intervalConnection = nil

	self._hooks.onComponentAdded = {}
	self._hooks.onComponentRemoved = {}
	self._hooks.onComponentUpdated = {}
	self._hooks.onComponentParentUpdated = {}

	self._scope = scope

	return self
end

function System:_stop()
	for _, entry in pairs(self._events) do
		if entry.connection then
			entry.connection:Disconnect()
			entry.connection = nil
		end
	end

	if self._intervalConnection then
		self._intervalConnection:Disconnect()
		self._intervalConnection = nil
	end
end

function System:_start()
	for _, entry in pairs(self.events) do
		if not entry.connection then
			entry.connection = entry.event:Connect(entry.hook)
		end
	end

	if not self._intervalConnection then
		self._intervalConnection = intervalSignal:Connect(
			function()
				for _, interval in pairs(self._intervals) do
					local lastInvoke = interval.lastInvoke
					local timestamp = tick()
					if not lastInvoke then
						interval.lastInvoke = timestamp
						interval.hook(0)
					elseif timestamp - lastInvoke >= interval.interval then
						interval.lastInvoke = timestamp
						interval.hook(timestamp - lastInvoke)
					end
				end
			end
		)
	end
end

function System:_listen() -- override
	for _, selector in pairs(self._selectors) do
		selector:onAdded(
			function(aggregate)
				local instance = aggregate.instance
				if self:check(instance, selector) then
					if not self._lookup[instance] then
						local entity = self._rocs:getEntity(instance, self._scope)
						table.insert(self._entities, entity)
						self._lookup[instance] = entity
						if #self._entities == 1 then
							self:_start()
						end
						self:_trigger("onAdded", entity)
					end
					self:_trigger("onComponentAdded", aggregate)
				end
			end
		)

		selector:onRemoved(
			function(aggregate)
				local instance = aggregate.instance
				if self._lookup[instance] then
					if not self:check(instance) then
						local entity = self._lookup[instance]
						self._lookup[instance] = nil
						for key, value in pairs(self._entities) do
							if value == entity then
								table.remove(self._entities, key)
								break
							end
						end
						if #self._entities == 0 then
							self:_stop()
						end
						self:_trigger("onRemoved", entity)
					end
					self:_trigger("onComponentRemoved", aggregate)
				end
			end
		)

		-- TODO: is this right?
		selector:onUpdated(
			function(aggregate)
				local entity = self._lookup[aggregate.instance]
				if entity then
					self:_trigger("onUpdated", entity)
					self:_trigger("onComponentUpdated", aggregate)
				end
			end
		)

		-- TODO: is this right?
		selector:onParentUpdated(
			function(aggregate)
				local entity = self._lookup[aggregate.instance]
				if entity then
					self:_trigger("onParentUpdated", entity)
					self:_trigger("onComponentParentUpdated", aggregate)
				end
			end
		)
	end
end

function System:setup() -- override
	if self._ready then
		return
	end
	self._ready = true

	self:_listen()

	for _, selector in pairs(self._selectors) do
		selector:setup()
	end

	for _, instance in pairs(self:instances()) do
		local entity = self._rocs:getEntity(instance, self._scope)
		table.insert(self._entities, entity)
		self._lookup[instance] = entity
	end

	if #self._entities > 0 then
		self:_start()
	end

	return self
end

function System:destroy() -- override
	if not self._ready then
		return
	end
	self._ready = nil

	for category, _ in pairs(self._hooks) do
		self._hooks[category] = {}
	end

	for _, selector in pairs(self._selectors) do
		selector:destroy()
	end

	self._entities = {}
	self._lookup = {}

	self:_stop()

	return self
end

function System:catchup()
	for _, entity in pairs(self._entities) do
		self:_trigger("onAdded", entity)
	end
	return self
end

function System:onInterval(interval, hook)
	table.insert(self._intervals, {
		interval = interval,
		hook = hook
	})

	if #self._entities > 0 then
		self:_start()
	end

	return self
end

function System:onEvent(event, hook)
	table.insert(self._events, {
		event = event,
		hook = hook
	})

	if #self._entities > 0 then
		self:_start()
	end

	return self
end

function System:get()
	return self._entities -- TODO: doc that user should not modify
end

function System:onComponentAdded(hook)
	table.insert(self._hooks.onComponentAdded, hook)
	return self
end

function System:onComponentRemoved(hook)
	table.insert(self._hooks.onComponentRemoved, hook)
	return self
end

function System:onComponentUpdated(hook)
	table.insert(self._hooks.onComponentUpdated, hook)
	return self
end

function System:onComponentParentUpdated(hook)
	table.insert(self._hooks.onComponentParentUpdated, hook)
	return self
end

return System
