--[=[
	@param instance Instance
	@param componentClass ComponentClass
	@return Component?

	Gets an instance of a component class given the Roblox instance
	and the component class. Returns `nil` if not found.

	```lua
	local MyComponent = require(somewhere.MyComponent)

	local myComponentInstance = Component.FromInstance(workspace.SomeInstance, MyComponent)
	```
]=]


function Component.FromInstance(instance: Instance, componentClass)
	return componentClass._instancesToComponents[instance]
end


function Component:_instantiate(instance: Instance)
	local component = setmetatable({}, self)
	component.Instance = instance
	if not ShouldConstruct(component, self._extensions) then
		return nil
	end
	InvokeExtensionFn(component, self._extensions, "Constructing")
	if type(component.Construct) == "function" then
		component:Construct()
	end
	InvokeExtensionFn(component, self._extensions, "Constructed")
	return component
end


function Component:_setup()
	
	local watchingInstances = {}
	
	local function StartComponent(component)
		InvokeExtensionFn(component, self._extensions, "Starting")
		component:Start()
		InvokeExtensionFn(component, self._extensions, "Started")
		local hasHeartbeatUpdate = typeof(component.HeartbeatUpdate) == "function"
		local hasSteppedUpdate = typeof(component.SteppedUpdate) == "function"
		local hasRenderSteppedUpdate = typeof(component.RenderSteppedUpdate) == "function"
		if hasHeartbeatUpdate then
			component._heartbeatUpdate = RunService.Heartbeat:Connect(function(dt)
				component:HeartbeatUpdate(dt)
			end)
		end
		if hasSteppedUpdate then
			component._steppedUpdate = RunService.Stepped:Connect(function(_, dt)
				component:SteppedUpdate(dt)
			end)
		end
		if hasRenderSteppedUpdate and not IS_SERVER then
			if component.RenderPriority then
				self._renderName = NextRenderName()
				RunService:BindToRenderStep(self._renderName, component.RenderPriority, function(dt)
					component:RenderSteppedUpdate(dt)
				end)
			else
				component._renderSteppedUpdate = RunService.RenderStepped:Connect(function(dt)
					component:RenderSteppedUpdate(dt)
				end)
			end
		end
		component._started = true
		self.Started:Fire(component)
	end
	
	local function StopComponent(component)
		if component._heartbeatUpdate then
			component._heartbeatUpdate:Disconnect()
		end
		if component._steppedUpdate then
			component._steppedUpdate:Disconnect()
		end
		if component._renderSteppedUpdate then
			component._renderSteppedUpdate:Disconnect()
		elseif component._renderName then
			RunService:UnbindFromRenderStep(self._renderName)
		end
		InvokeExtensionFn(component, self._extensions, "Stopping")
		component:Stop()
		InvokeExtensionFn(component, self._extensions, "Stopped")
		self.Stopped:Fire(component)
	end
	
	local function TryConstructComponent(instance)
		if self._instancesToComponents[instance] then return end
		local component = self:_instantiate(instance)
		if not component then
			return
		end
		self._instancesToComponents[instance] = component
		table.insert(self._components, component)
		task.defer(function()
			if self._instancesToComponents[instance] == component then
				StartComponent(component)
			end
		end)
	end
	
	local function TryDeconstructComponent(instance)
		local component = self._instancesToComponents[instance]
		if not component then return end
		self._instancesToComponents[instance] = nil
		local index = table.find(self._components, component)
		if index then
			local n = #self._components
			self._components[index] = self._components[n]
			self._components[n] = nil
		end
		if component._started then
			task.spawn(StopComponent, component)
		end
	end
	
	local function StartWatchingInstance(instance)
		if watchingInstances[instance] then return end
		local function IsInAncestorList(): boolean
			for _,parent in ipairs(self._ancestors) do
				if instance:IsDescendantOf(parent) then
					return true
				end
			end
			return false
		end
		local ancestryChangedHandle = self._trove:Connect(instance.AncestryChanged, function(_, parent)
			if parent and IsInAncestorList() then
				TryConstructComponent(instance)
			else
				TryDeconstructComponent(instance)
			end
		end)
		watchingInstances[instance] = ancestryChangedHandle
		if IsInAncestorList() then
			TryConstructComponent(instance)
		end
	end
	
	local function InstanceTagged(instance: Instance)
		StartWatchingInstance(instance)
	end
	
	local function InstanceUntagged(instance: Instance)
		local watchHandle = watchingInstances[instance]
		if watchHandle then
			watchHandle:Disconnect()
			watchingInstances[instance] = nil
		end
		TryDeconstructComponent(instance)
	end
	
	self._trove:Connect(CollectionService:GetInstanceAddedSignal(self.Tag), InstanceTagged)
	self._trove:Connect(CollectionService:GetInstanceRemovedSignal(self.Tag), InstanceUntagged)
	
	local tagged = CollectionService:GetTagged(self.Tag)
	for _,instance in ipairs(tagged) do
		task.defer(InstanceTagged, instance)
	end
	
end