--[=[
	@param tag string
	@param class table
	@param renderPriority number?
	@param requireComponents {string}?
	@return Component

	Constructs a new component class.
]=]


function Component.new(tag, class, renderPriority, requireComponents)

	assert(type(tag) == "string", "Argument #1 (tag) should be a string; got " .. type(tag))
	assert(type(class) == "table", "Argument #2 (class) should be a table; got " .. type(class))
	assert(type(class.new) == "function", "Class must contain a .new constructor function")
	assert(type(class.Destroy) == "function", "Class must contain a :Destroy function")
	assert(componentsByTag[tag] == nil, "Component already bound to this tag")

	local self = setmetatable({}, Component)

	self._trove = Trove.new()
	self._lifecycleTrove = self._trove:Construct(Trove)
	self._tag = tag
	self._class = class
	self._objects = {}
	self._instancesToObjects = {}
	self._hasHeartbeatUpdate = (type(class.HeartbeatUpdate) == "function")
	self._hasSteppedUpdate = (type(class.SteppedUpdate) == "function")
	self._hasRenderUpdate = (type(class.RenderUpdate) == "function")
	self._hasInit = (type(class.Init) == "function")
	self._hasDeinit = (type(class.Deinit) == "function")
	self._renderPriority = renderPriority or Enum.RenderPriority.Last.Value
	self._requireComponents = requireComponents or {}
	self._lifecycle = false
	self._nextId = 0

	self.Added = self._trove:Construct(Signal)
	self.Removed = self._trove:Construct(Signal)

	local observeTrove = self._trove:Construct(Trove)

	local function ObserveTag()

		local function HasRequiredComponents(instance)
			for _,reqComp in ipairs(self._requireComponents) do
				local comp = Component.FromTag(reqComp)
				if comp:GetFromInstance(instance) == nil then
					return false
				end
			end
			return true
		end

		observeTrove:Connect(CollectionService:GetInstanceAddedSignal(tag), function(instance)
			if IsDescendantOfWhitelist(instance) and HasRequiredComponents(instance) then
				self:_instanceAdded(instance)
			end
		end)

		observeTrove:Connect(CollectionService:GetInstanceRemovedSignal(tag), function(instance)
			self:_instanceRemoved(instance)
		end)

		for _,reqComp in ipairs(self._requireComponents) do
			local comp = Component.FromTag(reqComp)
			observeTrove:Connect(comp.Added, function(obj)
				if CollectionService:HasTag(obj.Instance, tag) and HasRequiredComponents(obj.Instance) then
					self:_instanceAdded(obj.Instance)
				end
			end)
			observeTrove:Connect(comp.Removed, function(obj)
				if CollectionService:HasTag(obj.Instance, tag) then
					self:_instanceRemoved(obj.Instance)
				end
			end)
		end

		observeTrove:Add(function()
			self:_stopLifecycle()
			for instance in pairs(self._instancesToObjects) do
				self:_instanceRemoved(instance)
			end
		end)

		do
			for _,instance in ipairs(CollectionService:GetTagged(tag)) do
				if IsDescendantOfWhitelist(instance) and HasRequiredComponents(instance) then
					task.defer(function()
						self:_instanceAdded(instance)
					end)
				end
			end
		end

	end

	if #self._requireComponents == 0 then
		ObserveTag()
	else
		-- Only observe tag when all required components are available:
		local tagsReady = {}
		local function Check()
			for _,ready in pairs(tagsReady) do
				if not ready then
					return
				end
			end
			ObserveTag()
		end
		local function Cleanup()
			observeTrove:Clean()
		end
		for _,requiredComponent in ipairs(self._requireComponents) do
			tagsReady[requiredComponent] = false
		end
		for _,requiredComponent in ipairs(self._requireComponents) do
			self._trove:Add(Component.ObserveFromTag(requiredComponent, function(_component, trove)
				tagsReady[requiredComponent] = true
				Check()
				trove:Add(function()
					tagsReady[requiredComponent] = false
					Cleanup()
				end)
			end))
		end
	end

	componentsByTag[tag] = self
	componentByTagCreated:Fire(self)
	self._trove:Add(function()
		componentsByTag[tag] = nil
		componentByTagDestroyed:Fire(self)
	end)

	return self

end


function Component:_startHeartbeatUpdate()
	local all = self._objects
	self._heartbeatUpdate = RunService.Heartbeat:Connect(function(dt)
		for _,v in ipairs(all) do
			v:HeartbeatUpdate(dt)
		end
	end)
	self._lifecycleTrove:Add(self._heartbeatUpdate)
end


function Component:_startSteppedUpdate()
	local all = self._objects
	self._steppedUpdate = RunService.Stepped:Connect(function(_, dt)
		for _,v in ipairs(all) do
			v:SteppedUpdate(dt)
		end
	end)
	self._lifecycleTrove:Add(self._steppedUpdate)
end


function Component:_startRenderUpdate()
	local all = self._objects
	self._renderName = (self._tag .. "RenderUpdate")
	RunService:BindToRenderStep(self._renderName, self._renderPriority, function(dt)
		for _,v in ipairs(all) do
			v:RenderUpdate(dt)
		end
	end)
	self._lifecycleTrove:Add(function()
		RunService:UnbindFromRenderStep(self._renderName)
	end)
end


function Component:_startLifecycle()
	self._lifecycle = true
	if self._hasHeartbeatUpdate then
		self:_startHeartbeatUpdate()
	end
	if self._hasSteppedUpdate then
		self:_startSteppedUpdate()
	end
	if self._hasRenderUpdate then
		self:_startRenderUpdate()
	end
end


function Component:_stopLifecycle()
	self._lifecycle = false
	self._lifecycleTrove:Clean()
end


function Component:_instanceAdded(instance)
	if self._instancesToObjects[instance] then return end
	if not self._lifecycle then
		self:_startLifecycle()
	end
	self._nextId = (self._nextId + 1)
	local id = (self._tag .. tostring(self._nextId))
	if IS_SERVER then
		instance:SetAttribute(ATTRIBUTE_ID_NAME, id)
	end
	local obj = self._class.new(instance)
	obj.Instance = instance
	obj._id = id
	self._instancesToObjects[instance] = obj
	table.insert(self._objects, obj)
	if self._hasInit then
		task.defer(function()
			if self._instancesToObjects[instance] ~= obj then return end
			obj:Init()
		end)
	end
	self.Added:Fire(obj)
	return obj
end


function Component:_instanceRemoved(instance)
	if not self._instancesToObjects[instance] then return end
	self._instancesToObjects[instance] = nil
	for i,obj in ipairs(self._objects) do
		if obj.Instance == instance then
			if self._hasDeinit then
				obj:Deinit()
			end
			if IS_SERVER and instance.Parent and instance:GetAttribute(ATTRIBUTE_ID_NAME) ~= nil then
				instance:SetAttribute(ATTRIBUTE_ID_NAME, nil)
			end
			self.Removed:Fire(obj)
			obj:Destroy()
			obj._destroyed = true
			TableUtil.SwapRemove(self._objects, i)
			break
		end
	end
	if #self._objects == 0 and self._lifecycle then
		self:_stopLifecycle()
	end
end