-- Define target box cleanup routine
function TargetBoxPool.Cleanup(TargetBox)
	TargetBox.Adornee = nil
	TargetBox.Visible = nil
end

function TargetingModule.HighlightTarget(Target)

	-- Clear previous target boxes
	TargetBoxPool:ReleaseAll()

	-- Make sure target exists
	if not Target then
		return
	end

	-- Get targetable items
	local Items = Support.FlipTable { Target }
	if not IsVisible(Target) then
		Items = Support.FlipTable(GetVisibleChildren(Target))
	end

	-- Focus target boxes on target
	for Target in pairs(Items) do
		local TargetBox = TargetBoxPool:Get()
		TargetBox.Adornee = Target
		TargetBox.Visible = true
	end

end;

local function IsAncestorSelected(Item)
	while Item and (Item ~= TargetingModule.Scope) do
		if Selection.IsSelected(Item) then
			return true
		else
			Item = Item.Parent
		end
	end
end

function TargetingModule.SelectTarget(Force)
	local Scope = TargetingModule.Scope

	-- Update target
	local Target, ScopeTarget = TargetingModule:UpdateTarget(Scope, true)

	-- Ensure target selection isn't cancelled
	if not Force and SelectionCancelled then
		SelectionCancelled = false;
		return;
	end;

	-- Focus on clicked, selected item
	if not Selection.Multiselecting and (Selection.IsSelected(ScopeTarget) or IsAncestorSelected(ScopeTarget)) then
		Selection.SetFocus(ScopeTarget)
		return;
	end;

	-- Clear selection if invalid target selected
	if not GetCore().IsSelectable({ Target }) then
		Selection.Clear(true);
		return;
	end;

	-- Unselect clicked, selected item if multiselection is enabled
	if Selection.Multiselecting and Selection.IsSelected(ScopeTarget) then
		Selection.Remove({ ScopeTarget }, true)
		return
	end

	-- Add to selection if multiselecting
	if Selection.Multiselecting then
		Selection.Add({ ScopeTarget }, true)
		Selection.SetFocus(ScopeTarget)

	-- Replace selection if not multiselecting
	else
		Selection.Replace({ ScopeTarget }, true)
		Selection.SetFocus(ScopeTarget)
	end

end;

function TargetingModule.SelectSiblings(Part, ReplaceSelection)
	-- Selects all parts under the same parent as `Part`

	-- If a part is not specified, assume the currently focused part
	local Part = Part or Selection.Focus;

	-- Ensure the part exists and its parent is not Workspace
	if not Part or Part.Parent == TargetingModule.Scope then
		return;
	end;

	-- Get the focused item's siblings
	local Siblings = Support.GetDescendantsWhichAreA(Part.Parent, 'BasePart')

	-- Ensure items are selectable
	if not GetCore().IsSelectable(Siblings) then
		return
	end

	-- Add to or replace selection
	if ReplaceSelection then
		Selection.Replace(Siblings, true);
	else
		Selection.Add(Siblings, true);
	end;

end;

function TargetingModule.StartRectangleSelecting()

	-- Ensure selection isn't cancelled
	if SelectionCancelled then
		return;
	end;

	-- Mark where rectangle selection started
	RectangleSelectStart = Vector2.new(Mouse.X, Mouse.Y);

	-- Track mouse while rectangle selecting
	GetCore().Connections.WatchRectangleSelection = Mouse.Move:Connect(function ()

		-- If rectangle selecting, update rectangle
		if RectangleSelecting then
			TargetingModule.UpdateSelectionRectangle();

		-- Watch for potential rectangle selections
		elseif RectangleSelectStart and (Vector2.new(Mouse.X, Mouse.Y) - RectangleSelectStart).magnitude >= 10 then
			RectangleSelecting = true;
			SelectionCancelled = true;
		end;

	end);

end;

function TargetingModule.UpdateSelectionRectangle()

	-- Ensure rectangle selection is ongoing
	if not RectangleSelecting then
		return;
	end;

	-- Get core API
	local Core = GetCore();

	-- Create selection rectangle
	if not SelectionRectangle then
		SelectionRectangle = Make 'Frame' {
			Name = 'SelectionRectangle',
			Parent = Core.UI,
			BackgroundColor3 = Color3.fromRGB(100, 100, 100),
			BorderColor3 = Color3.new(0, 0, 0),
			BackgroundTransparency = 0.5,
			BorderSizePixel = 1
		};
	end;

	local StartPoint = Vector2.new(
		math.min(RectangleSelectStart.X, Mouse.X),
		math.min(RectangleSelectStart.Y, Mouse.Y)
	);
	local EndPoint = Vector2.new(
		math.max(RectangleSelectStart.X, Mouse.X),
		math.max(RectangleSelectStart.Y, Mouse.Y)
	);

	-- Update size and position
	SelectionRectangle.Parent = Core.UI;
	SelectionRectangle.Position = UDim2.new(0, StartPoint.X, 0, StartPoint.Y);
	SelectionRectangle.Size = UDim2.new(0, EndPoint.X - StartPoint.X, 0, EndPoint.Y - StartPoint.Y);

end;

function TargetingModule.CancelRectangleSelecting()

	-- Prevent potential rectangle selections
	RectangleSelectStart = nil;

	-- Clear ongoing rectangle selection
	RectangleSelecting = false;

	-- Clear rectangle selection watcher
	local Connections = GetCore().Connections;
	if Connections.WatchRectangleSelection then
		Connections.WatchRectangleSelection:Disconnect();
		Connections.WatchRectangleSelection = nil;
	end;

	-- Clear rectangle UI
	if SelectionRectangle then
		SelectionRectangle.Parent = nil;
	end;

end;

function TargetingModule.CancelSelecting()
	SelectionCancelled = true;
	TargetingModule.CancelRectangleSelecting();
end;

function TargetingModule.FinishRectangleSelecting()
	local Core = GetCore()

	local RectangleSelecting = RectangleSelecting;
	local RectangleSelectStart = RectangleSelectStart;

	-- Clear rectangle selection
	TargetingModule.CancelRectangleSelecting();

	-- Ensure rectangle selection is ongoing
	if not RectangleSelecting then
		return;
	end;

	-- Ensure a targeting scope is set
	if not TargetingModule.Scope then
		return
	end

	-- Get rectangle dimensions
	local StartPoint = Vector2.new(
		math.min(RectangleSelectStart.X, Mouse.X),
		math.min(RectangleSelectStart.Y, Mouse.Y)
	);
	local EndPoint = Vector2.new(
		math.max(RectangleSelectStart.X, Mouse.X),
		math.max(RectangleSelectStart.Y, Mouse.Y)
	);

	local SelectableItems = {};

	-- Find items that lie within the rectangle
	local ScopeParts = Support.GetDescendantsWhichAreA(TargetingModule.Scope, 'BasePart')
	for _, Part in pairs(ScopeParts) do
		local ScreenPoint, OnScreen = Workspace.CurrentCamera:WorldToScreenPoint(Part.Position)
		if OnScreen then
			local LeftCheck = ScreenPoint.X >= StartPoint.X
			local RightCheck = ScreenPoint.X <= EndPoint.X
			local TopCheck = ScreenPoint.Y >= StartPoint.Y
			local BottomCheck = ScreenPoint.Y <= EndPoint.Y
			if (LeftCheck and RightCheck and TopCheck and BottomCheck) and Core.IsSelectable({ Part }) then
				local ScopeTarget = TargetingModule:FindTargetInScope(Part, TargetingModule.Scope)
				SelectableItems[ScopeTarget] = true
			end
		end
	end

	-- Add to selection if multiselecting
	if Selection.Multiselecting then
		Selection.Add(Support.Keys(SelectableItems), true)

	-- Replace selection if not multiselecting
	else
		Selection.Replace(Support.Keys(SelectableItems), true)
	end;

end;

function TargetingModule.PrismSelect()
	-- Selects parts in the currently selected parts

	-- Ensure parts are selected
	if #Selection.Items == 0 then
		return;
	end;

	-- Get core API
	local Core = GetCore();

	-- Get region for selection items and find potential parts
	local Extents = require(Core.Tool.Core.BoundingBox).CalculateExtents(Selection.Items, nil, true);
	local Region = Region3.new(Extents.Min, Extents.Max);
	local PotentialParts = Workspace:FindPartsInRegion3WithIgnoreList(Region, Selection.Items, math.huge);

	-- Enable collision on all potential parts
	local OriginalState = {};
	for _, PotentialPart in pairs(PotentialParts) do
		OriginalState[PotentialPart] = { Anchored = PotentialPart.Anchored, CanCollide = PotentialPart.CanCollide };
		PotentialPart.Anchored = true;
		PotentialPart.CanCollide = true;
	end;

	local Parts = {};

	-- Find all parts intersecting with selection
	for _, Part in pairs(Selection.Items) do
		local TouchingParts = Part:GetTouchingParts();
		for _, TouchingPart in pairs(TouchingParts) do
			if not Selection.IsSelected(TouchingPart) then
				Parts[TouchingPart] = true;
			end;
		end;
	end;

	-- Restore all potential parts' original states
	for PotentialPart, State in pairs(OriginalState) do
		PotentialPart.CanCollide = State.CanCollide;
		PotentialPart.Anchored = State.Anchored;
	end;

	-- Delete the selection parts
	Core.DeleteSelection();

	-- Select all found parts
	Selection.Replace(Support.Keys(Parts), true);

end;

function TargetingModule:EnableScopeSelection()
	-- Enables the scope selection interface

	-- Set up state
	local Scoping = false
	local InitialScope = nil

	local function HandleScopeInput(Action, State, Input)
		if State.Name == 'Begin' then
			local IsAltPressed = UserInputService:IsKeyDown 'LeftAlt' or
				UserInputService:IsKeyDown 'RightAlt'
			local IsShiftPressed = UserInputService:IsKeyDown 'LeftShift' or
				UserInputService:IsKeyDown 'RightShift'

			-- If Alt is pressed, begin scoping
			if (not Scoping) and (Input.KeyCode.Name:match 'Alt') then
				Scoping = self.Scope
				InitialScope = self.Scope

				-- Set new scope to current scope target
				local Target, ScopeTarget = self:UpdateTarget(self.Scope, true)
				if Target ~= ScopeTarget then
					self:SetScope(ScopeTarget or self.Scope)
					self:UpdateTarget(self.Scope, true)
					self.IsScopeLocked = false
					self.ScopeLockChanged:Fire(false)
					Scoping = self.Scope
				end

			-- If Alt-Shift-Z is pressed, exit current scope
			elseif Scoping and IsAltPressed and IsShiftPressed and (Input.KeyCode.Name == 'Z') then
				local NewScope = self.Scope.Parent or InitialScope
				if GetCore().Security.IsLocationAllowed(NewScope, GetCore().Player) then
					self:SetScope(NewScope)
					self:UpdateTarget(self.Scope, true)
					self.IsScopeLocked = false
					self.ScopeLockChanged:Fire(false)
					Scoping = self.Scope
				end
				return Enum.ContextActionResult.Sink

			-- If Alt-Z is pressed, enter scope of current target
			elseif Scoping and IsAltPressed and (Input.KeyCode.Name == 'Z') then
				local Target, ScopeTarget = self:UpdateTarget(self.Scope, true)
				if Target ~= ScopeTarget then
					self:SetScope(ScopeTarget or self.Scope)
					self:UpdateTarget(self.Scope, true)
					self.IsScopeLocked = false
					self.ScopeLockChanged:Fire(false)
					Scoping = self.Scope
				end
				return Enum.ContextActionResult.Sink

			-- If Alt-F is pressed, stay in current scope
			elseif Scoping and IsAltPressed and (Input.KeyCode.Name == 'F') then
				Scoping = true
				self.IsScopeLocked = true
				self.ScopeLockChanged:Fire(true)
				return Enum.ContextActionResult.Sink
			end

		-- Disable scoping on Alt release
		elseif State.Name == 'End' then
			if Scoping and (Input.KeyCode.Name:match 'Alt') then
				if self.Scope == Scoping then
					self:SetScope(InitialScope)
					self.IsScopeLocked = true
					self.ScopeLockChanged:Fire(true)
				end
				self:UpdateTarget(self.Scope, true)
				Scoping = nil
				InitialScope = nil
			end
		end

		-- If window focus changes, reset and disable scoping
		if Scoping and Input.UserInputType.Name == 'Focus' then
			if self.Scope == Scoping then
				self:SetScope(InitialScope)
				self.IsScopeLocked = true
				self.ScopeLockChanged:Fire(true)
			end
			self:UpdateTarget(self.Scope, true)
			Scoping = nil
			InitialScope = nil
		end

		-- Pass all non-sunken input to next handler
		return Enum.ContextActionResult.Pass
	end

	-- Enable scoping interface
	ContextActionService:BindAction('BT: Scope', HandleScopeInput, false,
		Enum.KeyCode.LeftAlt,
		Enum.KeyCode.RightAlt,
		Enum.KeyCode.Z,
		Enum.KeyCode.F,
		Enum.UserInputType.Focus
	)

	-- Disable scoping interface when tool disables
	GetCore().Disabling:Connect(function ()
		ContextActionService:UnbindAction('BT: Scope')
	end)

end

function TargetingModule:EnableScopeAutoReset()
	-- Enables automatic scope resetting (when scope becomes invalid)

	local LastScopeListener, LastScopeAncestry

	-- Listen to changes in scope
	GetCore().UIMaid.ScopeReset = self.ScopeChanged:Connect(function (Scope)

		-- Clear last scope listener
		LastScopeListener = LastScopeListener and LastScopeListener:Disconnect()

		-- Only listen to new scope if defined
		if not Scope then
			return
		end

		-- Capture new scope's ancestry
		LastScopeAncestry = {}
		local Ancestor = Scope.Parent
		while Ancestor:IsDescendantOf(Workspace) do
			table.insert(LastScopeAncestry, Ancestor)
			Ancestor = Ancestor.Parent
		end

		-- Reset scope when scope is gone
		LastScopeListener = Scope.AncestryChanged:Connect(function (_, Parent)
			if Parent == nil then

				-- Get next parent in ancestry
				local NextScopeInAncestry
				if LastScopeAncestry then
					for _, Parent in ipairs(LastScopeAncestry) do
						if Parent:IsDescendantOf(Workspace) then
							NextScopeInAncestry = Parent
							break
						end
					end
				end

				-- Set next scope
				if NextScopeInAncestry then
					self:SetScope(NextScopeInAncestry)
				else
					self:SetScope(Workspace, true)
				end

			-- Capture scope ancestry when it changes
			else
				LastScopeAncestry = {}
				local Ancestor = Scope.Parent
				while Ancestor:IsDescendantOf(Workspace) do
					table.insert(LastScopeAncestry, Ancestor)
					Ancestor = Ancestor.Parent
				end
			end
		end)

	end)
end