local DraggerFramework = script.Parent.Parent
local Utility = DraggerFramework.Utility

local SelectionInfo = require(Utility.SelectionInfo)
type SelectionInfo = SelectionInfo.Class

--[[
	Ignored handle hits: When the ToolImplementation's shouldBiasTowardsObject
	function returns true, then this function will decide whether to ignore
	handle clicks where the user clicked outside of the handle's visual, but
	still within the handle's invisible hitbox.
]]

local function isIgnoredHandleHit(handles: any, mouseRay: Ray, selectionInfo: any, hitItem: Instance?)
	if not handles:shouldBiasTowardsObjects() then
		-- Only potentially ignores if we're biased towards parts
		return false
	end

	if not hitItem or not selectionInfo:doesContainItem(hitItem) then
		-- Only bias towards parts when clicking on something in the selection
		return false
	end

	-- Ignore the hit if when ignoring the extra threshold the handle is no
	-- longer hit.
	local ignoreExtraThreshold = true
	return handles:hitTest(mouseRay, ignoreExtraThreshold) == nil
end

local HoverTracker = {}
HoverTracker.__index = HoverTracker

type HoverTrackerData = {
	_draggerSchema: any,
	_handlesList: { any },
	_hoverHandleId: string?,
	_hoverItem: Instance?,
	_hoverSelectable: Instance?,
	_hoverDistance: number?,
	_hoverPosition: Vector3?,
	_hoverHandles: any?,
	_onHoverChanged: (() -> ())?,
	_hoverEscapeDetector: any?,
}

type Proto = typeof(HoverTracker)
export type Class = setmetatable<HoverTrackerData, Proto>

function HoverTracker.new(draggerSchema: any, handlesList: { any }, onHoverExternallyChangedFunction: () -> ()): Class
	assert(type(handlesList) == "table")
	assert(type(draggerSchema) == "table")

	return setmetatable({
		_draggerSchema = draggerSchema,
		_handlesList = handlesList,
		_hoverHandleId = nil,
		_hoverItem = nil,
		_onHoverChanged = onHoverExternallyChangedFunction,
	}, HoverTracker) :: Class
end

local function isCloser(distance: number, isOnTop: boolean?, currentDistance: number, currentIsOnTop: boolean)
	if currentIsOnTop then -- Neat logic
		return isOnTop and distance < currentDistance
	else
		return isOnTop or distance < currentDistance
	end
end

function HoverTracker.update(self: Class, draggerContext: any, currentSelection: {Instance}, selectionInfo: any)
	assert(currentSelection ~= nil)
	local oldHoverSelectable = self._hoverSelectable

	-- Hover parts in the workspace
	local mouseRay = draggerContext:getMouseRay()
	local hitSelectable, hitItem, distanceToHover = self._draggerSchema.getMouseTarget(draggerContext, mouseRay, currentSelection)

	self._hoverItem = hitItem
	self._hoverSelectable = hitSelectable
	self._hoverHandleId = nil

	if hitSelectable ~= nil then
		self._hoverDistance = distanceToHover
		self._hoverPosition = mouseRay.Origin + mouseRay.Direction.Unit * distanceToHover
	else
		distanceToHover = math.huge
		self._hoverDistance = math.huge
		self._hoverPosition = nil
	end

	local currentIsOnTop = false
	self._hoverHandles = nil

	for _, handles in pairs(self._handlesList) do
		-- Possibly hover a handle instead if we have a handle closer than the part
		-- and the hit wasn't ignored by bias towards hovering parts.
		local hoverHandleId: string?, hoverHandleDistance: number?, isOnTop: boolean? = handles:hitTest(mouseRay, false)

		if hoverHandleId and hoverHandleDistance then
			if isCloser(hoverHandleDistance, isOnTop, distanceToHover, currentIsOnTop) and
				not isIgnoredHandleHit(handles, mouseRay, selectionInfo, hitItem) then
				self._hoverHandles = handles
				self._hoverHandleId = hoverHandleId
				self._hoverDistance = hoverHandleDistance
				self._hoverPosition = nil
				distanceToHover = hoverHandleDistance
				currentIsOnTop = isOnTop
			end
		end
	end

	if self._hoverHandles then
		self._draggerSchema.setHover(draggerContext, nil, nil)
	else
		self._draggerSchema.setHover(draggerContext, self._hoverSelectable, self._hoverItem)
	end

	if self._onHoverChanged and self._hoverSelectable ~= oldHoverSelectable then
		self:_freeHoverEscapeDetector()
		if self._hoverSelectable then
			self._hoverEscapeDetector = self._draggerSchema.HoverEscapeDetector.new(
				draggerContext, self._hoverSelectable, self._onHoverChanged
			)
		end
	end
end

function HoverTracker._freeHoverEscapeDetector(self: Class)
	if self._hoverEscapeDetector then
		self._hoverEscapeDetector:destroy()
		self._hoverEscapeDetector = nil
	end
end

function HoverTracker.clearHover(self: Class, draggerContext: any)
	self:_freeHoverEscapeDetector()
	self._hoverItem = nil
	self._hoverSelectable = nil
	self._hoverPosition = nil
	self._hoverHandles = nil
	self._hoverHandleId = nil
	self._hoverDistance = nil
	self._draggerSchema.setHover(draggerContext, nil, nil)
end

--[[
	Returns: The Id of the hovered handle, the distance to that handle
]]
function HoverTracker.getHoverHandleId(self: Class)
	return self._hoverHandles, self._hoverHandleId, self._hoverDistance
end

--[[
	Returns: The hovered instance, and the world position of the hit on it
]]
function HoverTracker.getHoverItem(self: Class)
	return self._hoverItem, self._hoverPosition
end

function HoverTracker.getHoverSelectable(self: Class)
	return self._hoverSelectable
end

return HoverTracker


local RunService = game:GetService("RunService")

local AttachmentMover = {}
AttachmentMover.__index = AttachmentMover

type AttachmentMover = {
	_originalWorldCFrames: {
		[Attachment]: CFrame,
	},

	_partsToUnanchor: {
		[BasePart]: true,
	},
}

type Proto = typeof(AttachmentMover)
export type Class = setmetatable<AttachmentMover, Proto>

function AttachmentMover.new(): Class
	return setmetatable({
		_originalWorldCFrames = {},
		_partsToUnanchor = {},
	}, AttachmentMover)
end

function AttachmentMover.setDragged(self: Class, attachments: { Attachment })
	local isRunning = RunService:IsRunning()
	table.clear(self._originalWorldCFrames)
	table.clear(self._partsToUnanchor)

	for _, attachment in ipairs(attachments) do
		self._originalWorldCFrames[attachment] = attachment.WorldCFrame

		if isRunning then
			local part = attachment:FindFirstAncestorWhichIsA("BasePart")

			if part and not part:IsGrounded() then
				self._partsToUnanchor[part] = true
			end
		end
	end

	for part in self._partsToUnanchor do
		part.Anchored = true
	end
end

function AttachmentMover.transformTo(self: Class, transform: CFrame)
	for attachment, originalWorldCFrame in pairs(self._originalWorldCFrames) do
		attachment.WorldCFrame = transform * originalWorldCFrame
	end
end

function AttachmentMover.commit(self: Class)
	table.clear(self._originalWorldCFrames)

	for part in self._partsToUnanchor do
		part.Anchored = false
	end

	table.clear(self._partsToUnanchor)
end

return AttachmentMover


local function getBoundingBoxInternal(cframe: CFrame, size: Vector3, inverseBasis: CFrame?)
	local localCFrame = if inverseBasis then inverseBasis * cframe else cframe
	local sx, sy, sz = size.X, size.Y, size.Z

	local _, _, _,
		t00, t01, t02,
		t10, t11, t12,
		t20, t21, t22 = localCFrame:GetComponents()

	local hw = 0.5 * (math.abs(sx * t00) + math.abs(sy * t01) + math.abs(sz * t02))
	local hh = 0.5 * (math.abs(sx * t10) + math.abs(sy * t11) + math.abs(sz * t12))
	local hd = 0.5 * (math.abs(sx * t20) + math.abs(sy * t21) + math.abs(sz * t22))
	local x, y, z = localCFrame.X, localCFrame.Y, localCFrame.Z

	local min = Vector3.new(x - hw, y - hh, z - hd)
	local max = Vector3.new(x + hw, y + hh, z + hd)

	return min, max
end

local BoundingBox = {}

--[[
	Returns the bounding box that contains the specified objects.

	The bounding box is computed in the given coordinate space, or world space if
	no local basis is provided.

	Params:
		table objects: An array of BaseParts, Models, and Attachments.
		CFrame maybeBasisCFrame: Optional local basis.

	Returns:
		Tuple (Vector3 offset, Vector3 size).
]]
function BoundingBox.fromObjects(objects: { Instance }, maybeBasisCFrame: CFrame?)
	local basisCFrame = maybeBasisCFrame or CFrame.identity
	local inverseBasis = basisCFrame:Inverse()
	local terrain = workspace.Terrain

	local max = -Vector3.one * math.huge
	local min = Vector3.one * math.huge

	if #objects == 0 then
		return Vector3.zero, Vector3.zero
	end

	for _, object in ipairs(objects) do
		if object:IsA("PVInstance") and object ~= terrain then
			local cframe = CFrame.identity
			local size = Vector3.zero

			if object:IsA("Model") then
				cframe, size = object:GetBoundingBox()
			elseif object:IsA("BasePart") then
				cframe = object.CFrame
				size = object.Size
			end

			local min1, max1 = getBoundingBoxInternal(cframe, size, inverseBasis)
			min = Vector3.min(min, min1)
			max = Vector3.max(max, max1)
		elseif object:IsA("Attachment") then
			local localPosition = basisCFrame:PointToObjectSpace(object.WorldPosition)
			min = Vector3.min(min, localPosition)
			max = Vector3.max(max, localPosition)
		end
	end

	local offset = 0.5 * (min + max)
	local size = max - min

	return offset, size
end

--[[
	Returns the bounding box that contains the specified objects, as well as the
	bounding boxes for all BaseParts and Models.

	Bounding boxes are computed in the given coordinate space, or world space if
	no local basis is provided.

	Params:
		table objects: An array of BaseParts, Models, and Attachments.
		CFrame basisCFrame: Optional local basis.

	Returns:
		Tuple (Vector3 offset, Vector3 size, table boundingBoxes).
]]
function BoundingBox.fromObjectsComputeAll(objects: { Instance }, maybeBasisCFrame: CFrame?)
	local basisCFrame = maybeBasisCFrame or CFrame.identity
	local inverseBasis = basisCFrame:Inverse()

	local max = -Vector3.one * math.huge
	local min = Vector3.one * math.huge

	local terrain = workspace.Terrain
	local boundingBoxes = {}

	for _, object in ipairs(objects) do
		if object:IsA("PVInstance") and object ~= terrain then
			local cframe = CFrame.identity
			local size = Vector3.zero

			if object:IsA("Model") then
				cframe, size = object:GetBoundingBox()
			elseif object:IsA("BasePart") then
				cframe = object.CFrame
				size = object.Size
			end

			local min1, max1 = getBoundingBoxInternal(cframe, size, inverseBasis)
			min = Vector3.min(min, min1)
			max = Vector3.max(max, max1)

			boundingBoxes[object] = {
				offset = 0.5 * (min1 + max1),
				size = max1 - min1
			}
		elseif object:IsA("Attachment") then
			local localPosition = basisCFrame:PointToObjectSpace(object.WorldPosition)
			min = Vector3.min(min, localPosition)
			max = Vector3.max(max, localPosition)
		end
	end

	if not next(boundingBoxes) then
		return Vector3.zero, Vector3.zero, boundingBoxes
	end

	local offset = 0.5 * (min + max)
	local size = max - min

	return offset, size, boundingBoxes
end

--[[
	Calculate an oriented bounding box for the passed in parts and attachments,
	in the supplied basis.
]]
function BoundingBox.fromPartsAndAttachments(parts: { BasePart }, attachments: { Attachment }, maybeBasisCFrame: CFrame?)
	if #parts == 0 and #attachments == 0 then
		return Vector3.zero, Vector3.zero
	end

	local basisCFrame = maybeBasisCFrame or CFrame.identity
	local inverseBasis = basisCFrame:Inverse()
	local terrain = workspace.Terrain

	local max = -Vector3.one * math.huge
	local min = Vector3.one * math.huge

	for _, part in ipairs(parts) do
		if part ~= terrain then
			local min1, max1 = getBoundingBoxInternal(part.CFrame, part.Size, inverseBasis)
			min = Vector3.min(min, min1)
			max = Vector3.max(max, max1)
		end
	end

	for _, attachment in ipairs(attachments) do
		local localPosition = basisCFrame:PointToObjectSpace(attachment.WorldPosition)
		min = Vector3.min(min, localPosition)
		max = Vector3.max(max, localPosition)
	end

	local offset = 0.5 * (min + max)
	local size = max - min

	return offset, size
end

--[[
	Micro-optimized code for computing a local space and the global space
	bounding box for a set of parts and attachments as fast as possible.
]]
function BoundingBox.computeTwoBoundingBoxes(parts: { BasePart }, attachments: { Attachment }, maybeBasisCFrame: CFrame?)
	if #parts == 0 and #attachments == 0 then
		return Vector3.zero, Vector3.zero, Vector3.zero, Vector3.zero
	end

	local basisCFrame = maybeBasisCFrame or CFrame.identity
	local inverseBasis = basisCFrame:Inverse()
	local basisPos = basisCFrame.Position

	local min1, max1 = Vector3.one * math.huge, -Vector3.one * math.huge
	local min2, max2 = Vector3.one * math.huge, -Vector3.one * math.huge
	local terrain = workspace.Terrain

	for _, part in ipairs(parts) do
		if part == terrain then
			continue
		end

		local size = part.Size
		local cframe = part.CFrame
		local sx, sy, sz = size.X, size.Y, size.Z

		-- Calculation for bounding box in the space of basisCFrame
		local localCFrame1 = inverseBasis * cframe -- put cframe in our local basis

		local _, _, _,
			t00, t01, t02,
			t10, t11, t12,
			t20, t21, t22 = localCFrame1:components()

		local whd = Vector3.new(
			0.5 * (math.abs(sx * t00) + math.abs(sy * t01) + math.abs(sz * t02)),
			0.5 * (math.abs(sx * t10) + math.abs(sy * t11) + math.abs(sz * t12)),
			0.5 * (math.abs(sx * t20) + math.abs(sy * t21) + math.abs(sz * t22))
		)

		local hw = 0.5 * (math.abs(sx * t00) + math.abs(sy * t01) + math.abs(sz * t02))
		local hh = 0.5 * (math.abs(sx * t10) + math.abs(sy * t11) + math.abs(sz * t12))
		local hd = 0.5 * (math.abs(sx * t20) + math.abs(sy * t21) + math.abs(sz * t22))

		local pos1 = localCFrame1.Position
		min1 = Vector3.min(min1, pos1 - whd)
		max1 = Vector3.max(max1, pos1 + whd)

		-- Calculation for the bounding box in the global coordinate space
		_, _, _,
			t00, t01, t02,
			t10, t11, t12,
			t20, t21, t22 = cframe:components()

		hw = 0.5 * (math.abs(sx * t00) + math.abs(sy * t01) + math.abs(sz * t02))
		hh = 0.5 * (math.abs(sx * t10) + math.abs(sy * t11) + math.abs(sz * t12))
		hd = 0.5 * (math.abs(sx * t20) + math.abs(sy * t21) + math.abs(sz * t22))

		local pos2 = cframe.Position
		min2 = Vector3.min(min2, pos2 - Vector3.new(hw, hh, hd))
		max2 = Vector3.max(max2, pos2 + Vector3.new(hw, hh, hd))
	end

	for _, attachment in ipairs(attachments) do
		local worldPosition = attachment.WorldPosition
		min2 = Vector3.min(min2, worldPosition)
		max2 = Vector3.max(max2, worldPosition)

		local localPosition = inverseBasis * worldPosition
		min1 = Vector3.min(min1, localPosition)
		max1 = Vector3.max(max1, localPosition)
	end

	local localOffset = 0.5 * (min1 + max1)
	local localSize = max1 - min1

	local globalOffset = 0.5 * (min2 + max2) - basisPos
	local globalSize = max2 - min2

	return localOffset, localSize, globalOffset, globalSize
end

return BoundingBox


--[[
	BoundsChangedTracker object

	Watches a set of parts for cframe and size changes.
]]

local Utility = script.Parent
local SelectionInfo = require(Utility.SelectionInfo)

local DraggerFramework = Utility.Parent
local Implementation = DraggerFramework.Implementation
local DraggerContext = require(Implementation.DraggerContext)

local MAX_PARTS_TO_TRACK_BOUNDS_FOR = 1024

local BoundsChangedTracker = {}
BoundsChangedTracker.__index = BoundsChangedTracker

type Entry = {
	CFrameChangedSignal: RBXScriptSignal,
	CFrameChangedConnection: RBXScriptConnection?,

	RootCFrameChangedSignal: RBXScriptSignal?,
	RootCFrameChangedConnection: RBXScriptConnection?,

	SizeChangedSignal: RBXScriptSignal,
	SizeChangedConnection: RBXScriptConnection?,

	Trampoline: () -> (),
}

type DraggerContext = DraggerContext.Class
type SelectionInfo = SelectionInfo.Class

export type Class = setmetatable<{
	_handler: (PVInstance | Attachment) -> (),
	_installed: boolean,
	_partToEntry: { [BasePart]: Entry },
	_attachmentToEntry: { [Attachment]: Entry },
	_basisObject: (PVInstance | Attachment)?,
	_basisPivotChangedSignal: RBXScriptSignal?,
	_basisPivotChangedConnection: RBXScriptConnection?,
	_basisPivotChangedTrampoline: () -> (),
}, typeof(BoundsChangedTracker)>

function BoundsChangedTracker.new(draggerContext: DraggerContext, handler: (PVInstance | Attachment) -> ()): Class
	local self = setmetatable({
		_handler = handler,
		_installed = false,
		_partToEntry = {},
		_attachmentToEntry = {},
		_basisObject = nil :: (PVInstance | Attachment)?,
	}, BoundsChangedTracker)

	self._basisPivotChangedTrampoline = function()
		if self._basisObject then
			handler(self._basisObject)
		end
	end

	return self :: Class
end

local function hookUpConnections(entry: Entry)
	entry.CFrameChangedConnection = entry.CFrameChangedSignal:Connect(entry.Trampoline)

	if entry.RootCFrameChangedSignal then
		entry.RootCFrameChangedConnection = entry.RootCFrameChangedSignal:Connect(entry.Trampoline)
	end

	entry.SizeChangedConnection = entry.SizeChangedSignal:Connect(entry.Trampoline)
end

local function disconnectConnections(entry: Entry)
	if entry.CFrameChangedConnection then
		entry.CFrameChangedConnection:Disconnect()
		entry.CFrameChangedConnection = nil
	end

	if entry.RootCFrameChangedConnection then
		entry.RootCFrameChangedConnection:Disconnect()
		entry.RootCFrameChangedConnection = nil
	end

	if entry.SizeChangedConnection then
		entry.SizeChangedConnection:Disconnect()
		entry.SizeChangedConnection = nil
	end
end

local function hookUpAttachmentConnections(entry: Entry)
	entry.CFrameChangedConnection = entry.CFrameChangedSignal:Connect(entry.Trampoline)
end

local function disconnectAttachmentConnections(entry: Entry)
	if entry.CFrameChangedConnection then
		entry.CFrameChangedConnection:Disconnect()
		entry.CFrameChangedConnection = nil
	end
end

function BoundsChangedTracker._hookupBasisConnection(self: Class)
	if self._basisPivotChangedSignal then
		self._basisPivotChangedConnection = self._basisPivotChangedSignal:Connect(self._basisPivotChangedTrampoline)
	end
end

function BoundsChangedTracker._disconnectBasisConnection(self: Class)
	if self._basisPivotChangedConnection then
		self._basisPivotChangedConnection:Disconnect()
		self._basisPivotChangedConnection = nil
	end
end

function BoundsChangedTracker.install(self: Class)
	assert(not self._installed)
	self._installed = true

	for _, entry in pairs(self._partToEntry) do
		hookUpConnections(entry)
	end

	for _, entry in pairs(self._attachmentToEntry) do
		hookUpAttachmentConnections(entry)
	end

	if self._basisObject then
		self:_hookupBasisConnection()
	end
end

function BoundsChangedTracker.uninstall(self: Class)
	assert(self._installed)
	self._installed = false

	for _, entry in pairs(self._partToEntry) do
		disconnectConnections(entry)
	end

	for _, entry in pairs(self._attachmentToEntry) do
		disconnectAttachmentConnections(entry)
	end

	if self._basisObject then
		self:_disconnectBasisConnection()
	end
end

-- Public access for the AlignmentTool, which borrows this class
function BoundsChangedTracker.setParts(self: Class, parts: { BasePart })
	self:_setParts(parts)
end

function BoundsChangedTracker._setBasisObject(self: Class, object: PVInstance?)
	if self._basisObject ~= object then
		if self._installed and self._basisObject then
			self:_disconnectBasisConnection()
		end

		self._basisObject = object

		if object then
			if object:IsA("BasePart") then
				self._basisPivotChangedSignal = object:GetPropertyChangedSignal("PivotOffset")
			elseif object:IsA("Model") then
				if object.PrimaryPart then
					self._basisPivotChangedSignal = object.PrimaryPart:GetPropertyChangedSignal("PivotOffset")
				else
					self._basisPivotChangedSignal = object:GetPropertyChangedSignal("WorldPivot")
				end
			else
				self._basisPivotChangedSignal = nil
			end

			if self._installed then
				self:_hookupBasisConnection()
			end
		end
	end
end

function BoundsChangedTracker._setAttachments(self: Class, attachments: { Attachment })
	local attachmentToNewEntry = {} :: {
		[Attachment]: Entry
	}

	for _, attachment in ipairs(attachments) do
		local entry = self._attachmentToEntry[attachment]
		self._attachmentToEntry[attachment] = nil

		if not entry then
			entry = {
				CFrameChangedSignal = attachment:GetPropertyChangedSignal("CFrame"),
				Trampoline = function()
					self._handler(attachment)
				end,
			}

			if self._installed then
				hookUpAttachmentConnections(entry :: Entry)
			end
		end

		attachmentToNewEntry[attachment] = entry :: Entry
	end

	if self._installed then
		for attachment, entry in pairs(self._attachmentToEntry) do
			if not attachmentToNewEntry[attachment] then
				disconnectAttachmentConnections(entry)
			end
		end
	end

	self._attachmentToEntry = attachmentToNewEntry
end

function BoundsChangedTracker._setParts(self: Class, parts: { BasePart })
	local newPartToEntry = {}

	for index, part in ipairs(parts) do
		if index > MAX_PARTS_TO_TRACK_BOUNDS_FOR then
			-- Too expensive to handle bounds changes for more than ~1000 parts,
			-- so only handle the first 1024 parts for large selections. This
			-- only effects the visible behavior of the tool in very uncommon
			-- scenarios. E.g., if you move an object, select a large multiple
			-- selection containing that object and at least 1024 other parts,
			-- and press undo.
			break
		end

		local entry = self._partToEntry[part]
		self._partToEntry[part] = nil

		if not entry then
			local rootPart = part.AssemblyRootPart
			local rootCFrameChangedSignal

			if rootPart and rootPart ~= part then
				rootCFrameChangedSignal = rootPart:GetPropertyChangedSignal("CFrame")
			end

			entry = {
				CFrameChangedSignal = part:GetPropertyChangedSignal("CFrame"),
				RootCFrameChangedSignal = rootCFrameChangedSignal,
				SizeChangedSignal = part:GetPropertyChangedSignal("Size"),

				Trampoline = function()
					self._handler(part)
				end,
			}

			if self._installed then
				hookUpConnections(entry :: Entry)
			end
		end

		assert(not newPartToEntry[part]) -- Selection should not have duplicates
		newPartToEntry[part] = entry :: Entry
	end

	if self._installed then
		for part, entry in pairs(self._partToEntry) do
			if not newPartToEntry[part] then
				disconnectConnections(entry)
			end
		end
	end

	self._partToEntry = newPartToEntry
end

function BoundsChangedTracker.setSelection(self: Class, selectionInfo: SelectionInfo)
	self:_setAttachments(selectionInfo:getAllAttachments())
	self:_setParts(selectionInfo:getObjectsToTransform())
	self:_setBasisObject(selectionInfo:getBasisObject())
end

return BoundsChangedTracker


local Colors = {}

Colors.WHITE = Color3.new(1, 1, 1)
Colors.BLACK = Color3.new(0, 0, 0)
Colors.GRAY = Color3.new(0.7, 0.7, 0.7)

Colors.X_AXIS = Color3.new(1, 0, 0)
Colors.Y_AXIS = Color3.new(0, 1, 0)
Colors.Z_AXIS = Color3.new(0, 0, 1)

Colors.WeldJoint = Color3.new(1, 1, 1)
Colors.RotatingJoint = Color3.new(0, 0, 1)
Colors.InvalidJoint = Color3.new(1, 0, 0)

Colors.SizeLimitReached = Color3.new(1, 1, 0)

function Colors.makeDimmed(color: Color3, amt: number?)
	return color:Lerp(Colors.BLACK, amt or 0.3)
end

return Colors


local Utility = script.Parent
local Math = require(Utility.Math)
local getGeometry = require(Utility.getGeometry)
local roundRotation = require(Utility.roundRotation)
local snapRotationToPrimaryDirection = require(Utility.snapRotationToPrimaryDirection)

local PrimaryDirections = {
	Vector3.xAxis,
	-Vector3.xAxis,

	Vector3.yAxis,
	-Vector3.yAxis,

	Vector3.zAxis,
	-Vector3.zAxis,
}

export type DragTargetType =
	| "Terrain"
	| "Polygon"
	| "Round"
	| "Nothing"

type Face = getGeometry.Face
type Surface = getGeometry.Surface
type Geometry = getGeometry.Geometry

export type DragTarget = {
	mainCFrame: CFrame,
	snapPoint: Vector3?,
	targetMatrix: CFrame?,
	dragTargetType: DragTargetType,
}

--[[
	If true, then "Rotate" will mean:
		Rotate around whatever axis of the drag target is closest to the
		camera's current up direction.
	If false, then "Rotate" always mean:
		Rotate around the normal of the surface we're dragging onto.
]]

local ROTATE_DEPENDS_ON_CAMERA = false
local VOXEL_RESOLUTION = 4
local DragHelper = {}

local function roundToTerrainGrid(value: number)
	return VOXEL_RESOLUTION * math.floor(value / VOXEL_RESOLUTION + 0.5)
end

local function findClosestBasis(normal: Vector3)
	local mostPerpendicularNormal1 = Vector3.zero
	local smallestDot = math.huge

	for _, primaryDirection in ipairs(PrimaryDirections) do
		local dot = math.abs(primaryDirection:Dot(normal))

		if dot < smallestDot then
			smallestDot = dot
			mostPerpendicularNormal1 = primaryDirection
		end
	end

	local mostPerpendicularNormal2 = mostPerpendicularNormal1:Cross(normal).Unit
	local closestNormal = -mostPerpendicularNormal1:Cross(mostPerpendicularNormal2)

	return closestNormal, mostPerpendicularNormal1, mostPerpendicularNormal2
end

function DragHelper.getSizeInSpace(sizeInGlobalSpace: Vector3, localSpace: CFrame)
	local _, _, _,
		t00, t01, t02,
		t10, t11, t12,
		t20, t21, t22 = localSpace:GetComponents()

	local sx, sy, sz = sizeInGlobalSpace.X, sizeInGlobalSpace.Y, sizeInGlobalSpace.Z

	local w = (math.abs(sx * t00) + math.abs(sy * t01) + math.abs(sz * t02))
	local h = (math.abs(sx * t10) + math.abs(sy * t11) + math.abs(sz * t12))
	local d = (math.abs(sx * t20) + math.abs(sy * t21) + math.abs(sz * t22))

	return Vector3.new(w, h, d)
end

function DragHelper.getClosestFace(part: BasePart, mouseWorld: Vector3): (Face?, Geometry)
	local geom = getGeometry(part, mouseWorld)
	local closestDist = math.huge
	local closestFace: Face?

	for _, face in ipairs(geom.faces) do
		local dist = math.abs((mouseWorld - face.point):Dot(face.normal))

		if dist < closestDist then
			closestFace = face
			closestDist = dist
		end
	end

	return closestFace, geom
end

function DragHelper.getPartAndSurface(mouseRay: Ray): (BasePart?, Surface?)
	local result = workspace:Raycast(mouseRay.Origin, mouseRay.Direction)
	local closestFace: Face?
	local part

	if result then
		local mouseWorld = result.Position
		part = result.Instance

		if part:IsA("Terrain") then
			-- Terrain doesn't have Primary Axis based surfaces to return
			return part, nil
		end

		closestFace = DragHelper.getClosestFace(part, mouseWorld)
	end

	if closestFace then
		return part, closestFace.surface
	else
		return part, nil
	end
end

function DragHelper.getSurfaceMatrix(mouseRay: Ray, selection: {Instance}, lastSurfaceMatrix: CFrame): (CFrame?, Vector3?, DragTargetType?)
	local ignoreList = table.clone(selection)

	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = ignoreList

	local result = workspace:Raycast(mouseRay.Origin, mouseRay.Direction, rayParams)
	local part = result and result.Instance

	if result and part and part:IsA("Terrain") then
		local mouseWorld = result.Position
		local normal = result.Normal

		-- First, find the closest aligned global axis normal, and the two other
		-- axes mutually orthogonal to it.
		local closestNormal, mostPerpendicularNormal1, mostPerpendicularNormal2
			= findClosestBasis(normal)

		-- Now we want to grid-align mouseWorld by snapping it to the
		-- grid size of the terrain on the non-normal axes.
		local alongNormal1 = mouseWorld:Dot(mostPerpendicularNormal1)
		local alongNormal2 = mouseWorld:Dot(mostPerpendicularNormal2)

		local snappedMouseWorldBase =
			mostPerpendicularNormal1 * roundToTerrainGrid(alongNormal1) +
			mostPerpendicularNormal2 * roundToTerrainGrid(alongNormal2)

		-- Since we grid-aligned the position on two of the axis, we have to
		-- bring the position back into the surface plane on the other axis.
		-- Do that by solving the following equation:
		-- (snappedMouseWorldBase + closestNormal * adjustmentIntoPlane):Dot(normal) = mouseWorld:Dot(normal)
		local adjustmentIntoPlane =
			(mouseWorld:Dot(normal) - snappedMouseWorldBase:Dot(normal)) / closestNormal:Dot(normal)
		local snappedMouseWorld = snappedMouseWorldBase + closestNormal * adjustmentIntoPlane

		return CFrame.fromMatrix(snappedMouseWorld,
			normal:Cross(mostPerpendicularNormal1).Unit, normal),
			mouseWorld, "Terrain"
	elseif result and part then
		-- Find the normal and secondary axis (the direction the studs / UV
		-- coords are oriented in) of the surface that we're dragging onto.
		-- Also find the closest "basis" point on the face to the mouse,
		local mouseWorld = result.Position
		local closestFace, geom = DragHelper.getClosestFace(part, mouseWorld)

		local normal = closestFace and closestFace.normal or Vector3.yAxis
		local point = closestFace and closestFace.point or Vector3.zero

		local secondary;
		local closestEdgeDist = math.huge

		for _, edge in ipairs(geom.edges) do
			if (edge.a - point):Dot(normal) < 0.001 and
				(edge.b - point):Dot(normal) < 0.001
			then
				-- Both ends of the edge are part of the selected face,
				-- consider it
				local dir = edge.direction or Vector3.zAxis
				local distAlongEdge = (mouseWorld - edge.a):Dot(dir)
				local pointOnEdge = edge.a + dir * distAlongEdge
				local distToEdge = (mouseWorld - pointOnEdge).Magnitude

				if distToEdge < closestEdgeDist then
					closestEdgeDist = distToEdge
					secondary = dir
				end
			end
		end

		local targetBasisPoint
		local closestBasisDist = math.huge

		if closestFace then
			for _, vert in ipairs(closestFace.vertices) do
				local dist = (vert - mouseWorld).Magnitude

				if dist < closestBasisDist then
					closestBasisDist = dist
					targetBasisPoint = vert
				end
			end
		end

		local dragTargetType: DragTargetType = "Polygon"

		-- TODO: Deal with round things better (this is the case that gets hit
		-- with round things like cylinders and spheres)
		if not secondary then
			dragTargetType = "Round"
			secondary = normal:Cross(Vector3.one).Unit
		end

		if not targetBasisPoint then
			targetBasisPoint = mouseWorld
		end

		-- Find the total transform from the target point on the dragged to
		-- surface to our original CFrame
		return CFrame.fromMatrix(
			targetBasisPoint or Vector3.zero,
			-(secondary or Vector3.xAxis):Cross(normal), normal
		), mouseWorld, dragTargetType
	elseif lastSurfaceMatrix then
		-- Use the last target mat, and the intersection of the mouse mouseRay with
		-- that plane that target mat defined as the drag point.
		local t = Math.intersectRayPlane(
			mouseRay.Origin, mouseRay.Direction,
			lastSurfaceMatrix.Position, lastSurfaceMatrix.UpVector)

		local mouseWorld = mouseRay.Origin + mouseRay.Direction * t
		return lastSurfaceMatrix, mouseWorld, "Nothing"
	else
		-- No previous target or current target, can't drag
		return nil, nil, nil
	end
end

--[[
	Update the tiltRotate by rotating 90 degrees around an axis. Axis is the
	axis in camera space over which the rotation should happen.
]]
function DragHelper.updateTiltRotate(
	cameraCFrame: CFrame,
	mouseRay: Ray, selection: { Instance },
	mainCFrame: CFrame,
	lastTargetMat: CFrame,
	tiltRotate: CFrame,
	axis: Vector3,
	alignRotation: boolean
): (CFrame, DragTargetType?)
	-- Find targetMatrix and dragInTargetSpace in the same way as
	-- in DragHelper.getDragTarget, see that function for further explanation.
	local targetMatrix, _, dragTargetType =
		DragHelper.getSurfaceMatrix(mouseRay, selection, lastTargetMat)

	if not targetMatrix then
		return tiltRotate, nil
	end

	local dragInTargetSpace = targetMatrix:Inverse() * mainCFrame

	if alignRotation then
		dragInTargetSpace = snapRotationToPrimaryDirection(dragInTargetSpace)
	else
		dragInTargetSpace = dragInTargetSpace - dragInTargetSpace.Position
	end

	-- Current global rotation when dragging is given by:
	--   (targetMatrix * dragInTargetSpace * tiltRotate)
	--
	-- So we need to find the local axis of:
	--   (targetMatrix * dragInTargetSpace)
	--
	-- Which is the closest to Camera.RightVector. Then we can construct
	-- the additional `rotation` to add to tiltRotate around that axis:
	--   (targetMatrix * dragInTargetSpace * (rotation * tiltRotate))
	local baseMatrix = targetMatrix * dragInTargetSpace
	local targetDirection

	if not ROTATE_DEPENDS_ON_CAMERA and axis == Vector3.yAxis then
		-- This will end up rotating around the normal of the target surface
		targetDirection = targetMatrix.UpVector
	else
		targetDirection = cameraCFrame:VectorToWorldSpace(axis)
	end

	-- Greater dot product = smaller angle, so the closest direction is
	-- the one with the greatest dot product.
	local closestAxis
	local closestDelta = -math.huge

	for _, direction in ipairs(PrimaryDirections) do
		local delta = baseMatrix:VectorToWorldSpace(direction):Dot(targetDirection)
		if delta > closestDelta then
			closestAxis = direction
			closestDelta = delta
		end
	end

	-- If we somehow had NaNs in our CFrame then closestAxis may still be nil
	closestAxis = closestAxis or Vector3.yAxis

	-- Could be written without the need for rounding by permuting the
	-- components of closestAxis, but this is more understandable.
	local rotation = roundRotation(CFrame.fromAxisAngle(closestAxis, math.pi / 2))
	return rotation * tiltRotate, dragTargetType
end

function DragHelper.getDragTarget(
	mouseRay: Ray,
	snapFunction: (number) -> number,
	dragInMainSpace: Vector3,
	selection: { Instance },
	mainCFrame: CFrame,
	basisPoint: Vector3,
	boundingBoxSize: Vector3,
	boundingBoxOffset: Vector3,
	tiltRotate: CFrame,
	lastTargetMat: CFrame,
	alignRotation: boolean,
	localBoundingBoxSize: Vector3
): DragTarget?
	if not dragInMainSpace then
		return nil
	end

	local targetMatrix, mouseWorld, dragTargetType =
		DragHelper.getSurfaceMatrix(mouseRay, selection, lastTargetMat)

	if not (targetMatrix and mouseWorld and dragTargetType) then
		return nil
	end

	local dragInTargetSpace = targetMatrix:Inverse() * mainCFrame

	if alignRotation then
		-- Now we want to "snap" the rotation of this transformation to 90 degree
		-- increments, such that the dragInTargetSpace is only some combination of
		-- the primary direction vectors.
		dragInTargetSpace = snapRotationToPrimaryDirection(dragInTargetSpace)
	else
		-- Just reduce dragInTargetSpace to a rotation
		dragInTargetSpace = dragInTargetSpace - dragInTargetSpace.Position
	end

	-- Now we want to "snap" the basisPoint to be on-Grid in the main space
	-- the basisPoint is already in the main space, so we can just snap it to
	-- grid and see what offset it moved by. We will need to use a bounding box
	-- modified by that offset for the purposes of bumping, and also shift the
	-- parts by that much when we finally apply them. That is equivalent to
	-- applying the offset as a final factor in the transform this function
	-- returns.
	local offsetX = snapFunction(basisPoint.X) - basisPoint.X
	local offsetY = snapFunction(basisPoint.Y) - basisPoint.Y
	local offsetZ = snapFunction(basisPoint.Z) - basisPoint.Z

	local contentOffset = Vector3.new(offsetX, offsetY, offsetZ)
	local contentOffsetCF = CFrame.new(contentOffset)
	local snappedBoundingBoxOffset = boundingBoxOffset + contentOffset

	-- Compute the size in the space we're dragging into. If alignRotation
	-- is enabled, it could just be computed as a permutation of the size
	-- components. However, when it is an arbitrary rotational offset thanks
	-- to alignRotation being disabled, a larger computation is needed.
	local sizeInTargetSpace = DragHelper.getSizeInSpace(boundingBoxSize, dragInTargetSpace * tiltRotate)

	-- Figure out how much we have to "bump up" the selection to have its
	-- bounding box sit on top of the plane we're dragging onto.
	local offsetInTargetSpace = (dragInTargetSpace * tiltRotate):VectorToWorldSpace(snappedBoundingBoxOffset)
	local normalBumpNeeded = (0.5 * sizeInTargetSpace.Y) - offsetInTargetSpace.Y
	local normalBumpCF = CFrame.new(0, normalBumpNeeded, 0)

	-- Now we have to figure out the offset of the point we started the drag
	-- with from the mainCFrame, and apply that same offset from the point we
	-- dragged to on the new plane, to get a total offset which we should apply
	-- the increment snapping to in the target space.
	local mouseInMainSpace = dragInMainSpace
	local mouseInMainSpaceCF = CFrame.new(mouseInMainSpace)

	-- New mouse position is defined by:
	-- targetMatrix * snapAdjust * normalBump * dragInTargetSpace * tiltRotate * mouseInMainSpace * contentOffset =
	-- mouseWorld
	-- So we want to isolate snapAdjust to snap it's X and Z components
	local mouseWorldCF = (targetMatrix - targetMatrix.Position) * dragInTargetSpace * tiltRotate + mouseWorld

	local snapAdjust =
		targetMatrix:Inverse() *
		mouseWorldCF *
		(normalBumpCF * dragInTargetSpace * tiltRotate * mouseInMainSpaceCF * contentOffsetCF):inverse()

	-- Now that the snapping space is isolated we can apply the snap
	local snapAdjustCF = CFrame.new(snapFunction(snapAdjust.X), 0, snapFunction(snapAdjust.Z))

	-- Get the final CFrame to move the parts to.
	local rotatedBase =
		targetMatrix * snapAdjustCF * normalBumpCF *
		dragInTargetSpace * tiltRotate * contentOffsetCF

	-- Note: Snap point is the visual point that was snapped-to in world space
	-- if we want to display that at some point.
	local snapPoint = (targetMatrix * snapAdjustCF).Position

	return {
		mainCFrame = rotatedBase,
		snapPoint = snapPoint,
		targetMatrix = targetMatrix,
		dragTargetType = dragTargetType,
	}
end

-- All values should be in world coordinates
function DragHelper.getCameraPlaneDragTarget(mouseRay: Ray, cameraLookVector: Vector3, clickPoint: Vector3?): DragTarget?
	if not clickPoint then
		return nil
	end

	local unitMouseRay = mouseRay.Unit
	local cameraPlaneNormal = -1 * cameraLookVector.Unit
	local t = Math.intersectRayPlane(unitMouseRay.Origin, unitMouseRay.Direction, clickPoint, cameraPlaneNormal)

	if t >= 0 then
		local targetPosition = unitMouseRay.Origin + (t * unitMouseRay.Direction)
		local positionDelta = targetPosition - clickPoint

		return {
			mainCFrame = CFrame.new(positionDelta),
			snapPoint = nil, -- not applicable to in-camera plane drag
			targetMatrix = nil, -- not applicable to in-camera plane drag
			dragTargetType = "Nothing",
		}
	end

	return nil
end

return DragHelper


local Utility = script.Parent
local SelectionHelper = require(Utility.SelectionHelper)

local DraggerFramework = Utility.Parent
local Implementation = DraggerFramework.Implementation

local DraggerContext = require(Implementation.DraggerContext)
type DraggerContext = DraggerContext.Class

-- Minimum distance (pixels) required for a drag to select parts.
local DRAG_SELECTION_THRESHOLD = 3

local DragSelector = {}
DragSelector.__index = DragSelector

type Frustum = {{
	origin: Vector3,
	normal: Vector3,
}}

type DragCandidate = {
	Center: Vector3,
	Selectable: Instance,
	Selected: boolean,
}

type DragSelector = {
	_isDragging: boolean,
	_selectionBeforeDrag: {Instance},
	_dragStartLocation: Vector2?,
	_dragCandidates: { DragCandidate },
	_selectionWrapper: any,
	_beginBoxSelect: (DraggerContext) -> {Instance},
	_endBoxSelect: (DraggerContext) -> (),
	_insertionOrder: { [Instance]: number },
	_insertionOrderNext: number,
}

type Proto = typeof(DragSelector)
export type Class = setmetatable<DragSelector, Proto>

function DragSelector.new(selectionWrapper: any, beginBoxSelect: (DraggerContext) -> {Instance}, endBoxSelect: (DraggerContext) -> ()): Class
	local self: DragSelector = {
		_isDragging = false,
		_selectionBeforeDrag = {},
		_dragStartLocation = nil,
		_dragCandidates = {},
		_selectionWrapper = selectionWrapper,
		_beginBoxSelect = beginBoxSelect,
		_endBoxSelect = endBoxSelect,
		_insertionOrder = {},
		_insertionOrderNext = 1,
	}

	return setmetatable(self, DragSelector)
end

-- Create a frustum described by the selection start and end locations and current camera.
local function getSelectionFrustum(draggerContext: DraggerContext, startLocation: Vector2, endLocation: Vector2): Frustum
	local rect = Rect.new(startLocation, endLocation)

	local topLeft = draggerContext:viewportPointToRay(rect.Min)
	local topRight = draggerContext:viewportPointToRay(Vector2.new(rect.Max.X, rect.Min.Y))

	local bottomRight = draggerContext:viewportPointToRay(rect.Max)
	local bottomLeft = draggerContext:viewportPointToRay(Vector2.new(rect.Min.X, rect.Max.Y))

	if topRight.Direction:FuzzyEq(topLeft.Direction) then
		-- Ortho view
		local top = (topRight.Origin - topLeft.Origin):Cross(topRight.Direction)
		local right = (bottomRight.Origin - topRight.Origin):Cross(bottomRight.Direction)
		local bottom = (bottomLeft.Origin - bottomRight.Origin):Cross(bottomLeft.Direction)
		local left = (topLeft.Origin - bottomLeft.Origin):Cross(topLeft.Direction)

		return {
			{origin = topLeft.Origin, normal = top},
			{origin = topRight.Origin, normal = right},
			{origin = bottomRight.Origin, normal = bottom},
			{origin = bottomLeft.Origin, normal = left}
		}
	else
		-- Perspective view
		local left = bottomLeft.Direction:Cross(topLeft.Direction)
		local top = topLeft.Direction:Cross(topRight.Direction)
		local right = topRight.Direction:Cross(bottomRight.Direction)
		local bottom = bottomRight.Direction:Cross(bottomLeft.Direction)

		return {
			{origin = topLeft.Origin, normal = top},
			{origin = topRight.Origin, normal = right},
			{origin = bottomRight.Origin, normal = bottom},
			{origin = bottomLeft.Origin, normal = left}
		}
	end
end

function DragSelector.getStartLocation(self: Class)
	return self._dragStartLocation
end

-- Get list of drag candidates from all selectable parts in the workspace.
-- startLocation can override the location the drag is treated as having
-- started at.
function DragSelector.beginDrag(self: Class, draggerContext: DraggerContext, startLocation: Vector2?)
	assert(not self._isDragging, "Cannot begin drag when already dragging.")
	self._isDragging = true

	self._dragCandidates = self._beginBoxSelect(draggerContext)
	self._selectionBeforeDrag = self._selectionWrapper:get()
	self._dragStartLocation = startLocation or draggerContext:getMouseLocation()
end

--[[
	Test selectable parts against the frustum defined by the drag start location
	and passed in location. Parts within the frustum are added or removed from
	the selection, based on the held modified keys.
]]
function DragSelector.updateDrag(self: Class, draggerContext: DraggerContext)
	assert(self._isDragging, "Cannot update drag when no drag in progress.")

	local shouldXorSelection = draggerContext:shouldExtendSelection()
	local location = draggerContext:getMouseLocation()

	local screenMovement = location - self._dragStartLocation
	if screenMovement.Magnitude < DRAG_SELECTION_THRESHOLD then
		return
	end

	local planes = getSelectionFrustum(draggerContext, self._dragStartLocation, location)
	local newSelection = {}

	local didChangeSelection = false
	local insertionOrder = self._insertionOrder

	for _, candidate in ipairs(self._dragCandidates) do
		local inside = true
		for _, plane in ipairs(planes) do
			local dot = (candidate.Center - plane.origin):Dot(plane.normal)
			if dot < 0 then
				inside = false
				break
			end
		end
		if inside ~= candidate.Selected then
			candidate.Selected = inside
			didChangeSelection = true

			if inside then
				insertionOrder[candidate.Selectable] = self._insertionOrderNext
				self._insertionOrderNext += 1
			end
		end

		if inside then
			table.insert(newSelection, candidate.Selectable)
		end
	end

	if didChangeSelection then
		table.sort(newSelection, function(a, b)
			return (insertionOrder[a] or 0) < (insertionOrder[b] or 0)
		end)

		newSelection = SelectionHelper.updateSelectionWithMultipleSelectables(
			newSelection, self._selectionBeforeDrag,
			shouldXorSelection)

		self._selectionWrapper:set(newSelection)
	end
end

function DragSelector.commitDrag(self: Class, draggerContext: DraggerContext)
	self:updateDrag(draggerContext)
	self._endBoxSelect(draggerContext)

	self._selectionBeforeDrag = {}
	self._dragStartLocation = nil
	self._isDragging = false
end

return DragSelector


local DraggerFramework = script.Parent.Parent
local Utility = DraggerFramework.Utility

local getGeometry = require(Utility.getGeometry)
local JointPairs = require(Utility.JointPairs)
local JointUtil = require(Utility.JointUtil)

local JointMaker = {}
JointMaker.__index = JointMaker

type LengthConstraint =
	| RopeConstraint
	| RodConstraint

type Geometry = getGeometry.Geometry

type JointMaker = {
	_isSimulating: boolean?,
	_partSet: { [BasePart]: true },
	_parts: { BasePart },
	_rootPartSet: { [BasePart]: true },

	_lengthConstraintsToFixupSet: {
		[LengthConstraint]: {
			Span: number,
			Length: number
		}
	},

	_springsToFixupSet: {
		[SpringConstraint]: {
			Span: number,
			FreeLength: number
		}
	},

	_internalJointSet: { [JointInstance]: BasePart },
	_initiallyTouchingSets: { [BasePart]: { [BasePart]: true } },
	_jointsToDestroy: { JointInstance },
	_weldConstraintsToReenableSet: { [WeldConstraint]: true },
	_motor6dsToAdjustAndReenableSet: { [Motor6D]: CFrame },
	_alreadyConnectedToSets: { [BasePart]: { [BasePart]: true } },
	_geometryCache: { [BasePart]: Geometry },
	_toUnanchorSet: { [BasePart]: true }?,
}

type Proto = typeof(JointMaker)
export type Class = setmetatable<JointMaker, Proto>

function JointMaker.new(isSimulating: boolean?): Class
	local self: JointMaker = {
		_partSet = {},
		_parts = {},
		_rootPartSet = {},
		_lengthConstraintsToFixupSet = {},
		_springsToFixupSet = {},
		_internalJointSet = {},
		_initiallyTouchingSets = {},
		_jointsToDestroy = {},
		_weldConstraintsToReenableSet = {},
		_motor6dsToAdjustAndReenableSet = {},
		_alreadyConnectedToSets = {},
		_geometryCache = {},
		_isSimulating = isSimulating,
	}

	return setmetatable(self, JointMaker)
end

local function getConstraintLength(joint: Constraint)
	local a0 = joint.Attachment0
	local a1 = joint.Attachment1

	if a0 and a1 then
		return (a0.WorldPosition - a1.WorldPosition).Magnitude
	end

	return 0
end

--[[
	Set the parts to compute joints for, precomputing as much info as possible
]]
function JointMaker.pickUpParts(self: Class, parts: { BasePart })
	local partSet = {}

	for _, part in ipairs(parts) do
		partSet[part] = true
	end

	self._parts = parts
	self._partSet = partSet
	self._rootPartSet = {} -- Intentionally empty, only needed for IK moves

	local weldConstraintsToReenableSet = {}
	local motor6dsToAdjustAndReenableSet = {}
	local jointsToDestroy = {}
	local alreadyConnectedToSets = {}
	local initiallyTouchingSets = {}
	local internalJointSet = {}
	local springsToFixupSet = {}
	local lengthConstraintsToFixupSet = {}

	for _, part in ipairs(parts) do
		alreadyConnectedToSets[part] = {}

		for _, joint in ipairs(part:GetJoints()) do
			if joint:IsA("Constraint") then
				local other = JointUtil.getConstraintCounterpart(joint, part)

				if other then
					alreadyConnectedToSets[part][other] = true

					if joint:IsA("RopeConstraint") or
						joint:IsA("RodConstraint") then
						lengthConstraintsToFixupSet[joint] = {
							Span = getConstraintLength(joint),
							Length = joint.Length,
						}
					elseif joint:IsA("SpringConstraint") then
						springsToFixupSet[joint] = {
							Span = getConstraintLength(joint),
							FreeLength = joint.FreeLength,
						}
					end
				end
			elseif joint:IsA("JointInstance") then
				local other = JointUtil.getJointInstanceCounterpart(joint, part)

				if other and partSet[other] then
					internalJointSet[joint] = joint.Part1
				else
					if joint:IsA("Motor6D") then
						if other then
							joint.Enabled = false
							motor6dsToAdjustAndReenableSet[joint] = part.CFrame
							alreadyConnectedToSets[part][other] = true
						end
					else
						table.insert(jointsToDestroy, joint)
					end
				end
			elseif joint:IsA("WeldConstraint") then
				local other = JointUtil.getWeldConstraintCounterpart(joint, part)

				if other then
					joint.Enabled = false
					alreadyConnectedToSets[part][other] = true
					weldConstraintsToReenableSet[joint] = true
				end
			elseif joint:IsA("NoCollisionConstraint") then
				local other = JointUtil.getNoCollisionConstraintCounterpart(joint, part)

				if other then
					alreadyConnectedToSets[part][other] = true
				end
			end
		end

		initiallyTouchingSets[part] = {}

		for _, otherPart in ipairs(part:GetTouchingParts()) do
			initiallyTouchingSets[part][otherPart] = true
		end
	end

	self._lengthConstraintsToFixupSet = lengthConstraintsToFixupSet
	self._springsToFixupSet = springsToFixupSet
	self._internalJointSet = internalJointSet
	self._initiallyTouchingSets = initiallyTouchingSets
	self._jointsToDestroy = jointsToDestroy
	self._weldConstraintsToReenableSet = weldConstraintsToReenableSet
	self._motor6dsToAdjustAndReenableSet = motor6dsToAdjustAndReenableSet
	self._alreadyConnectedToSets = alreadyConnectedToSets
	self._geometryCache = {}
end

function JointMaker.anchorParts(self: Class)
	local toUnanchorSet = {}

	for _, part in ipairs(self._parts) do
		if not part.Anchored then
			part.Anchored = true
			toUnanchorSet[part] = true
		end
	end

	self._toUnanchorSet = toUnanchorSet
end

function JointMaker.restoreAnchored(self: Class)
	if self._toUnanchorSet then
		for part, _ in pairs(self._toUnanchorSet) do
			part.Anchored = false
		end
		self._toUnanchorSet = nil
	end
end

--[[
	Break existing joints to others
]]
function JointMaker.breakJointsToOutsiders(self: Class)
	for _, joint in ipairs(self._jointsToDestroy) do
		joint.Parent = nil
	end
	self._jointsToDestroy = {}
end

--[[
	Break joints between parts in the part list
]]
function JointMaker.disconnectInternalJoints(self: Class)
	for joint, _ in pairs(self._internalJointSet) do
		joint.Part1 = nil
	end
end

--[[
	Reconnect the internal joints between parts with a scale
]]
function JointMaker.reconnectInternalJointsWithScale(self: Class, scale: number)
	for joint, part1 in pairs(self._internalJointSet) do
		joint.C0 = joint.C0 + joint.C0.Position * (scale - 1)
		joint.C1 = joint.C1 + joint.C1.Position * (scale - 1)
		joint.Part1 = part1
	end
end

--[[
	Compute the candidate joint pairs for the parts at their current location.
]]
function JointMaker.computeJointPairs(self: Class)
	local jointPairs = JointPairs.new(self._parts, self._partSet, self._rootPartSet,
		CFrame.identity,
		self._alreadyConnectedToSets, function(part)
			return self:_getGeometry(part)
		end)

	if self._isSimulating then
		self._geometryCache = {}
	end

	return jointPairs
end

function JointMaker.isColliding(self: Class, includeInitiallyTouching: boolean)
	for _, part in ipairs(self._parts) do
		for _, otherPart in ipairs(part:GetTouchingParts()) do
			if not self._partSet[otherPart] then
				if includeInitiallyTouching or not self._initiallyTouchingSets[part][otherPart] then
					return true
				end
			end
		end
	end

	return false
end

function JointMaker.fixupConstraintLengths(self: Class)
	for constraint, data in pairs(self._lengthConstraintsToFixupSet) do
		local scaledBy = getConstraintLength(constraint) / data.Span
		constraint.Length = data.Length * scaledBy
	end
	for constraint, data in pairs(self._springsToFixupSet) do
		local scaledBy = getConstraintLength(constraint) / data.Span
		constraint.FreeLength = data.FreeLength * scaledBy
	end
end

function JointMaker.putDownParts(self: Class)
	for weld, _ in pairs(self._weldConstraintsToReenableSet) do
		weld.Enabled = true
	end

	for motor6d, originalCFrame in pairs(self._motor6dsToAdjustAndReenableSet) do
		local part0 = motor6d.Part0
		local part1 = motor6d.Part1

		if part0 and self._partSet[part0] then
			-- Modify C0
			motor6d.C0 = part0.CFrame:Inverse() * originalCFrame * motor6d.C0
		elseif part1 then
			-- Modify C1
			motor6d.C1 = part1.CFrame:Inverse() * originalCFrame * motor6d.C1
		end

		motor6d.Enabled = true
	end

	table.clear(self._motor6dsToAdjustAndReenableSet)
	table.clear(self._weldConstraintsToReenableSet)
	table.clear(self._alreadyConnectedToSets)
	table.clear(self._geometryCache)

	table.clear(self._parts)
	table.clear(self._partSet)
end

function JointMaker._getGeometry(self: Class, part: BasePart): Geometry
	if self._partSet[part] then
		-- Scaling, so our geometry might change every step
		return getGeometry(part)
	else
		local geometry = self._geometryCache[part]

		if not geometry then
			geometry = getGeometry(part)
			self._geometryCache[part] = geometry
		end

		return geometry
	end
end

return JointMaker


local Utility = script.Parent
local DraggerFramework = Utility.Parent
local Packages = DraggerFramework.Parent

local React = require(Packages.React)
local getGeometry = require(Utility.getGeometry)
local Colors = require(Utility.Colors)
local Math = require(Utility.Math)

local JointPairs = {}
JointPairs.__index = JointPairs

local JointTypeToColor = {
	Rotate = Colors.RotatingJoint,
	RotateV = Colors.RotatingJoint,
	RotateP = Colors.RotatingJoint,
	Weld = Colors.WeldJoint,
	None = Colors.InvalidJoint,
}

type getGeometryFunc = typeof(getGeometry)
type Vertex = getGeometry.Vertex
type Shape = getGeometry.Shape
type Face = getGeometry.Face

type IJointPair<T> = {
	ClassName: T,
}

type JointPair = IJointPair<"None"> | (IJointPair<"Weld"> & {
	Part0: BasePart,
	C0: CFrame,
	Part1: BasePart,
	C1: CFrame,
})

type JointPairs = {
	_jointPairs: { JointPair },
	_facesToHighlightSet: { [Face]: JointPair },
}

type Proto = typeof(JointPairs)
export type Class = setmetatable<JointPairs, Proto>

--[[
	How far away from a part to look for other parts to join to. This is purely
	an optimization setting, as long as it is larger than JOINT_TOLERANCE it
	won't change the PartMover behavior.
]]
local FUZZY_TOLERANCE = 0.1

--[[
	How close together the parallel faces of two parts have to be for a joint
	to be made between them.
]]
local JOINT_TOLERANCE = 0.05

--[[
	How close the dot product of two face normals has to be for a joint to be
	made between them.
]]
local JOINT_ANGLE_TOLERANCE = 0.001

local function isVertexInFace(vert: Vector3, face: { Vector3 }, normal: Vector3)
	for i = 1, #face do
		local e1, e2;

		if i == 1 then
			e1 = face[#face]
			e2 = face[1]
		else
			e1 = face[i - 1]
			e2 = face[i]
		end

		local edge = e2 - e1
		local to = vert - e1
		-- TODO: Fix this. For very large parts this will lead to erroneous
		-- misses because we're comparing to an angle tolerance rather than
		-- a distance tolerance by having a tolerance here
		if edge:Cross(to):Dot(normal) < 0.01 then
			return false
		end
	end
	return true
end

local function faceHasVertsInFace(face: { Vector3 }, containingFace: { Vector3 }, normal: Vector3)
	for _, vert in ipairs(face) do
		if isVertexInFace(vert, containingFace, normal) then
			return true
		end
	end

	return false
end

local function edgeIntersectsEdge(a1: Vector3, a2: Vector3, b1: Vector3, b2: Vector3, bnormal: Vector3)
	local arun, brun = (a2 - a1), (b2 - b1)
	local alen, blen = arun.Magnitude, brun.Magnitude
	local edgeDot = arun:Dot(brun) / alen / blen
	local athres = 0.01 / alen
	local bthres = 0.01 / blen

	--[[
		More common case, edges coincident over a non-zero length section.
		We also have a consistent winding direction on our geometry, so if the
		edges are going in the same direction, they are from an edge that is
		touching but not thanks to a touching face. For example:
		[]
		  [] Those two blocks touch at one edge, but not at any faces.
	]]
	if math.abs(math.abs(edgeDot) - 1) < 0.0001 then
		--parallel case
		if edgeDot > 0 then
			-- They are going in the same direction, they can't be
			-- an edge pair to join on
			return false
		else
			-- They are going in opposite directions, this might be an exactly
			-- coincident edge.

			-- First check if they are cooincident
			local inB = (a1 - b1):Dot(brun) / blen / blen
			local dist = (a1 - (b1 + inB * brun)).Magnitude
			if dist > 0.001 then
				return false
			end

			-- Now we need to see if they overlap
			local inB2 = (a2 - b1):Dot(brun) / blen / blen
			local interval = math.clamp(inB, 0, 1) - math.clamp(inB2, 0, 1)
			return interval > 0.001 -- They are coincident and share an interval
		end
	end

	--[[
		Less common case, edges that cross one and other. Imagine two long thin
		parts arranged in an X shape. They should be joined, and that joint comes
		from this case, where their edges intersect at the middle of the X.
	]]
	local intersects, s = Math.intersectRayRay(a1, arun, b1, brun)
	if not intersects or s < athres or s > 1 - athres then
		return false
	end
	local intersects2, t = Math.intersectRayRay(b1, brun, a1, arun)
	assert(intersects2) -- Must be true if intersects was true
	return t >= bthres and t <= 1 - bthres
end

local function edgesIntersectsEdges(face: { Vector3 }, otherFace: { Vector3 }, otherNormal: Vector3)
	for i = 1, #face do
		local a1, a2;

		if i == 1 then
			a1, a2 = face[#face], face[1]
		else
			a1, a2 = face[i - 1], face[i]
		end

		for j = 1, #otherFace do
			local b1, b2;

			if j == 1 then
				b1, b2 = otherFace[#otherFace], otherFace[1]
			else
				b1, b2 = otherFace[j - 1], otherFace[j]
			end

			if edgeIntersectsEdge(a1, a2, b1, b2, otherNormal) then
				return true
			end
		end
	end

	return false
end

local function canMakeJointBetweenFaces(part: BasePart, face: Face, otherPart: BasePart, otherFace: Face, otherNormal: Vector3)
	-- Can join if there are any touching faces. This can be divided into:
	-- 1. Obviously touching if verts of one face are contained in the other face
	-- 2. If edges of one face intersect the other, for example, when two long
	--	parts are arranged in an X shape.
	return faceHasVertsInFace(face.vertices, otherFace.vertices, otherNormal) or
		faceHasVertsInFace(otherFace.vertices, face.vertices, -otherNormal) or
		edgesIntersectsEdges(face.vertices, otherFace.vertices, otherNormal)
end

local function getFaceCenter(face: Face): Vector3
	if not face.center then
		local total = Vector3.zero

		for _, vert in ipairs(face.vertices) do
			total = total + vert
		end

		face.center = total / #face.vertices
	end

	return assert(face.center)
end

local function buildJoint(part0: BasePart, face0: Face, part1: BasePart, jointType: "Weld"): JointPair
	-- Determine the C0 and C1 for the joint. Center it around the center of
	-- the relevant face, since that's where the surface axis gizmo appears.
	local centerCFrame =
		CFrame.fromMatrix(
			getFaceCenter(face0),
			face0.direction, face0.normal:Cross(face0.direction))

	return {
		ClassName = jointType,
		Part0 = part0,
		C0 = part0.CFrame:Inverse() * centerCFrame,
		Part1 = part1,
		C1 = part1.CFrame:Inverse() * centerCFrame,
	}
end

local function buildInvalidJoint(): JointPair
	return {
		ClassName = "None",
	}
end

--[[
	Called once we already know that there is a joint between two faces, but
	don't know what _kind_ of joint it is yet or which part will be the
	part0 and which will be the part1.

	`part` is the part being moved, and its face surface type takes priority
	as far as determining the type of joint.
]]
local function buildAppropriateJoint(part: BasePart, otherPart: BasePart, shape: Shape, otherShape: Shape, face: Face, otherFace: Face): JointPair?
	local isFaceAcceptable =
		(shape == "Mesh") or
		(shape == "Cylinder" and
			(face.surface == "RightSurface" or face.surface == "LeftSurface"))

	local isOtherFaceAcceptable =
		(otherShape == "Mesh") or
		(otherShape == "Cylinder" and
			(otherFace.surface == "RightSurface" or otherFace.surface == "LeftSurface"))

	if isFaceAcceptable and isOtherFaceAcceptable then
		return buildJoint(part, face, otherPart, "Weld")
	else
		-- Only planar meshes and the flat ends of cylinders are allowed to
		-- form surface welds.
		return nil
	end
end

--[[
	Check if a joint is possible between two specific parts and return the joint
	if there is a possible joint.
	Updates facesToHighlightSet with both the possible joint if there is one,
	and with an invalid joint if there's touching faces that can't be joined.
]]
local function tryToCreateJointPair(
	transform: CFrame,
	part: BasePart,
	otherPart: BasePart,
	facesToHighlightSet: { [Face]: JointPair },
	getGeometryFunction: getGeometryFunc,
	isAHumanoidModelFunction: (Instance?) -> boolean
): (boolean, JointPair?)
	local partGeometry = getGeometryFunction(part)
	local otherGeometry = getGeometryFunction(otherPart)

	-- Transform faces of the
	local tempTransformedFaces = {}

	-- Easy case first, just handle mesh-mesh for now
	for _, partFace in ipairs(partGeometry.faces) do
		for _, otherFace in ipairs(otherGeometry.faces) do
			local transformedNormal = transform:VectorToWorldSpace(partFace.normal)
			local transformedDirection = transform:VectorToWorldSpace(partFace.direction)
			local dot = transformedNormal:Dot(otherFace.normal)
			if dot < -(1 - JOINT_ANGLE_TOLERANCE) then
				local newPoint = transform:PointToWorldSpace(partFace.point)
				local dist = (newPoint - otherFace.point):Dot(otherFace.normal)
				if math.abs(dist) < JOINT_TOLERANCE then
					local tempTransformedFace = tempTransformedFaces[partFace]
					if not tempTransformedFace then
						local transformedFaceVerts = {}
						for _, vertex in ipairs(partFace.vertices) do
							table.insert(transformedFaceVerts, transform * vertex)
						end

						tempTransformedFace = {
							id = partFace.id,
							vertices = transformedFaceVerts,
							normal = transformedNormal,
							direction = transformedDirection,
							surface = partFace.surface,
							-- Note: There are more fields, we're only copying
							-- over the modified ones we need.
						} :: Face

						tempTransformedFaces[partFace] = tempTransformedFace
					end
					local canJoin = canMakeJointBetweenFaces(
						part, tempTransformedFace,
						otherPart, otherFace,
						otherFace.normal)
					if canJoin then
						-- The faces are aligned and close to cooincident
						local joint = buildAppropriateJoint(
							part, otherPart,
							partGeometry.shape, otherGeometry.shape,
							tempTransformedFace, otherFace)

						if joint and not isAHumanoidModelFunction(part.Parent) and
							not isAHumanoidModelFunction(otherPart.Parent) then
							-- This if exists because of the extra weird case of
							-- surface hinges/motors blocking joints on the rest
							-- of the surface that they're on.
							-- Also block joints between parts when either part
							-- is inside of a humanoid.
							facesToHighlightSet[tempTransformedFace] = joint
							facesToHighlightSet[otherFace] = joint
							return true, joint
						else
							-- If the joint doesn't exist, we draw an invalid
							-- joint pair (red) between the parts.
							joint = buildInvalidJoint()
							facesToHighlightSet[tempTransformedFace] = joint
							facesToHighlightSet[otherFace] = joint
							return false, nil
						end
					end
				end
			end
		end
	end

	return false, nil
end

--[[
	Compute the joint pairs between a collection of parts and the rest of the
	world. Return a JointPairs object storing information about the computed
	joints for further use.

	partList - A list of the parts
	partSet -- The same parts, organized as a set rather than a list.
	rootPartSet -- A set of the root parts of the parts in the partList.
	globalTransform --
		A transform to apply to the partSet, including any getGeometry results
		returned for them.
	alreadyConnectedToSets --
		alreadyConnectedToSets[part][otherPart] is true if part and other are
		already connected with a joint.
	getGeometryFunction --
		A function that takes a part and returns the getGeometry() for it.
]]
function JointPairs.new(
	partList: { BasePart },
	partSet: { [BasePart]: true },
	rootPartSet: { [BasePart]: true },
	globalTransform: CFrame,
	alreadyConnectedToSets: { [BasePart]: { [BasePart]: true } },
	getGeometryFunction: getGeometryFunc
): Class
	local self = setmetatable({}, JointPairs)
	local isAHumanoidModel = {}

	local function isAHumanoidModelFunction(object: Instance?)
		if not object then
			return false
		end

		local status = isAHumanoidModel[object]

		if not status then
			status =
				(object:FindFirstChildWhichIsA("Humanoid") ~= nil) or
				isAHumanoidModelFunction(object.Parent)

			isAHumanoidModel[object] = status
		end

		return status
	end

	local terrain = workspace.Terrain
	local facesToHighlightSet = {}
	local jointPairs = {}

	for _, part in ipairs(partList) do
		local radius = part.Size.Magnitude
		local nearbyParts = workspace:GetPartBoundsInRadius(part.Position, radius + FUZZY_TOLERANCE)

		-- Terrain joint case. If we are touching the terrain at all then
		-- create a joint pair with it, regardless of surface geometry.
		for _, touchingPart in ipairs(part:GetTouchingParts()) do
			if touchingPart == terrain then
				if not alreadyConnectedToSets[part][terrain] then
					table.insert(jointPairs, {
						ClassName = "Weld",
						C0 = CFrame.identity,
						C1 = part.CFrame,
						Part0 = part,
						Part1 = terrain,
					} :: JointPair)
				end

				break
			end
		end

		for _, otherPart in ipairs(nearbyParts) do
			if not partSet[otherPart] and not rootPartSet[otherPart.AssemblyRootPart] then
				if otherPart ~= terrain then
					local otherRadius = otherPart.Size.Magnitude / 2
					if (otherPart.Position - part.Position).Magnitude <= (radius + otherRadius) + JOINT_TOLERANCE then
						-- This is a very uncommon condition (happens only for
						-- constraints joining a part in the selection and
						-- one not in the selection and only when those two parts
						-- are touching) which is why I put it innermost.
						if not alreadyConnectedToSets[part][otherPart] then
							local success, joint = tryToCreateJointPair(globalTransform,
								part, otherPart,
								facesToHighlightSet,
								getGeometryFunction, isAHumanoidModelFunction)

							if success and joint then
								table.insert(jointPairs, joint)
							end
						end
					end
				end
			end
		end
	end

	self._jointPairs = jointPairs
	self._facesToHighlightSet = facesToHighlightSet

	return self
end

--[[
	Return a React component visually displaying the joint pairs to be created.
]]
function JointPairs.renderJoints(self: Class, scale: number)
	local faceViews = {}

	for face, joint in pairs(self._facesToHighlightSet) do
		local edgeViews = {}

		for i = 1, #face.vertices do
			local v0, v1;

			if i == 1 then
				v0, v1 = face.vertices[#face.vertices], face.vertices[1]
			else
				v0, v1 = face.vertices[i - 1], face.vertices[i]
			end

			local edgeLength = (v0 - v1).Magnitude

			edgeViews[i] = React.createElement("CylinderHandleAdornment", {
				CFrame = CFrame.new(v0, v1) * CFrame.new(0, 0, -edgeLength / 2) * CFrame.Angles(0, 0, math.pi / 2),
				Color3 = JointTypeToColor[joint.ClassName],
				Radius = 0.05 + 0.05 * scale,
				Height = edgeLength,
				Shading = Enum.AdornShading.XRay,
				Adornee = workspace.Terrain
			})
		end

		-- Using the face table's hash as key here. There's no other good way to
		-- uniquely identify the face, because they key is really the part that
		-- the face is being shown for, but we can't use a part as a key here.
		faceViews[tostring(face)] = React.createElement("Folder", {}, edgeViews)
	end

	return React.createElement(React.Fragment, {}, faceViews)
end

--[[
	Create the joint pairs and parent them to the Workspace.
]]
function JointPairs.createJoints(self: Class)
	for _, joint in ipairs(self._jointPairs) do
		if joint.ClassName == "None" then
			continue
		end

		local jointInstance: JointInstance = Instance.new(joint.ClassName) :: any
		jointInstance.Part0 = joint.Part0
		jointInstance.C0 = joint.C0
		jointInstance.Part1 = joint.Part1
		jointInstance.C1 = joint.C1
		jointInstance.Parent = joint.Part0
	end
end

return JointPairs


local JointUtil = {}

function JointUtil.getConstraintCounterpart(constraint: Constraint, part: BasePart?): BasePart?
	-- Ugly micro-optimized code because this function gets hit in hot paths
	-- The micro-optimized version saves 3-4ms on some actions over the
	-- unoptimized variant.
	local attachment0 = constraint.Attachment0

	if attachment0 then
		local attachment0Parent = attachment0.Parent

		if attachment0Parent == part then
			local attachment1 = constraint.Attachment1
			local attachment1Parent = attachment1 and attachment1.Parent

			if attachment1Parent and attachment1Parent:IsA("BasePart") then
				return attachment1Parent
			end
		elseif attachment0Parent and attachment0Parent:IsA("BasePart") then
			return attachment0Parent
		end
	else
		local attachment1 = constraint.Attachment1
		local attachment1Parent = attachment1 and attachment1.Parent

		if attachment1Parent and attachment1Parent:IsA("BasePart") then
			return attachment1Parent
		end
	end

	return nil
end

function JointUtil.getJointInstanceCounterpart(joint: JointInstance, part: BasePart?): BasePart?
	local part0 = joint.Part0

	if part0 == part then
		return joint.Part1
	else
		return part0
	end
end

function JointUtil.getWeldConstraintCounterpart(weldConstraint: WeldConstraint, part: BasePart?): BasePart?
	local part0 = weldConstraint.Part0

	if part0 == part then
		return weldConstraint.Part1
	else
		return part0
	end
end

function JointUtil.getNoCollisionConstraintCounterpart(noCollisionConstraint: NoCollisionConstraint, part: BasePart?): BasePart?
	local part0 = noCollisionConstraint.Part0
	if part0 == part then
		return noCollisionConstraint.Part1
	else
		return part0
	end
end

return JointUtil


--[[
	Shared utility class which transforms parts efficiently as a group, and
	manages joining / unjoining them from the world.
]]
local RunService = game:GetService("RunService")

local Utility = script.Parent
local JointUtil = require(Utility.JointUtil)
local JointPairs = require(Utility.JointPairs)
local getGeometry = require(Utility.getGeometry)

local DEFAULT_COLLISION_THRESHOLD = 0.001

-- Get all the instances the user has directly selected (actually part of the
-- selection)
local function getSelectedInstanceSet(selection: { Instance }): { [Instance]: boolean }
	local selectedInstanceSet = {}

	for _, instance in pairs(selection) do
		selectedInstanceSet[instance] = true
	end

	return selectedInstanceSet
end

local PartMover = {}
PartMover.__index = PartMover

type Geometry = getGeometry.Geometry

type PartMover = {
	_parts: { BasePart },
	_partSet: { [BasePart]: boolean },
	_rootPartSet: { [BasePart]: boolean }?,
	_toUnanchor: { [BasePart]: true },
	_facesToHighlightSet: { [BasePart]: boolean },

	_nearbyGeometry: {
		[BasePart]: Geometry, --[[ getGeometry result ]]
	},

	_workspaceParts: { BasePart }?,
	_moving: boolean?,

	_originalCFrameMap: {
		[BasePart]: CFrame,
	},

	_originalModelPivotMap: {
		[Model]: CFrame,
	}?,

	_originalMainPartCFrame: CFrame,
	_hasMovementWelds: boolean?,
	_hasSetupGeometryTracking: boolean?,
	_customCenter: Vector3?,
	_mainPart: BasePart?,

	_moveWithCFrameChangeParts: { BasePart }?,
	_moveWithCFrameChangeOriginalCFrames: { CFrame }?,
	_moveWithCFrameChangeTargetCFrames: { CFrame }?,

	_bulkMoveParts: { BasePart }?,
	_bulkMoveOriginalCFrames: { CFrame }?,
	_bulkMoveTargetCFrames: { CFrame }?,

	_reenableWeldConstraints: { [WeldConstraint]: true }?,
	_adjustAndReenableMotor6Ds: { [Motor6D]: true }?,

	_alreadyConnectedToSets: {
		[BasePart]: { [BasePart]: true },
	}?,

	_temporaryJoints: { JointInstance }?,
	_lastTransform: CFrame?,
}

type Proto = typeof(PartMover)
export type Class = setmetatable<PartMover, Proto>

--[[
	Default value for IK dragging translation and rotation stiffness.
]]
local DRAG_CONSTRAINT_STIFFNESS = 0.85
local DRAG_CONSTRAINT_LESS_STIFFNESS = 0.40

function PartMover.new()
	local partMover: PartMover = {
		_parts = {},
		_partSet = {},
		_toUnanchor = {},
		_originalCFrameMap = {},
		_facesToHighlightSet = {},
		_nearbyGeometry = {},
		_originalMainPartCFrame = CFrame.identity,
	}

	local self = setmetatable(partMover, PartMover)
	self:_createMainPart()

	return self
end

--[[
	The part we're using to move stuff that needs to be ignored for raycasting.
]]
function PartMover.getIgnorePart(self: Class): BasePart?
	return self._mainPart
end

function PartMover.setDragged(
	self: Class,
	parts: { BasePart },
	originalCFrameMap: { [BasePart]: CFrame },
	breakJoints: boolean,
	customCenter: Vector3?,
	selection: { Instance },
	allModels: { Model }
)
	-- Separate out the Workspace parts which will be passed to
	-- Workspace::ArePartsTouchingOthers for collision testing
	local workspaceParts = table.create<<BasePart>>(16)

	for _, part in ipairs(parts) do
		if part:IsDescendantOf(workspace) then
			table.insert(workspaceParts, part)
		end
	end

	self._workspaceParts = workspaceParts
	assert(not self._moving)

	self._moving = true
	self._originalCFrameMap = originalCFrameMap

	if #parts == 0 then
		self._parts = {}
		return
	end

	self:_initPartSet(parts)
	self._customCenter = customCenter or Vector3.zero
	self:_prepareJoints(parts, breakJoints)
	self._hasSetupGeometryTracking = false
	self:_setupBulkMove(parts, getSelectedInstanceSet(selection))

	local originalModelPivotMap = {}
	for _, model in ipairs(allModels) do
		originalModelPivotMap[model] = model:GetPivot()
	end
	self._originalModelPivotMap = originalModelPivotMap

	self._parts = parts
	self._hasMovementWelds = false
end

function PartMover._setupBulkMove(self: Class, parts: { BasePart }, selectedInstanceSet: { [BasePart]: boolean })
	local movingRootSet = {} :: { [BasePart]: true }
	local originalCFrameMap = self._originalCFrameMap

	-- Directly selected instances need special handling, they must be moved
	-- with CFrame changes. If they are not, the properties widget will not
	-- show updates to their properties in real time, that is why there are
	-- two loops below: A first pass to handle all the directly selected
	-- instances, and a second pass to capture everything else.

	local moveWithCFrameChangeOriginalCFrameArray = {}
	local moveWithCFrameChangePartArray = {}
	local moveWithCFrameChangeNextIndex = 1

	for _, part in ipairs(parts) do
		if selectedInstanceSet[part] then
			-- Only need to care about parts which are their own root here.
			-- In order to move directly selected parts, at least one of the
			-- directly selected parts will usually become a root as the
			-- selection is dis-jointed from the surroundings at the start
			-- of the drag. The only edge-case this does not cover is the
			-- case where you select a model and part welded to the model,
			-- and the assembly root is in the model. This case is not
			-- possible to handle so don't worry about it.
			if (part.AssemblyRootPart or part) == part then
				-- We don't need to check whether we're already in the set,
				-- because the root == part check already implies that
				-- we're not as long as the parts list doesn't have
				-- duplicates (a condition which is enforced elsewhere).
				movingRootSet[part] = true

				moveWithCFrameChangePartArray[moveWithCFrameChangeNextIndex] = part
				moveWithCFrameChangeOriginalCFrameArray[moveWithCFrameChangeNextIndex] =
					originalCFrameMap[part]
				moveWithCFrameChangeNextIndex = moveWithCFrameChangeNextIndex + 1
			end
		end
	end

	local partsToBulkMoveArray = {}
	local originalCFramesArray = {}
	local nextIndexToInsertAt = 1

	for _, part in ipairs(parts) do
		local movementRoot = part.AssemblyRootPart or part

		if not movingRootSet[movementRoot] then
			movingRootSet[movementRoot] = true

			partsToBulkMoveArray[nextIndexToInsertAt] = movementRoot
			originalCFramesArray[nextIndexToInsertAt] = originalCFrameMap[movementRoot]
			nextIndexToInsertAt = nextIndexToInsertAt + 1
		end
	end

	-- Anchor all of the unanchored roots if we're dragging while simulation
	-- is happening, so that the parts don't move around as we're trying
	-- to drag them.
	if RunService:IsRunning() then
		for root in movingRootSet do
			if not root.Anchored then
				root.Anchored = true
				self._toUnanchor[root] = true
			end
		end
	end

	self._moveWithCFrameChangeParts = moveWithCFrameChangePartArray
	self._moveWithCFrameChangeOriginalCFrames = moveWithCFrameChangeOriginalCFrameArray
	self._moveWithCFrameChangeTargetCFrames = table.create<<CFrame>>(#moveWithCFrameChangePartArray)

	self._bulkMoveParts = partsToBulkMoveArray
	self._bulkMoveOriginalCFrames = originalCFramesArray
	self._bulkMoveTargetCFrames = table.create<<CFrame>>(#partsToBulkMoveArray)
end

function PartMover:_initPartSet(parts)
	self._partSet = {}
	for _, part in ipairs(parts) do
		self._partSet[part] = true
	end
end

--[[
	_getGeometry caches into nearbyGeometry, so here we reset that table
	and load the initial geometry of the parts to move into the cache.
]]
function PartMover._setupGeometryTracking(self: Class)
	assert(not self._hasSetupGeometryTracking)
	self._nearbyGeometry = {}
	self._rootPartSet = {}

	for _, part in ipairs(self._workspaceParts) do
		-- If the part somehow got removed from the workspace (for instance,
		-- by a user plugin) since we added it to the _workspaceParts table,
		-- then it won't have a root part.
		local root = part:GetRootPart()
		if root then
			-- We have to track the roots separately, because some of the root parts
			-- of the dragged parts may not be in the set of dragged parts.
			self._rootPartSet[root] = true
		end
	end
end

--[[
	Call once per move, setup geometry tracking.
]]
function PartMover._ensureGeometryTrackingHasBeenSetup(self: Class)
	if not self._hasSetupGeometryTracking then
		self:_setupGeometryTracking()
		self._hasSetupGeometryTracking = true
	end
end

--[[
	Create an auxiliary part to weld everything to in order to move everything
	as a single aggregate from the Lua side.
]]
function PartMover._createMainPart(self: Class)
	local part = Instance.new("Part")
	part.Name = "PartDragMover"
	part.Transparency = 1
	part.Archivable = false
	part.CanCollide = false

	-- 0 Density so that we don't effect the IK drag weighting
	part.CustomPhysicalProperties =
		PhysicalProperties.new(
			0.0001, -- Density
			0.3, -- Friction
			0.5 -- Elasticity
		)

	self._mainPart = part
	self._originalMainPartCFrame = part.CFrame
end

function PartMover._setupMainPart(self: Class, customCenter: Vector3)
	-- Part must be anchored in run mode, so that physics can't simulate it.
	-- However, the part must be UNanchored in normal mode, because IK dragging
	-- won't work if the part is anchored.
	local mainPart = self._mainPart

	if mainPart then
		local cf = CFrame.new(customCenter)
		mainPart.Anchored = RunService:IsRunning()
		self._originalMainPartCFrame = cf

		mainPart.CFrame = cf
		mainPart.Parent = workspace.Terrain

		self._partSet[mainPart] = true
	end
end

--[[
	Every moved part is joined together so that we can move the parts as
	a single solid body with only one property set from Lua.
	If breakJoints is true, break joints to outsiders before doing the drag
]]
function PartMover._prepareJoints(self: Class, parts: { BasePart }, breakJoints: boolean)
	local reenableWeldConstraints = {} :: { [WeldConstraint]: true }
	local adjustAndReenableMotor6Ds = {} :: { [Motor6D]: true }
	local alreadyConnectedToSets = {} :: {
		[BasePart]: { [BasePart]: true },
	}

	self._reenableWeldConstraints = reenableWeldConstraints
	self._adjustAndReenableMotor6Ds = adjustAndReenableMotor6Ds
	self._alreadyConnectedToSets = alreadyConnectedToSets

	for _, part in ipairs(parts) do
		alreadyConnectedToSets[part] = {}

		for _, joint in ipairs(part:GetJoints()) do
			if joint:IsA("JointInstance") then
				local other = JointUtil.getJointInstanceCounterpart(joint, part)

				if other then
					if breakJoints then
						if not self._partSet[other] then
							if joint:IsA("Motor6D") then
								-- Disable Motor6Ds temporarily and adjust them
								-- after the move. Motor6Ds need to feel more
								-- "permanent" than other joints.
								joint.Enabled = false
								adjustAndReenableMotor6Ds[joint] = true
								alreadyConnectedToSets[part][other] = true
							else
								-- We can't destroy these, otherwise Undo behavior
								-- won't be able to put them back in the workspace.
								joint.Parent = nil
							end
						end
					else
						alreadyConnectedToSets[part][other] = true
					end
				end
			elseif joint:IsA("Constraint") then
				-- Constraints don't effect non-IK movement, so
				-- we don't have to break or unparent them.

				-- However we do have to record the constraints between
				-- parts so that we know what joint pairs are redundant.
				local other = JointUtil.getConstraintCounterpart(joint, part)
				if other then
					-- The if is because some constraints like VectorForce
					-- will not have a counterpart.
					alreadyConnectedToSets[part][other] = true
				end
			elseif joint:IsA("WeldConstraint") then
				local other = JointUtil.getWeldConstraintCounterpart(joint, part)

				if other then
					alreadyConnectedToSets[part][other] = true

					if breakJoints then
						-- Weld constraints to non-dragged parts need to be disabled,
						-- and then re-enabled after the move. Note: To show up in
						-- GetJoints, this weld must have been enabled.
						if not self._partSet[other] then
							joint.Enabled = false
							reenableWeldConstraints[joint] = true
						end
					end
				end
			elseif joint:IsA("NoCollisionConstraint") then
				local other = JointUtil.getNoCollisionConstraintCounterpart(joint, part)

				if other then
					alreadyConnectedToSets[part][other] = true
				end
			else
				error("Unexpected Joint Type: " .. joint.ClassName)
			end
		end
	end
end

function PartMover._installMovementWelds(self: Class)
	if self._hasMovementWelds then
		return
	end

	self:_setupMainPart(self._customCenter)
	self._hasMovementWelds = true
	self._temporaryJoints = {}

	local mainPartCFrame = self._originalMainPartCFrame
	local mainPartCFrameInv = mainPartCFrame:Inverse()

	for _, part in self._parts do
		local moveJoint = Instance.new("Weld")
		moveJoint.Archivable = false
		moveJoint.Name = "Temp Movement Weld"
		moveJoint.Part0 = self._mainPart
		moveJoint.Part1 = part
		moveJoint.C0 = mainPartCFrameInv * part.CFrame
		moveJoint.Parent = self._mainPart
		table.insert(self._temporaryJoints, moveJoint)
	end
end

-- Find the candidate joints after transforming the dragged parts by `transform`
function PartMover.computeJointPairs(self: Class, globalTransform: CFrame)
	assert(self._moving)
	self:_ensureGeometryTrackingHasBeenSetup()

	local jointPairs = JointPairs.new(self._parts, self._partSet, self._rootPartSet,
		globalTransform,
		self._alreadyConnectedToSets, function(part)
			-- Note, the part may not be in originalCFrameMap, in which case
			-- assumedCFrame = nil, which is the correct value for that case.
			local assumedCFrame = self._originalCFrameMap[part]
			return self:_getGeometry(part, assumedCFrame)
		end)

	if RunService:IsRunning() then
		self:_flushNonDraggedGeometryCache()
	end

	return jointPairs
end

function PartMover._transformModelPivots(self: Class, globalTransform: CFrame)
	if self._originalModelPivotMap then
		for model, originalPivot in pairs(self._originalModelPivotMap) do
			model.WorldPivot = globalTransform * originalPivot
		end
	end
end

function PartMover._transformToImpl(self: Class, transform: CFrame, mode: Enum.BulkMoveMode)
	if self._bulkMoveParts then
		local targets = assert(self._bulkMoveTargetCFrames)
		local originals = assert(self._bulkMoveOriginalCFrames)

		for i = 1, #self._bulkMoveParts do
			targets[i] = transform * originals[i]
		end

		workspace:BulkMoveTo(self._bulkMoveParts, targets, mode)

		local moveWithCFrameChangeParts = assert(self._moveWithCFrameChangeParts)
		originals = assert(self._moveWithCFrameChangeOriginalCFrames)
		targets = assert(self._moveWithCFrameChangeTargetCFrames)

		for i = 1, #moveWithCFrameChangeParts do
			targets[i] = transform * originals[i]
		end

		workspace:BulkMoveTo(moveWithCFrameChangeParts, targets, Enum.BulkMoveMode.FireAllEvents)
	end

	self:_transformModelPivots(transform)
end

--[[
	The main function to move the parts geometrically.
]]
function PartMover.transformTo(self: Class, transform: CFrame)
	assert(self._moving)
	self._lastTransform = transform
	self:_transformToImpl(transform, Enum.BulkMoveMode.FireCFrameChanged)
end

--[[
	The main function to move the parts via inverse kinematics.
]]
function PartMover.transformToWithIk(self: Class, transform: CFrame, translateStiffness: number, rotateStiffness: number, collisionsMode: Enum.IKCollisionsMode)
	assert(self._moving)
	-- If we have no physical parts, then IK dragging is the same as
	-- geometric dragging. (We still may have free parts to move)
	if #self._parts == 0 then
		self:transformTo(transform)
		return transform
	end

	-- Make sure the movement welds exist, they are needed to do IK movement
	self:_installMovementWelds()

	local originalMainPartCFrame = assert(self._originalMainPartCFrame :: CFrame?)
	local targetCFrame = transform * originalMainPartCFrame

	pcall(function()
		workspace:IKMoveTo(
			self._mainPart, targetCFrame,
			translateStiffness, rotateStiffness,
			collisionsMode)
	end)

	local actualCFrame = self._mainPart.CFrame
	local actualGlobalTransform = actualCFrame * originalMainPartCFrame:Inverse()

	self:_transformModelPivots(actualGlobalTransform)

	-- We need to clear all non-dragged parts cached geometry, since we may
	-- have bumped into and moved other parts as part of the IK move.
	self:_flushNonDraggedGeometryCache()

	return actualGlobalTransform
end

function PartMover.moveToWithIk(self: Class, transform: CFrame, collisionsMode: Enum.IKCollisionsMode)
	local translateStiffness = DRAG_CONSTRAINT_STIFFNESS
	local rotateStiffness = DRAG_CONSTRAINT_LESS_STIFFNESS
	return self:transformToWithIk(transform, translateStiffness, rotateStiffness, collisionsMode)
end

function PartMover.rotateToWithIk(self: Class, transform: CFrame, collisionsMode: Enum.IKCollisionsMode)
	local translateStiffness = DRAG_CONSTRAINT_LESS_STIFFNESS
	local rotateStiffness = DRAG_CONSTRAINT_STIFFNESS
	return self:transformToWithIk(transform, translateStiffness, rotateStiffness, collisionsMode)
end

--[[
	Are any of the parts to move intersecting other parts not in the set of
	parts to move?
]]
function PartMover.isIntersectingOthers(self: Class, overlapToIgnore: number?)
	-- Pcall because a user plugin may have removed some of the _workspaceParts
	-- from the workspace mid-move, and ArePartsTouchingOthers requires all the
	-- passed parts to be in a WorldRoot.
	local workspaceParts = assert(self._workspaceParts)

	local st, result = pcall(workspace.ArePartsTouchingOthers, workspace,
		workspaceParts, overlapToIgnore or DEFAULT_COLLISION_THRESHOLD)

	if st then
		return result
	else
		return true
	end
end

--[[
	Finish moving parts, and removing all of the temporary joints.
]]

function PartMover.commit(self: Class)
	assert(self._moving)
	self._moving = false

	if self._mainPart then
		self._mainPart.Parent = nil
	end

	if self._reenableWeldConstraints then
		for weldConstraint in self._reenableWeldConstraints do
			weldConstraint.Enabled = true
		end

		self._reenableWeldConstraints = nil
	end

	if self._adjustAndReenableMotor6Ds then
		for motor6d in self._adjustAndReenableMotor6Ds do
			local part0 = motor6d.Part0
			local part1 = motor6d.Part1

			if part0 and self._partSet[part0] then
				-- Modify C0
				motor6d.C0 = part0.CFrame:Inverse() * self._originalCFrameMap[part0] * motor6d.C0
			elseif part1 then
				-- Modify C1
				motor6d.C1 = part1.CFrame:Inverse() * self._originalCFrameMap[part1] * motor6d.C1
			end

			motor6d.Enabled = true
		end
	end

	self._facesToHighlightSet = {}

	if self._temporaryJoints then
		for _, joint in self._temporaryJoints do
			joint:Destroy()
		end

		self._temporaryJoints = {}
	end

	for part in self._toUnanchor do
		part.Anchored = false
	end

	self._toUnanchor = {}

	if self._bulkMoveParts then
		self._moveWithCFrameChangeParts = nil
		self._moveWithCFrameChangeOriginalCFrames = nil
		self._moveWithCFrameChangeTargetCFrames = nil

		self._bulkMoveParts = nil
		self._bulkMoveOriginalCFrames = nil
		self._bulkMoveTargetCFrames = nil
	end

	-- Note: We didn't do anything with the freeParts during commit. That's
	-- because there's nothing to do with them, they are not simulated in any
	-- way, so the changes we already made to their CFrames suffice.
end

function PartMover:_getGeometry(part, assumedCFrame)
	local geometry = self._nearbyGeometry[part]

	if not geometry then
		geometry = getGeometry(part, Vector3.zero, assumedCFrame)
		self._nearbyGeometry[part] = geometry
	end

	return geometry
end

function PartMover._flushNonDraggedGeometryCache(self: Class)
	for part, _ in pairs(self._nearbyGeometry) do
		if not self._partSet[part] then
			self._nearbyGeometry[part] = nil
		end
	end
end

return PartMover


--[[
	Provides utility functions related to the selection.
]]

local SelectionHelper = {}

type SelectionChangeHint = {
	Added: { Instance },
	Removed: { Instance },
}

-- Returns: Did the selection change, The new selection, A change hint
function SelectionHelper.updateSelection(
	selectable: Instance?,
	oldSelection: { Instance },
	isExclusive: boolean,
	shouldExtendSelection: boolean
): (boolean, { Instance }, SelectionChangeHint?)
	local doExtendSelection = shouldExtendSelection

	if not selectable then
		if doExtendSelection then
			return false, oldSelection, nil
		else
			local wasOldSelectionNonempty = (#oldSelection > 0)
			return wasOldSelectionNonempty, {}, nil
		end
	end

	if doExtendSelection and not isExclusive then
		-- Add or remove from the selection when ctrl or shift is held.
		local newSelection = {}
		local added, removed = {}, {}
		local didRemoveSelectableInstance = false

		for _, item in ipairs(oldSelection) do
			if item == selectable then
				didRemoveSelectableInstance = true
			else
				table.insert(newSelection, item)
			end
		end

		if didRemoveSelectableInstance then
			table.insert(removed, selectable)
		else
			table.insert(newSelection, selectable)
			table.insert(added, selectable)
		end

		return true, newSelection, {Added = added, Removed = removed}
	else
		local index = table.find(oldSelection, selectable)

		if index and not isExclusive then
			-- The instance is already in the selection. If the active instance
			-- needs to be updated, and the instance isn't already the last item
			-- in the list, move it to the end of the selection.
			local lastIndex = #oldSelection

			if index < lastIndex then
				local newSelection = {}
				table.move(oldSelection, 1, index, 1, newSelection)
				table.move(oldSelection, index + 1, lastIndex, index, newSelection)
				newSelection[lastIndex] = selectable

				-- Remove and then add the selectable to push it to
				-- the end of the selection.
				local hint = {Added = {selectable}, Removed = {selectable}}
				return true, newSelection, hint
			end

			-- Otherwise, leave the selection alone.
			return false, oldSelection, nil
		else
			-- The instance is not in the selection and the selection is not being
			-- extended; overwrite the old selection.
			return true, {selectable}, nil
		end
	end
end

function SelectionHelper.updateSelectionWithMultipleSelectables(
	selectables: { Instance },
	oldSelection: { Instance },
	shouldXorSelection: boolean,
	shouldExtendSelection: boolean?
): { Instance }
	if #selectables == 0 then
		return (shouldXorSelection or shouldExtendSelection) and oldSelection or {}
	end

	local newSelection

	if shouldXorSelection or shouldExtendSelection then
		local alreadySelectedInstances = {}
		newSelection = {}
		-- Add or remove from the selection when ctrl or shift is held.

		for _, instance in ipairs(oldSelection) do
			alreadySelectedInstances[instance] = true
		end

		if shouldXorSelection then
			local newInstancesToSelect = {}

			for _, instance in ipairs(selectables) do
				newInstancesToSelect[instance] = true
			end

			for _, selectable in ipairs(oldSelection) do
				if not newInstancesToSelect[selectable] then
					table.insert(newSelection, selectable)
				end
			end

			for _, selectable in ipairs(selectables) do
				if not alreadySelectedInstances[selectable] then
					table.insert(newSelection, selectable)
				end
			end
		elseif shouldExtendSelection then
			for _, selectable in ipairs(oldSelection) do
				table.insert(newSelection, selectable)
			end

			for _, selectable in ipairs(selectables) do
				if not alreadySelectedInstances[selectable] then
					table.insert(newSelection, selectable)
				end
			end
		end
	else
		-- The selection is not being extended; overwrite the old selection.
		newSelection = selectables
	end

	return newSelection
end

return SelectionHelper