-- Event that fires when a new point is snapped
PointSnapped = Signal.new()

function StartSnapping()

	-- Make sure snapping isn't already enabled
	if SnappingStage or SnapTracking.Enabled then
		return;
	end;

	-- Start first snapping stage
	SnappingStage = 'Starting';

	-- Only enable corner snapping
	SnapTracking.TrackEdgeMidpoints = false;
	SnapTracking.TrackFaceCentroids = false;
	SnapTracking.TargetFilter = function (Target)
		return Selection.PartIndex[Target]
	end

	-- Trigger the PointSnapped event when a new point is snapped
	SnapTracking.StartTracking(function (NewPoint)
		if NewPoint and NewPoint.p ~= SnappedPoint then
			SnappedPoint = NewPoint.p;
			PointSnapped:Fire(NewPoint.p);
		end;
	end);

	-- Listen for when the user starts dragging while in snap mode
	Connections.SnapDragStart = Support.AddUserInputListener('Began', 'MouseButton1', false, function (Input)

		-- Initialize snapping state
		SnappingStage = 'Direction';
		SnappingStartAim = Vector2.new(Input.Position.X, Input.Position.Y);
		SnappingStartPoint = SnappedPoint;
		SnappingStartTarget = SnapTracking.Target;
		SnappingStartDirections = GetFaceOffsetsFromCorner(SnappingStartTarget, SnappingStartPoint);
		SnappingStartSelectionState = PreparePartsForResizing();
		AreaPermissions = Security.GetPermissions(Security.GetSelectionAreas(Selection.Parts), Core.Player);

		-- Pause snapping
		SnapTracking.StopTracking();

		-- Start a direction line
		DirectionLine = Core.Tool.Interfaces.SnapLine:Clone();
		DirectionLine.Parent = Core.UI;
		DirectionLine.Visible = false;

		-- Track changes for history
		TrackChange();

		-- Listen for when the user drags
		Connections.SnapDrag = Support.AddUserInputListener('Changed', 'MouseMovement', true, function (Input)

			-- Update the latest aim
			SnappingEndAim = Vector2.new(Input.Position.X, Input.Position.Y);
			ScreenSnappedPoint = Workspace.CurrentCamera:WorldToScreenPoint(SnappingStartPoint);
			ScreenSnappedPoint = Vector2.new(ScreenSnappedPoint.X, ScreenSnappedPoint.Y);

			-- Calculate direction setting length
			local DirectionSettingLength = math.min(50, math.max(50, (SnappingStartAim - ScreenSnappedPoint).magnitude * 1.5));

			-- Use the mouse position to figure out the resize direction (until after direction setting length)
			if SnappingStage == 'Direction' then

				-- Get current angle from snap point
				local DragAngle = math.deg(math.atan2(SnappingEndAim.Y - ScreenSnappedPoint.Y, SnappingEndAim.X - ScreenSnappedPoint.X));
				DragAngle = (DragAngle > 0) and (DragAngle - 360) or DragAngle;

				-- Go through corner offsets representing the possible directions
				local Directions = {};
				for _, Direction in pairs(SnappingStartDirections) do

					-- Map the corner & corner offset to screen points
					local ScreenOffsetPoint = Workspace.CurrentCamera:WorldToScreenPoint(Direction.Offset);

					-- Get direction angle from snap point
					local DirectionAngle = math.deg(math.atan2(ScreenOffsetPoint.Y - ScreenSnappedPoint.Y, ScreenOffsetPoint.X - ScreenSnappedPoint.X));
					DirectionAngle = (DirectionAngle > 0) and (DirectionAngle - 360) or DirectionAngle;

					-- Calculate delta between drag and direction angles
					local AngleDelta = math.abs(DragAngle - DirectionAngle) % 180;
					AngleDelta = (AngleDelta > 90) and (180 - AngleDelta) or AngleDelta;

					-- Insert the potential direction
					table.insert(Directions, {
						Face = Direction.Face,
						AngleDelta = AngleDelta,
						DirectionAngle = DirectionAngle,
						Offset = Direction.Offset
					});

				end;

				-- Get the direction most similar to the dragging angle
				table.sort(Directions, function (A, B)
					return A.AngleDelta < B.AngleDelta;
				end);

				-- Center direction line at snap point
				DirectionLine.Position = UDim2.new(0, ScreenSnappedPoint.X, 0, ScreenSnappedPoint.Y);

				-- Orient direction line towards drag direction
				if math.abs(DragAngle - Directions[1].DirectionAngle) <= 90 then
					DirectionLine.Rotation = Directions[1].DirectionAngle;
				else
					DirectionLine.Rotation = 180 + Directions[1].DirectionAngle;
				end;

				-- Show the direction line
				DirectionLine.PointMarker.Rotation = -DirectionLine.Rotation;
				DirectionLine.SnapProgress.Size = UDim2.new(0, DirectionSettingLength, 2, 0);
				DirectionLine.Visible = true;

				-- Check if drag has passed direction setting length
				local Length = (SnappingEndAim - ScreenSnappedPoint).magnitude;
				if Length < DirectionSettingLength then
					return;
				end;

				-- Clear the direction line
				DirectionLine:Destroy()

				-- Select the resizing direction that was closest to the mouse drag
				SnappingDirection = Directions[1].Face;
				SnappingDirectionOffset = Directions[1].Offset;

				-- Move to the destination-picking stage of snapping
				SnappingStage = 'Destination';

				-- Set destination-stage snapping options
				SnapTracking.TrackEdgeMidpoints = true;
				SnapTracking.TrackFaceCentroids = true;
				SnapTracking.TargetFilter = function (Target) return not Target.Locked; end;
				SnapTracking.TargetBlacklist = Selection.Items;

				-- Start a distance alignment line
				AlignmentLine = Core.Tool.Interfaces.SnapLineSegment:Clone();
				AlignmentLine.Visible = false;
				AlignmentLine.Parent = Core.UI;

				-- Re-enable snapping to select destination
				SnapTracking.StartTracking(function (NewPoint)
					if NewPoint and NewPoint.p ~= SnappedPoint then
						SnappedPoint = NewPoint.p;
						PointSnapped:Fire(NewPoint.p);
					end;
				end);

			end;

		end);

		-- Listen for when a new point is snapped
		Connections.Snap = PointSnapped:Connect(function (SnappedPoint)

			-- Resize to snap point if in the destination stage of snapping
			if SnappingStage == 'Destination' then

				-- Calculate direction and distance to resize towards
				local Direction = (SnappingDirectionOffset - SnappingStartPoint).unit;
				local Distance = (SnappedPoint - SnappingStartPoint):Dot(Direction);

				-- Resize the parts on the selected faces by the calculated distance
				local Success = ResizePartsByFace(SnappingDirection, Distance, 'Normal', SnappingStartSelectionState);

				-- Update the UI on resize success
				if Success then

					-- Update "studs resized" indicator
					if ResizeTool.UI then
						ResizeTool.UI.Changes.Text.Text = 'resized ' .. Support.Round(Distance, 3) .. ' studs';
					end;

					-- Get snap point and destination point screen positions for UI alignment
					local ScreenStartPoint = Workspace.CurrentCamera:WorldToScreenPoint(SnappingStartPoint + (Direction * Distance));
					ScreenStartPoint = Vector2.new(ScreenStartPoint.X, ScreenStartPoint.Y);
					local ScreenDestinationPoint = Workspace.CurrentCamera:WorldToScreenPoint(SnappedPoint);
					ScreenDestinationPoint = Vector2.new(ScreenDestinationPoint.X, ScreenDestinationPoint.Y)

					-- Update the distance alignment line
					local AlignmentAngle = math.deg(math.atan2(ScreenDestinationPoint.Y - ScreenStartPoint.Y, ScreenDestinationPoint.X - ScreenStartPoint.X));
					local AlignmentCenter = ScreenStartPoint:Lerp(ScreenDestinationPoint, 0.5);
					AlignmentLine.Position = UDim2.new(0, AlignmentCenter.X, 0, AlignmentCenter.Y);
					AlignmentLine.Rotation = AlignmentAngle;
					AlignmentLine.Size = UDim2.new(0, (ScreenDestinationPoint - ScreenStartPoint).magnitude, 0, 1);
					AlignmentLine.PointMarkerA.Rotation = -AlignmentAngle;
					AlignmentLine.Visible = true;

				end;

				-- Make sure we're not entering any unauthorized private areas
				if Core.Mode == 'Tool' and Security.ArePartsViolatingAreas(Selection.Parts, Core.Player, false, AreaPermissions) then
					for Part, State in pairs(SnappingStartSelectionState) do
						Part.Size = State.Size;
						Part.CFrame = State.CFrame;
					end;
				end;

			end;

		end);

	end);

end;