--!native
--!strict

local DeltaTable = {}

export type LazyTable = {
    [any]: any,
}

export type Patch = {
    Path: { string },
    Value: any,
}

export type Trace = {
    Path: { string },
    OnApply: (path: { string }, value: any) -> (),
}

local function Deep(tbl: any): any
    local tCopy = table.create(#tbl)
    setmetatable(tCopy, getmetatable(tbl))

    for k, v in pairs(tbl) do
        if type(v) == "table" then
            tCopy[k] = Deep(v)
        else
            tCopy[k] = v
        end
    end

    return tCopy
end

local function GetDeltaKey(key: any): any
    if type(key) == "number" then
        return `_#{key}`
    end

    return key
end

local function ParseDeltaKey(key: any): any
    if type(key) == "string" and key:sub(1, 2) == "_#" then
        return tonumber(key:sub(3))
    end

    return key
end

local function Apply(maybeTarget: LazyTable?, deltaTable: LazyTable, trace: Trace?)
    local target: LazyTable = maybeTarget or {}
    local deletions = deltaTable.__deletions

    for deltaKey in deltaTable do
        if deltaKey == "__deletions" then
            continue
        end

        local key = ParseDeltaKey(deltaKey)

        if trace then
            table.insert(trace.Path, tostring(key))
        end

        if type(deltaTable[deltaKey]) == "table" then
            target[key] = Apply(target[key], deltaTable[deltaKey], trace)
        else
            target[key] = deltaTable[deltaKey]

            if trace then
                trace.OnApply(trace.Path, deltaTable[deltaKey])
            end
        end

        if trace then
            table.remove(trace.Path)
        end
    end

    if deletions then
        for i, key in pairs(deletions) do
            target[key] = nil

            if trace then
                table.insert(trace.Path, key)
                trace.OnApply(trace.Path, nil)
                table.remove(trace.Path)
            end
        end
    end

    return target
end

DeltaTable.DeepCopy = Deep

--- Compares two tables, and produces a new table containing the differences
function DeltaTable.Create(oldTable: any, newTable: any, fuzzyEpsilon: number?): (any, number)
    if oldTable == nil then
        return Deep(newTable), 0
    end

    local deltaTable = {}
    local changes = 0

    for key, data in pairs(newTable) do
        local deltaKey = GetDeltaKey(key)

        if oldTable[key] == nil then
            changes += 1
            deltaTable[deltaKey] = data
        else
            if type(newTable[key]) == "table" then
                -- its a table, recurse
                local childTable, num = DeltaTable.Create(oldTable[key], newTable[key])

                if num > 0 then
                    changes += num
                    deltaTable[deltaKey] = childTable
                end
            else
                local old = oldTable[key]
                local new = newTable[key]

                if fuzzyEpsilon then
                    if type(old) == "number" and type(new) == "number" then
                        if math.abs(old - new) < fuzzyEpsilon then
                            continue
                        end
                    elseif type(old) == "vector" and type(new) == "vector" then
                        if vector.magnitude((new - old) :: any) < fuzzyEpsilon then
                            continue
                        end
                    end
                end

                if old ~= new then
                    changes += 1
                    deltaTable[deltaKey] = new
                end
            end
        end
    end

    -- Check for deletions
    local deletions = {}

    for key in pairs(oldTable) do
        if newTable[key] == nil then
            table.insert(deletions, key)
            changes += 1
        end
    end

    if next(deletions) then
        deltaTable.__deletions = deletions
    end

    return deltaTable, changes
end

--- Produces a new table that is the combination of a target, and a deltaTable produced by DeltaTable.Create
function DeltaTable.Apply(target: LazyTable?, deltaTable: LazyTable, onApply: (path: {string}, value: any) -> ()?): LazyTable
    local trace: Trace?

    if onApply then
        trace = {
            Path = {},
            OnApply = onApply,
        }
    end

    return Apply(target, deltaTable, trace)
end

return DeltaTable


--[[
	Component that displays a rubber band-style selection frame.
]]

local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)

local Colors = require(DraggerFramework.Utility.Colors)

export type Props = {
	BackgroundColor3: Color3?,
	BackgroundTransparency: number?,
	BorderColor3: Color3?,

	DragStartLocation: Vector2,
	DragEndLocation: Vector2,
}

local function DragSelectionView(props: Props): React.Node?
	local min = props.DragStartLocation
	local max = props.DragEndLocation

	if not min or not max then
		return nil
	end

	local size = max - min
	local backgroundColor3 = props.BackgroundColor3 or Colors.BLACK
	local backgroundTransparency = props.BackgroundTransparency or 1
	local borderColor3 = props.BorderColor3 or Colors.GRAY

	return React.createElement("ScreenGui", {
		IgnoreGuiInset = true,
	}, {
		React.createElement("Frame", {
			Position = UDim2.fromOffset(min.X, min.Y),
			Size = UDim2.fromOffset(size.X, size.Y),
			BackgroundColor3 = backgroundColor3,
			BackgroundTransparency = backgroundTransparency,
			BorderSizePixel = 0,
		}, {
			Stroke = React.createElement("UIStroke", {
				BorderStrokePosition = Enum.BorderStrokePosition.Inner,
				Color = borderColor3,
				Thickness = 1,
			}),
		})
	}) :: React.Node
end

return DragSelectionView


local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)

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

local MAIN_SPHERE_RADIUS = 0.4
local MAIN_SPHERE_TRANSPARENCY = 0.5

type DraggerContext = DraggerContext.Class

type Props = {
	CFrame: CFrame,
	IsActive: boolean,
	DraggerContext: DraggerContext,
}

return function(props: Props)
	local handleScale: number = props.DraggerContext:getHandleScale(props.CFrame.Position)

	return React.createElement("SphereHandleAdornment", {
		Adornee = workspace.Terrain,
		CFrame = props.CFrame,
		Radius = handleScale * MAIN_SPHERE_RADIUS,
		ZIndex = 0,
		AlwaysOnTop = false,
		Transparency = MAIN_SPHERE_TRANSPARENCY,
		Color3 = props.DraggerContext:getSelectionBoxColor(props.IsActive),
	})
end


--[[
	DraggerToolComponent is a React component which drives an internal
	DraggerToolModel with inputs in a real-time situation such as ingame or in
	studio plugin.
]]

-- Services
local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")
local HttpService = game:GetService("HttpService")

local Components = script.Parent
local DraggerFramework = Components.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)

local Implementation = DraggerFramework.Implementation
local DraggerContext = require(Implementation.DraggerContext)
local DraggerToolModel = require(Implementation.DraggerToolModel)

-- Utilities
local Utility = DraggerFramework.Utility
local shouldDragAsFace = require(Utility.shouldDragAsFace)
local ViewChangeDetector = require(Utility.ViewChangeDetector)

-- Constants
local DRAGGER_UPDATE_BIND_NAME = "DraggerToolViewUpdate"
type DraggerSettings = DraggerToolModel.DraggerSettings
type DraggerToolModel = DraggerToolModel.Class

export type Props = {
	DraggerContext: DraggerContext.Class,
	DraggerSettings: DraggerSettings,
	DraggerSchema: any,

	Mouse: Mouse,
	InitialMouseDown: boolean?,
}

local function DraggerToolComponent(props: Props)
	local viewBoundsAreDirty = React.useRef(false)
	local selectionBoundsAreDirty = React.useRef(false)
	local draggerToolModelRef = React.useRef(nil :: DraggerToolModel?)
	local renderNode, setRenderNode = React.useState(nil :: React.Node?)

	local requestRedraw = React.useMemo(function ()
		return function()
			local draggerToolModel = draggerToolModelRef.current

			if draggerToolModel then
				local render = draggerToolModel:render()
				setRenderNode(render)
			end
		end
	end, {})

	React.useEffect(function ()
		local draggerToolModel = DraggerToolModel.new(
			props.DraggerContext,
			props.DraggerSchema,
			props.DraggerSettings,
			requestRedraw,
			function() viewBoundsAreDirty.current = true end,
			function() selectionBoundsAreDirty.current = true end
		)

		draggerToolModelRef.current = draggerToolModel

		if props.InitialMouseDown then
			task.defer(function()
				if draggerToolModel then
					draggerToolModel:_processMouseDown()
				end
			end)
		end

		-- Select it first before we potentially start feeding input to it
		draggerToolModel:_processSelected()

		-- Trigger a render now that the model exists
		draggerToolModel:render()

		return function()
			draggerToolModel:_processDeselected()
		end
	end, {
		props.DraggerSchema,
		props.DraggerContext,
		props.DraggerSettings,
	})

	-- Input wiring and render loop
	React.useEffect(function ()
		local mouse = props.Mouse
		local bindName = DRAGGER_UPDATE_BIND_NAME .. HttpService:GenerateGUID(false)

		local mouseDownConn = mouse.Button1Down:Connect(function()
			local draggerToolModel = draggerToolModelRef.current

			if draggerToolModel then
				draggerToolModel:_processMouseDown()
			end
		end)

		local mouseUpConn = mouse.Button1Up:Connect(function()
			local draggerToolModel = draggerToolModelRef.current

			if draggerToolModel then
				draggerToolModel:_processMouseUp()
			end
		end)

		local keyDownConn = UserInputService.InputBegan:Connect(function(input, _gameProcessed)
			if input.UserInputType == Enum.UserInputType.Keyboard then
				local draggerToolModel = draggerToolModelRef.current

				if draggerToolModel then
					draggerToolModel:_processKeyDown(input.KeyCode)
				end
			end
		end)

		local keyUpConn = UserInputService.InputEnded:Connect(function(input, _gameProcessed)
			if input.UserInputType == Enum.UserInputType.Keyboard then
				local draggerToolModel = draggerToolModelRef.current

				if draggerToolModel then
					draggerToolModel:_processKeyUp(input.KeyCode)
				end
			end
		end)

		local dragEnterConn: RBXScriptConnection?

		if mouse:IsA("PluginMouse") then
			dragEnterConn = mouse.DragEnter:Connect(function(instances)
				local draggerToolModel = draggerToolModelRef.current

				if #instances > 0 and draggerToolModel then
					if #instances == 1 and shouldDragAsFace(instances[1]) then
						draggerToolModel:_processToolboxInitiatedFaceDrag(instances)
					else
						draggerToolModel:_processToolboxInitiatedFreeformSelectionDrag()
					end
				end
			end)
		end

		local viewChange = ViewChangeDetector.new(mouse)
		local lastUseLocalSpace = props.DraggerContext:shouldUseLocalSpace()

		RunService:BindToRenderStep(bindName, Enum.RenderPriority.First.Value, function()
			local draggerToolModel = draggerToolModelRef.current

			if draggerToolModel then
				draggerToolModel:update()
			else
				return
			end

			local shouldUpdateView = false
			local shouldUpdateSelection = false

			if viewChange:poll() then
				shouldUpdateView = true
			end

			if selectionBoundsAreDirty.current then
				selectionBoundsAreDirty.current = false
				shouldUpdateSelection = true
			end

			if viewBoundsAreDirty.current then
				viewBoundsAreDirty.current = false
				shouldUpdateView = true
			end

			local currentUseLocalSpace = props.DraggerContext:shouldUseLocalSpace()

			if currentUseLocalSpace ~= lastUseLocalSpace then
				-- Can't use a changed event for this, since Changed doesn't fire
				-- for changes to UseLocalSpace.
				shouldUpdateSelection = true
			end

			if shouldUpdateSelection then
				draggerToolModel:_processSelectionChanged()
			end

			if shouldUpdateView then
				draggerToolModel:_processViewChanged()
			end

			lastUseLocalSpace = currentUseLocalSpace
		end)

		return function()
			local draggerToolModel = draggerToolModelRef.current
			mouseDownConn:Disconnect()
			mouseUpConn:Disconnect()
			keyDownConn:Disconnect()
			keyUpConn:Disconnect()

			if dragEnterConn then
				dragEnterConn:Disconnect()
			end

			RunService:UnbindFromRenderStep(bindName)

			-- Deselect after we stop potentially sending events
			if draggerToolModel then
				draggerToolModel:_processDeselected()
			end
		end
	end, { props.Mouse })

	return renderNode
end

return DraggerToolComponent


--[[
	Component that displays an "L" label near the bottom-right corner of the
	passed in bounding volume.
]]

local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)

local PADDING = 3
local Implementation = DraggerFramework.Implementation
local DraggerContext = require(Implementation.DraggerContext)

type DraggerContext = DraggerContext.Class

export type Props = {
	CFrame: CFrame,
	Size: Vector3,
	DraggerContext: DraggerContext,

	BackgroundTransparency: number?,
	Font: Enum.Font?,
	TextSize: number?,
	TextColor3: Color3?,
	TextStrokeColor3: Color3?,
	TextStrokeTransparency: number?,
}

const defaultProps = {
	BackgroundTransparency = 1,
	Font = Enum.Font.ArialBold,
	TextSize = 16,
	TextColor3 = Color3.new(1, 1, 1),
	TextStrokeColor3 = Color3.new(0, 0, 0),
	TextStrokeTransparency = 0,
}

local function LocalSpaceIndicator(props: Props): React.ReactElement?
	local draggerContext = props.DraggerContext
	local cframe = props.CFrame
	local halfSize = props.Size / 2

	-- Compute the bounding box corners in object space.
	local max = halfSize
	local min = -halfSize

	local font = props.Font or defaultProps.Font
	local textSize = props.TextSize or defaultProps.TextSize
	local textColor3 = props.TextColor3 or defaultProps.TextColor3
	local textStrokeColor3 = props.TextStrokeColor3 or defaultProps.TextStrokeColor3
	local textStrokeTransparency = props.TextStrokeTransparency or defaultProps.TextStrokeTransparency
	local backgroundTransparency = props.BackgroundTransparency or defaultProps.BackgroundTransparency

	local corners = {
		Vector3.new(min.X, min.Y, min.Z),
		Vector3.new(min.X, max.Y, min.Z),
		Vector3.new(min.X, max.Y, max.Z),
		Vector3.new(min.X, min.Y, max.Z),
		Vector3.new(max.X, min.Y, min.Z),
		Vector3.new(max.X, max.Y, min.Z),
		Vector3.new(max.X, max.Y, max.Z),
		Vector3.new(max.X, min.Y, max.Z),
	}

	local projectedCorners = {} :: {{
		point: Vector2,
		onScreen: boolean,
	}}

	local optimalX, optimalY = -math.huge, -math.huge

	-- Find the optimal screen position for the label. This will be the maximum
	-- of all the points.
	for i = 1, #corners do
		-- For each projected corner record whether it is onscreen, but use the
		-- point for the optimal point calculation regardless. Not using all of
		-- the bounding volume corners can cause the "L" indicator to jump around
		-- when the bounding volume is partly outside the viewport.
		local worldPoint = cframe:PointToWorldSpace(corners[i])
		local screenPoint, onScreen = draggerContext:worldToViewportPoint(worldPoint)
		local point = Vector2.new(screenPoint.X, screenPoint.Y)

		table.insert(projectedCorners, {
			point = point,
			onScreen = onScreen,
		})

		optimalX = math.max(optimalX, point.X)
		optimalY = math.max(optimalY, point.Y)
	end

	-- Take the projected point closest to the optimal point to use as the
	-- position of the label.
	local optimalPoint = Vector2.new(optimalX, optimalY)
	local minDistanceToOptimal = math.huge
	local isProjectedCornerOnScreen = false
	local position = Vector2.zero

	for i = 1, #projectedCorners do
		local screenPoint = projectedCorners[i].point
		local distanceToOptimal = (screenPoint - optimalPoint).Magnitude
		if distanceToOptimal < minDistanceToOptimal then
			minDistanceToOptimal = distanceToOptimal
			position = screenPoint
			isProjectedCornerOnScreen = projectedCorners[i].onScreen
		end
	end

	if not isProjectedCornerOnScreen then
		return nil
	end

	-- Label size calculation is an approximation to avoid using TextService
	-- to measure a single-character string.
	local labelSize = textSize + PADDING * 2

	return React.createElement("ScreenGui", {}, {
		React.createElement("TextLabel", {
			BackgroundTransparency = backgroundTransparency,
			Position = UDim2.fromOffset(position.X, position.Y),
			Size = UDim2.fromOffset(labelSize, labelSize),
			Font = font,
			TextSize = textSize,
			Text = "L",
			TextColor3 = textColor3,
			TextStrokeColor3 = textStrokeColor3,
			TextStrokeTransparency = textStrokeTransparency,
			Selectable = false,
		})
	})
end

return LocalSpaceIndicator


local DraggerFramework = script.Parent.Parent
local Shared = DraggerFramework.Parent
local Math = require(DraggerFramework.Utility.Math)

local React = require(Shared.React)
local ReactRoblox = require(Shared.ReactRoblox)

local CULLING_MODE = Enum.AdornCullingMode.Never

local BASE_HANDLE_RADIUS = 0.10
local BASE_HANDLE_HITTEST_RADIUS = BASE_HANDLE_RADIUS * 4 -- Handle hittests bigger than it looks
local BASE_HANDLE_OFFSET = 0.60
local BASE_HANDLE_LENGTH = 4.00
local BASE_TIP_OFFSET = 0.20
local BASE_TIP_LENGTH = 0.25
local TIP_RADIUS_MULTIPLIER = 3
local SCREENSPACE_HANDLE_SIZE = 6
local HANDLE_THIN_BY_FRAC = 0.34
local HANDLE_THICK_BY_FRAC = 1.5

export type Props = {
	Axis: CFrame,
	Color: Color3,
	Scale: number,
	Outset: number?,
	Hovered: boolean?,
	Thin: boolean?,
	AlwaysOnTop: boolean?,
	Shading: Enum.AdornShading?,
	GuiParent: BasePlayerGui?,
}

local MoveHandleView = {}

function MoveHandleView.render(props: Props)
	local scale = props.Scale

	local length = scale * BASE_HANDLE_LENGTH
	local radius = scale * BASE_HANDLE_RADIUS

	local offset = scale * BASE_HANDLE_OFFSET
	offset = offset + length * (props.Outset or 0)

	local tipOffset = scale * BASE_TIP_OFFSET
	local tipLength = length * BASE_TIP_LENGTH

	if props.Thin then
		radius = radius * HANDLE_THIN_BY_FRAC
	end

	if props.Hovered then
		radius = radius * HANDLE_THICK_BY_FRAC
		tipLength = tipLength * HANDLE_THICK_BY_FRAC
	end

	local shading = props.Shading or Enum.AdornShading.Default

	if props.Shading then
		shading = props.Shading
	elseif props.AlwaysOnTop then
		shading = Enum.AdornShading.XRayShaded
	end

	local coneAtCFrame = props.Axis * CFrame.new(0, 0, -(offset + length))
	local tipAt = coneAtCFrame * Vector3.new(0, 0, -tipOffset)
	local tipAtScreen = workspace.CurrentCamera:WorldToScreenPoint(tipAt)
	local children = {}

	children.Shaft = React.createElement("CylinderHandleAdornment", {
		Adornee = workspace.Terrain, -- Just a neutral anchor point
		ZIndex = 0,
		Radius = radius,
		Height = length,
		CFrame = props.Axis * CFrame.new(0, 0, -(offset + length * 0.5)),
		Color3 = props.Color,
		Shading = shading,
		AdornCullingMode = CULLING_MODE,
	})

	if not props.Thin then
		children.Head = React.createElement("ConeHandleAdornment", {
			Adornee = workspace.Terrain,
			ZIndex = 0,
			Radius = TIP_RADIUS_MULTIPLIER * radius,
			Height = tipLength,
			CFrame = coneAtCFrame,
			Color3 = props.Color,
			Shading = shading,
			AdornCullingMode = CULLING_MODE,
		})
	end

	if not props.Thin and props.GuiParent then
		local halfHandleSize = 0.5 * SCREENSPACE_HANDLE_SIZE

		children.ScreenBox = ReactRoblox.createPortal({
			MoveToolScreenspaceHandle = React.createElement("ScreenGui", {}, {
				Frame = React.createElement("Frame", {
					BorderSizePixel = 0,
					BackgroundColor3 = props.Color,
					Position = UDim2.new(0, tipAtScreen.X - halfHandleSize, 0, tipAtScreen.Y - halfHandleSize),
					Size = UDim2.new(0, SCREENSPACE_HANDLE_SIZE, 0, SCREENSPACE_HANDLE_SIZE),
				})
			})
		}, props.GuiParent)
	end

	return React.createElement("Folder", {}, children)
end

function MoveHandleView.hitTest(props: Props, mouseRay: Ray): number?
	local scale = props.Scale

	local length = scale * BASE_HANDLE_LENGTH
	local radius = scale * BASE_HANDLE_HITTEST_RADIUS
	local tipRadius = radius * TIP_RADIUS_MULTIPLIER

	local offset = scale * BASE_HANDLE_OFFSET
	offset = offset + length * (props.Outset or 0)

	local tipOffset = scale * BASE_TIP_OFFSET
	local tipLength = length * BASE_TIP_LENGTH
	local shaftEnd = offset + length

	if not props.AlwaysOnTop then
		-- Check the always on top 2D element at the tip of the vector
		local tipAt = props.Axis * Vector3.new(0, 0, -(offset + length + tipOffset))
		local tipAtScreen, _ = workspace.CurrentCamera:WorldToScreenPoint(tipAt)
		local mouseAtScreen = workspace.CurrentCamera:WorldToScreenPoint(mouseRay.Origin)
		local halfHandleSize = 0.5 * SCREENSPACE_HANDLE_SIZE

		if mouseAtScreen.X > tipAtScreen.X - halfHandleSize and
			mouseAtScreen.Y > tipAtScreen.Y - halfHandleSize and
			mouseAtScreen.X < tipAtScreen.X + halfHandleSize and
			mouseAtScreen.Y < tipAtScreen.Y + halfHandleSize
		then
			return 0
		end
	end

	local hasIntersection, hitDistance = Math.intersectRayRay(
			props.Axis.Position, props.Axis.LookVector,
			mouseRay.Origin, mouseRay.Direction.Unit)

	if not hasIntersection then
		return nil
	end

	-- Must have an intersection if the above intersect did
	local _, distAlongMouseRay =
		Math.intersectRayRay(
			mouseRay.Origin, mouseRay.Direction.Unit,
			props.Axis.Position, props.Axis.LookVector)

	local hitRadius =
		((props.Axis.Position + props.Axis.LookVector * hitDistance) -
		(mouseRay.Origin + mouseRay.Direction.Unit * distAlongMouseRay)).Magnitude

	if hitRadius < radius and hitDistance > offset and hitDistance < shaftEnd then
		return distAlongMouseRay
	elseif hitRadius < tipRadius and hitDistance > shaftEnd and hitDistance < shaftEnd + tipLength then
		return distAlongMouseRay
	else
		return nil
	end
end

--[[
	Returns:
		float Offset - From base CFrame
		float Size - Extending from CFrame + Offset
]]
function MoveHandleView.getHandleDimensionForScale(scale: number, outset: number?): (number, number)
	local length = scale * BASE_HANDLE_LENGTH

	local offset = scale * BASE_HANDLE_OFFSET
	offset = offset + length * (outset or 0)

	local tipLength = length * BASE_TIP_LENGTH
	return offset, length + tipLength
end

return MoveHandleView


--[[
	Displays rotation gimbal handles. When dragging, start and end radii showing
	the central angle of rotation are displayed.
]]

-- Dragger Framework
local RotateHandleView = {}

local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)
local Math = require(DraggerFramework.Utility.Math)

local CULLING_MODE = Enum.AdornCullingMode.Never

local HANDLE_RADIUS = 4.5
local HANDLE_THICKNESS = 0.15
local ANGLE_DISPLAY_THICKNESS = 0.08
local HANDLE_HITTEST_THICKNESS = HANDLE_THICKNESS * 4
local HANDLE_THIN_BY_FRAC = 0.0
local HANDLE_THICK_BY_FRAC = 1.5
local HANDLE_DIM_TRANSPARENCY = 0.45
local HANDLE_TICK_WIDTH = 0.05
local HANDLE_TICK_WIDE_WIDTH = 0.10
local HANDLE_TICK_RADIUS_FRAC = 0.10 -- Fraction of the radius
local HANDLE_TICK_RADIUS_LONG_FRAC = 0.30 -- Fraction for the primary angles (multiple of 90)
local QUARTER_ROTATION = math.pi / 2

local function isMultipleOf90Degrees(angle: number)
	local roundedTo90 = math.floor(angle / QUARTER_ROTATION + 0.5) * QUARTER_ROTATION
	return math.abs(angle - roundedTo90) < 0.001
end

export type Props = {
	HandleCFrame: CFrame,
	Color: Color3,
	Scale: number,
	StartAngle: number?,
	EndAngle: number?,
	TickAngle: number?,
	RadiusOffset: number?,
	Shading: Enum.AdornShading?,
	Hovered: boolean,
	Thin: boolean,
}

function RotateHandleView.render(props: Props)
	-- TODO: DEVTOOLS-3876: [Modeling] Rotate tool enhancements
	-- Gimbal arc length should be a function of the viewing angle, and handle
	-- should face the camera.

	local radiusOffset = props.RadiusOffset or 0.0
	local radius = (HANDLE_RADIUS + radiusOffset) * props.Scale

	if props.Hovered then
		radius = radius + props.Scale * 0.1
	end

	local thickness = HANDLE_THICKNESS * props.Scale

	local children = {} :: {
		[string]: any,
	}

	-- Thinning for drag
	if props.Thin then
		thickness = HANDLE_THIN_BY_FRAC * thickness
	end
	if props.Hovered then
		thickness = HANDLE_THICK_BY_FRAC * thickness
	end

	-- Draw main rotation gimbal.
	local halfThickness = 0.5 * thickness

	children["Handle"] = React.createElement("CylinderHandleAdornment", {
		Adornee = workspace.Terrain,
		CFrame = props.HandleCFrame * CFrame.Angles(props.StartAngle or 0, math.pi / 2, math.pi / 2),
		Height = thickness,
		Radius = radius + halfThickness,
		InnerRadius = radius - halfThickness,
		Color3 = props.Color,
		Shading = props.Shading,
		Transparency = HANDLE_DIM_TRANSPARENCY,
		ZIndex = 0,
		AdornCullingMode = CULLING_MODE,
	})

	if props.TickAngle then
		local angleStep = props.TickAngle
		local count = math.ceil(math.pi * 2 / angleStep)
		local smallTickWidth = HANDLE_TICK_WIDTH * props.Scale
		local smallTickLength = HANDLE_TICK_RADIUS_FRAC * radius

		-- Information for the primary ticks placed at 90 degree intervals
		-- relative to the angle the rotate started at.
		local primaryTickWidth = HANDLE_TICK_WIDE_WIDTH * props.Scale
		local primaryTickLength = HANDLE_TICK_RADIUS_LONG_FRAC * radius
		local placementAngleMod = 0
		local primaryTickAngleMod = 0
		local hasPrimaryTicks = false

		if props.StartAngle and props.EndAngle then
			placementAngleMod = props.EndAngle - props.StartAngle
			primaryTickAngleMod = props.StartAngle
			hasPrimaryTicks = true
		end

		for i = 1, count do
			local angle = math.pi + (i - 1) * angleStep - placementAngleMod
			local isPrimaryTick = hasPrimaryTicks and isMultipleOf90Degrees(angle - primaryTickAngleMod)
			local tickLength = isPrimaryTick and primaryTickLength or smallTickLength
			local tickWidth = isPrimaryTick and primaryTickWidth or smallTickWidth

			local cframe =
				props.HandleCFrame *
				CFrame.Angles(angle, 0, 0) *
				CFrame.new(0, 0, radius - 0.5 * smallTickLength)

			children["Tick" .. tostring(i)] = React.createElement("BoxHandleAdornment", {
				Adornee = workspace.Terrain,
				Shading = props.Shading,
				Transparency = 0,
				CFrame = cframe,
				Color3 = props.Color,
				Size = Vector3.new(tickWidth, tickWidth, tickLength),
				ZIndex = 0,
				AdornCullingMode = CULLING_MODE,
			})

			children["OnTopTick" .. tostring(i)] = React.createElement("BoxHandleAdornment", {
				Adornee = workspace.Terrain,
				Shading = props.Shading,
				Transparency = HANDLE_DIM_TRANSPARENCY,
				CFrame = cframe,
				Color3 = props.Color,
				Size = Vector3.new(tickWidth, tickWidth, tickLength),
				ZIndex = 0,
				AdornCullingMode = CULLING_MODE,
			})
		end
	end

	-- Draw the swept angle as circular section at the outer edge. The circular
	-- section shows the smallest swept angle back to the starting point.
	if props.StartAngle and props.EndAngle then
		local smallTickLength = HANDLE_TICK_RADIUS_FRAC * radius
		local primaryTickLength = HANDLE_TICK_RADIUS_LONG_FRAC * radius
		local outerWidth = 0.5 * (primaryTickLength - smallTickLength)

		local theta = props.EndAngle - props.StartAngle
		local startAngle = props.StartAngle

		if theta > math.pi then
			theta = theta - math.pi * 2
		end

		if theta < -math.pi then
			theta = theta + math.pi * 2
		end

		if theta < 0 then
			startAngle = startAngle + theta
			theta = math.abs(theta)
		end

		if math.abs(theta) > 0.001 then
			children.AngleSweepElement = React.createElement("CylinderHandleAdornment", {
				Adornee = workspace.Terrain,
				CFrame = props.HandleCFrame * CFrame.Angles(startAngle - math.pi / 2, math.pi / 2, math.pi / 2),
				Height = 0,
				Radius = radius,
				InnerRadius = 0,
				Angle = math.deg(theta),
				Color3 = props.Color,
				AlwaysOnTop = true,
				Transparency = 0.6,
				ZIndex = 0,
			})
		end

		local angleDisplayThickness = ANGLE_DISPLAY_THICKNESS * props.Scale
		local offset = CFrame.new(0, 0, -(radius + outerWidth) / 2)
		local cframe = props.HandleCFrame * CFrame.Angles(props.EndAngle, 0, 0) * offset

		children.EndAngleElement = React.createElement("CylinderHandleAdornment", {
			Adornee = workspace.Terrain,
			AlwaysOnTop = true,
			CFrame = cframe,
			Color3 = props.Color,
			Height = radius + outerWidth,
			Radius = angleDisplayThickness / 2,
			ZIndex = 0,
		})
	end

	return React.createElement("Folder", {}, children)
end

--[[
	Check if the mouse is over the rotation handle.

	The point of intersection between the mouse ray and plane perpendicular
	to the rotation axis is computed. The hit radius (distance from the origin
	of rotation to the intersection point) is compared to the gimbal radius,
	within a threshold to aid handle selection.
]]
function RotateHandleView.hitTest(props: Props, mouseRay: Ray): number?
	local cframe = props.HandleCFrame
	local unitRay = mouseRay.Unit

	local radiusOffset = props.RadiusOffset or 0.0
	local radius = (HANDLE_RADIUS + radiusOffset) * props.Scale
	local thickness = HANDLE_HITTEST_THICKNESS * props.Scale
	local normal = cframe.RightVector
	local point = cframe.Position

	local smallestDistance = math.huge
	local foundHit = false
	local hit, t

	-- Top ring
	local topPoint = point + normal * 0.5 * thickness
	t = Math.intersectRayPlane(unitRay.Origin, unitRay.Direction, topPoint, normal)

	if t >= 0 and t < smallestDistance then
		local mouseWorld = unitRay.Origin + unitRay.Direction * t
		local hitRadius = (mouseWorld - topPoint).Magnitude

		local distance = math.abs(hitRadius - radius)
		if distance < 0.5 * thickness then
			foundHit = true
			smallestDistance = t
		end
	end

	-- Bottom ring
	local bottomPoint = point - normal * 0.5 * thickness
	t = Math.intersectRayPlane(unitRay.Origin, unitRay.Direction, bottomPoint, -normal)

	if t >= 0 and t < smallestDistance then
		local mouseWorld = unitRay.Origin + unitRay.Direction * t
		local hitRadius = (mouseWorld - bottomPoint).Magnitude

		local distance = math.abs(hitRadius - radius)
		if distance < 0.5 * thickness then
			foundHit = true
			smallestDistance = t
		end
	end

	-- Get the ray in local space, so that we can use the intersectRayCylinder
	-- call for the intersection. The canonical normal of the cylinder is
	-- (1, 0, 0) which is what that call expects.
	local o = cframe:PointToObjectSpace(unitRay.Origin)
	local d = cframe:VectorToObjectSpace(unitRay.Direction)

	-- Inner Cylinder
	local innerRadius = radius - 0.5 * thickness
	hit, t = Math.intersectRayCylinder(o, d, innerRadius, thickness)

	if hit and t < smallestDistance then
		foundHit = true
		smallestDistance = t
	end

	-- Outer Cylinder
	local outerRadius = radius + 0.5 * thickness
	hit, t = Math.intersectRayCylinder(o, d, outerRadius, thickness)

	if hit and t < smallestDistance then
		foundHit = true
		smallestDistance = t
	end

	if foundHit then
		return smallestDistance
	else
		return nil
	end
end

return RotateHandleView


--[[
	Component that displays a spherical scale handle.
]]

local ScaleHandleView = {}

local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent
local React = require(Packages.React)

-- Dragger Framework
local Math = require(DraggerFramework.Utility.Math)

local CULLING_MODE = Enum.AdornCullingMode.Never

local HANDLE_RADIUS = 0.5
local HANDLE_RADIUS_HOVERED_SCALE = 1.15 -- Radius scale when handle is hovered
local HANDLE_HITTEST_RADIUS_SCALE = 2.5 -- Radius scale for hit testing

local HANDLE_TRANSPARENCY_START = 0.75
local HANDLE_TRANSPARENCY_END = 0.2
local HANDLE_OFFSET = 1.5

local HANDLE_THIN_BY_FRAC = 0.34

export type Props = {
	NormalId: number?,
	HandleCFrame: CFrame,
	Color: Color3,
	Scale: number,
	Hovered: boolean,
	Thin: boolean,
}

local function getDebugSettingValue(name: string, defaultValue: number)
	local setting = workspace:FindFirstChild(name)

	if setting and setting:IsA("NumberValue") then
		return setting.Value
	end

	return defaultValue
end

function ScaleHandleView.render(props: Props)
	-- DEBUG: Allow designers to play with handle settings.
	-- Remove before shipping!
	HANDLE_OFFSET = getDebugSettingValue("ScaleHandleOffset", 1.5)
	HANDLE_RADIUS = getDebugSettingValue("ScaleHandleRadius", 0.5)
	HANDLE_TRANSPARENCY_START = getDebugSettingValue("ScaleHandleTransparencyStart", 0.75)
	HANDLE_TRANSPARENCY_END = getDebugSettingValue("ScaleHandleTransparencyEnd", 0.2)

	local children = {}
	local color = props.Color
	local cframe = props.HandleCFrame * CFrame.new(0, 0, -HANDLE_OFFSET * props.Scale)
	local radius = HANDLE_RADIUS * props.Scale

	if props.Thin then
		radius = radius * HANDLE_THIN_BY_FRAC
	end

	if not props.Hovered then
		children.HiddenHandle = React.createElement("SphereHandleAdornment", {
			Adornee = workspace.Terrain,
			AlwaysOnTop = true,
			CFrame = cframe,
			Color3 = color,
			Radius = radius,
			Transparency = HANDLE_TRANSPARENCY_START,
			ZIndex = 1,
			AdornCullingMode = CULLING_MODE,
		})
	end

	local transparencyEnd = HANDLE_TRANSPARENCY_END

	if props.Hovered then
		radius = radius * HANDLE_RADIUS_HOVERED_SCALE
		transparencyEnd = 0
	end

	children.Handle = React.createElement("SphereHandleAdornment", {
		Adornee = workspace.Terrain,
		Shading = props.Hovered and Enum.AdornShading.AlwaysOnTop or Enum.AdornShading.Shaded,
		AlwaysOnTop = props.Hovered,
		CFrame = cframe,
		Color3 = color,
		Radius = radius,
		Transparency = transparencyEnd,
		ZIndex = 0,
		AdornCullingMode = CULLING_MODE,
	})

	return React.createElement("Folder", {}, children)
end

--[[
	Check if the mouse is over the scale handle.
	DON'T include the hitTest radius. We will deal with the scale handles by
	first checking if we hit any of them, and taking that one if we do.
	If we don't actually hit any, then we see if we're within the hitTest radius
	for any of them, and if we are take the closest one.
]]
function ScaleHandleView.hitTest(props: Props, mouseRay: Ray): number?
	local radius = HANDLE_RADIUS * props.Scale
	local unitRay = mouseRay.Unit

	local worldPosition = props.HandleCFrame * Vector3.new(0, 0, -HANDLE_OFFSET * props.Scale)
	local result, t = Math.intersectRaySphere(unitRay.Origin, unitRay.Direction, worldPosition, radius)

	if result then
		return t
	else
		return nil
	end
end

function ScaleHandleView.distanceFromHandle(props: Props, mouseRay: Ray): number
	local hitTestRadius = HANDLE_RADIUS * props.Scale * HANDLE_HITTEST_RADIUS_SCALE
	local worldPosition = props.HandleCFrame * Vector3.new(0, 0, -HANDLE_OFFSET * props.Scale)

	local rayDir = mouseRay.Direction.Unit
	local projectedLength = (worldPosition - mouseRay.Origin):Dot(rayDir)

	local projectedPoint = mouseRay.Origin + rayDir * projectedLength
	local distanceToRay = (worldPosition - projectedPoint).Magnitude

	return distanceToRay - hitTestRadius
end

return ScaleHandleView


--[[
	Component that displays a dot of fixed size at the given position.
	Intended to be used to show the center of the current selection.
]]
local DraggerFramework = script.Parent.Parent
local Packages = DraggerFramework.Parent

local React = require(Packages.React)
local Colors = require(DraggerFramework.Utility.Colors)

export type Props = {
	Position: Vector3?,
	Size: number?,

	BackgroundColor3: Color3?,
	BorderColor3: Color3?,
}

local function SelectionDot(props: Props): React.ReactElement?
	local screenPosition, onScreen = workspace.CurrentCamera:WorldToScreenPoint(props.Position or Vector3.zero)

	if not onScreen then
		return nil
	end

	local size = props.Size or 3

	return React.createElement("ScreenGui", {}, {
		React.createElement("Frame", {
			BackgroundColor3 = props.BackgroundColor3 or Colors.WHITE,
			BorderColor3 = props.BorderColor3 or Colors.BLACK,
			BorderSizePixel = 1,
			Position = UDim2.fromOffset(screenPosition.X, screenPosition.Y),
			Selectable = false,
			Size = UDim2.fromOffset(size, size),
		})
	})
end

return SelectionDot


--[[
	Component that displays a SelectionBox with an arbitrary position and size,
	without having to create an adornee.

	Internally, StandaloneSelectionBox creates a transparent adornee with the
	correct position/size and parents it to CoreGui to prevent implementation
	details from leaking into the workspace.
]]

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

local React = require(Packages.React)
local ReactRoblox = require(Packages.ReactRoblox)

export type Props = {
	CFrame: CFrame,
	Size: Vector3,
	Color: Color3,
	LineThickness: number,
	Container: Instance?, -- Optional parent for the internal dummy part. Defaults to CoreGui.
}

local function StandaloneSelectionBox(props: Props)
	local ones = Vector3.new(1, 1, 1)
	local dummyPartSize = props.Size:Min(ones)
	local dummyPartOffset = props.Size / 2 - dummyPartSize / 2

	local container = props.Container or game:GetService("CoreGui")
	local dummyPartRef = React.useRef(nil :: Model?)

	return ReactRoblox.createPortal({
		DummyModel = React.createElement("Model", {
			ref = dummyPartRef,
		}, {
			DummyPart1 = React.createElement("Part", {
				Shape = Enum.PartType.Block,
				Anchored = true,
				CanCollide = false,
				CFrame = props.CFrame * CFrame.new(-dummyPartOffset),
				Size = dummyPartSize,
				Transparency = 0,
			}),

			DummyPart2 = React.createElement("Part", {
				Shape = Enum.PartType.Block,
				Anchored = true,
				CanCollide = false,
				CFrame = props.CFrame * CFrame.new(dummyPartOffset),
				Size = dummyPartSize,
				Transparency = 0,
			})
		}),

		SelectionBox = React.createElement("SelectionBox", {
			Adornee = dummyPartRef,
			Color3 = props.Color,
			LineThickness = props.LineThickness,
			SurfaceTransparency = 1,
			Transparency = 0,
		})
	}, container)
end

return StandaloneSelectionBox


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

local Packages = DraggerFramework.Parent
local React = require(Packages.React)

local SummonHandlesHider = {}

-- When the user has summoned the handles for at duration, the hint that they
-- can be summoned will permanently become hidden. We do it this way so that if
-- you just slightly tap the tab key you won't lose the hint, you actually have
-- to hold it down.
local sTotalSummonHintTime = 2

-- Was the hint hidden in a previous session?
local SETTING_NAME = "CoreDraggersSummonHintHidden"
local sWasPreviouslyHidden = nil :: boolean?

type DraggerContext = DraggerContext.Class

type Props = {
	DraggerContext: DraggerContext,
}

function SummonHandlesHider.render(props: Props)
	React.useEffect(function ()
		local startTime = os.clock()

		return function ()
			local duration = os.clock() - startTime
			sTotalSummonHintTime -= duration

			if sWasPreviouslyHidden == nil then
				sWasPreviouslyHidden = props.DraggerContext:getSetting(SETTING_NAME)
			end

			if sTotalSummonHintTime <= 0 and not sWasPreviouslyHidden then
				props.DraggerContext:setSetting(SETTING_NAME, true)
				sWasPreviouslyHidden = true
			end
		end
	end, {})
end

function SummonHandlesHider.hasSeenEnough(draggerContext: DraggerContext)
	if sWasPreviouslyHidden == nil then
		sWasPreviouslyHidden = draggerContext:getSetting(SETTING_NAME)
	end

	return sWasPreviouslyHidden or sTotalSummonHintTime <= 0
end

return SummonHandlesHider


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

local Packages = DraggerFramework.Parent
local ReactRoblox = require(Packages.ReactRoblox)
local React = require(Packages.React)

-- How much space between the note and the window edge
local EDGE_PADDING = 2

local FRAME_PADDING = 3
local TEXT_MARGIN = 2

type PaddingProps = {
	Padding: number,
}

export type Props = {
	DraggerContext: DraggerContext.Class,
	InView: boolean,
}

type Node = React.Node

local function SimplePadding(props: PaddingProps): Node
	local padding = React.useMemo(function()
		return UDim.new(0, props.Padding)
	end, { props.Padding :: any })

	return React.createElement("UIPadding", {
		PaddingBottom = padding,
		PaddingRight = padding,
		PaddingLeft = padding,
		PaddingTop = padding,
	}) :: Node
end

local function SummonHandlesNote(props: Props)
	if props.InView then
		return nil
	end

	local viewportSize = props.DraggerContext:getViewportSize()
	local background = props.DraggerContext:getThemeColor(Enum.StudioStyleGuideColor.Tooltip)
	local border = props.DraggerContext:getThemeColor(Enum.StudioStyleGuideColor.Border)
	local foreground = props.DraggerContext:getThemeColor(Enum.StudioStyleGuideColor.MainText)
	local tabBubble = props.DraggerContext:getThemeColor(Enum.StudioStyleGuideColor.DimmedText)
	local guiParent = props.DraggerContext:getGuiParent()

	return ReactRoblox.createPortal({
		SummonHandlesNoteGui = React.createElement("ScreenGui", {}, {
			Frame = React.createElement("Frame", {
				AnchorPoint = Vector2.new(0.5, 0),
				AutomaticSize = Enum.AutomaticSize.XY,
				BackgroundColor3 = background,
				BorderColor3 = border,
				Position = UDim2.new(0, viewportSize.X / 2, 0, EDGE_PADDING),
			}, {
				Padding = React.createElement(SimplePadding, {
					Padding = FRAME_PADDING
				}),

				Layout = React.createElement("UIListLayout", {
					FillDirection = Enum.FillDirection.Horizontal,
					SortOrder = Enum.SortOrder.LayoutOrder,
					Padding = UDim.new(0, FRAME_PADDING),
				}),
				Tab = React.createElement("TextLabel", {
					Text = props.DraggerContext:getText("SummonPivot", "TabText"),
					TextColor3 = foreground,
					BackgroundColor3 = tabBubble,
					AutomaticSize = Enum.AutomaticSize.XY,
					LayoutOrder = 1,
				}, {
					Padding = React.createElement(SimplePadding, {
						Padding = TEXT_MARGIN
					}),

					Corner = React.createElement("UICorner", {
						CornerRadius = UDim.new(0, 4),
					}),
				}),
				Text = React.createElement("TextLabel", {
					Text = props.DraggerContext:getText("SummonPivot", "SummonText"),
					TextColor3 = foreground,
					AutomaticSize = Enum.AutomaticSize.XY,
					BackgroundTransparency = 1,
					LayoutOrder = 2,
				}, {
					Padding = React.createElement(SimplePadding, {
						Padding = TEXT_MARGIN
					}),
				})
			})
		})
	}, guiParent)
end

return SummonHandlesNote


-- Libraries
local DraggerFramework = script.Parent.Parent
local Utility = DraggerFramework.Utility

local Shared = DraggerFramework.Parent
local React = require(Shared.React)

local Colors = require(Utility.Colors)
local SelectionInfo = require(Utility.SelectionInfo)
local computeDraggedDistance = require(Utility.computeDraggedDistance)

local Components = DraggerFramework.Components
local MoveHandleView = require(Components.MoveHandleView)
local StandaloneSelectionBox = require(Components.StandaloneSelectionBox)

local Implementation = DraggerFramework.Implementation
local DraggerContext = require(Implementation.DraggerContext)
local DraggerToolModel = require(Implementation.DraggerToolModel)

local ALWAYS_ON_TOP = true

local MoveHandles = {}
MoveHandles.__index = MoveHandles

type MoveHandleDef = {
	HideWhenTempPart: boolean?,
	Offset: CFrame,
	Color: Color3,
}

type HandleProps = MoveHandleView.Props
type SelectionInfo = SelectionInfo.Class
type DraggerContext = DraggerContext.Class
type DraggerToolModel = DraggerToolModel.Class

local MoveHandleDefinitions: { [string]: MoveHandleDef } = {
	MinusZ = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(1, 0, 0), Vector3.new(0, 1, 0)),
		Color = Colors.Z_AXIS,
	},
	PlusZ = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(1, 0, 0), Vector3.new(0, -1, 0)),
		Color = Colors.Z_AXIS,
	},
	MinusY = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(0, 0, 1), Vector3.new(1, 0, 0)),
		Color = Colors.Y_AXIS,
		HideWhenTempPart = true,
	},
	PlusY = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(0, 0, 1), Vector3.new(-1, 0, 0)),
		Color = Colors.Y_AXIS,
		HideWhenTempPart = true,
	},
	MinusX = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(0, 1, 0), Vector3.new(0, 0, 1)),
		Color = Colors.X_AXIS,
	},
	PlusX = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.new(0, 1, 0), Vector3.new(0, 0, -1)),
		Color = Colors.X_AXIS,
	},
}

type Props = {
	Outset: number?,
	Summonable: boolean?,
	ShowBoundingBox: boolean?,
}

type MoveHandles = {
	_handles: {
		[string]: HandleProps
	},

	_draggingHandleId: string?,
	_draggingOriginalBoundingBoxCFrame: CFrame?,
	_lastGlobalTransformForRender: CFrame?,
	_draggingHandleFrac: number?,
	_startDistance: number,
	_axis: Vector3,

	_boundingBox: {
		CFrame: CFrame,
		Size: Vector3,
	},

	_basisOffset: CFrame?,
	_summonBasisOffset: CFrame?,

	_schema: any,
	_selectionWrapper: any,
	_selectionInfo: SelectionInfo?,
	_implementation: Implementation,

	_props: Props,
	_draggerContext: DraggerContext,
}

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

export type Implementation = {
	beginDrag: (self: any, selection: { [number]: Instance }, selectionInfo: any) -> (),
	updateDrag: (self: any, globalTransform: CFrame) -> CFrame,
	endDrag: (self: any) -> any?,
	render: (self: any, handleCFrame: CFrame) -> React.ReactElement<any>,
	getSnapPoints: (self: any) -> {CFrame}?,
}

function MoveHandles.new(
	draggerContext: DraggerContext,
	props: Props,
	implementation: any
): Class
	local self: MoveHandles = {
		_handles = {},

		_props = props or {
			Summonable = true,
		},

		_draggerContext = draggerContext,
		_implementation = implementation,
		_tabKeyDown = false,
		_selectionInfo = nil,

		_boundingBox = {
			CFrame = CFrame.identity,
			Size = Vector3.one,
		},

		_axis = Vector3.zero,
		_startDistance = 0,
	}

	return setmetatable(self, MoveHandles) :: Class
end

function MoveHandles.update(self: Class, draggerToolModel: DraggerToolModel, selectionInfo: SelectionInfo)
	if not self._draggingHandleId then
		-- Don't clobber these fields while we're dragging because we're
		-- updating the bounding box in a smart way given how we're moving the
		-- parts.
		local cframe, offset, size = selectionInfo:getBoundingBox()
		self._basisOffset = CFrame.new(-offset) -- negative since relative to bounding box
		self._boundingBox = {
			Size = size,
			CFrame = cframe * CFrame.new(offset),
		}
		self._schema = draggerToolModel:getSchema()
		self._selectionWrapper = draggerToolModel:getSelectionWrapper()
		self._selectionInfo = selectionInfo
	end

	self:_updateHandles()
end

function MoveHandles._getBasisOffset(self: Class)
	return self._summonBasisOffset or self._basisOffset or CFrame.identity
end

function MoveHandles.shouldBiasTowardsObjects(self: Class)
	return false
end

function MoveHandles.hitTest(self: Class, mouseRay, ignoreExtraThreshold)
	local closestHandleId, closestHandleDistance = nil, math.huge

	for handleId, handleProps in pairs(self._handles) do
		local distance = MoveHandleView.hitTest(handleProps, mouseRay)

		if distance and distance < closestHandleDistance then
			closestHandleDistance = distance
			closestHandleId = handleId
		end
	end

	return closestHandleId, closestHandleDistance, ALWAYS_ON_TOP
end

function MoveHandles.render(self: Class, hoveredHandleId: string?)
	local children = {} :: {
		[string]: React.Node
	}

	if self._draggingHandleId and self._handles[self._draggingHandleId] then
		local handleProps = self._handles[self._draggingHandleId]

		children[self._draggingHandleId] = React.createElement<<any>>(MoveHandleView.render, {
			GuiParent = self._draggerContext:getGuiParent(),
			Axis = handleProps.Axis,
			Outset = handleProps.Outset,
			Color = handleProps.Color,
			Scale = handleProps.Scale,
			AlwaysOnTop = ALWAYS_ON_TOP,
			Hovered = false,
		}) :: React.Node

		for otherHandleId, otherHandleProps in pairs(self._handles) do
			if otherHandleId ~= self._draggingHandleId then
				children[otherHandleId] = React.createElement<<any>>(MoveHandleView.render, {
					GuiParent = self._draggerContext:getGuiParent(),
					Color = otherHandleProps.Color,
					Axis = otherHandleProps.Axis,
					Outset = handleProps.Outset,
					Scale = otherHandleProps.Scale,
					AlwaysOnTop = ALWAYS_ON_TOP,
					Thin = true,
				}) :: React.Node
			end
		end

		children.ImplementationRendered = self._implementation:render(self._boundingBox.CFrame * self:_getBasisOffset())
	else
		for handleId, handleProps in pairs(self._handles) do
			local color = handleProps.Color
			local hovered = (handleId == hoveredHandleId)
			local shading = hovered and Enum.AdornShading.XRay or Enum.AdornShading.XRayShaded

			children[handleId] = React.createElement<<any>>(MoveHandleView.render, {
				GuiParent = self._draggerContext:getGuiParent(),
				Axis = handleProps.Axis,
				Outset = handleProps.Outset,
				Color = color,
				Scale = handleProps.Scale,
				Shading = shading,
				Hovered = hovered,
			}) :: React.Node
		end
	end

	if self._props.ShowBoundingBox and #self._selectionWrapper:get() > 1 then
		children.SelectionBoundingBox = React.createElement<<any>>(StandaloneSelectionBox, {
			CFrame = self._boundingBox.CFrame,
			Size = self._boundingBox.Size,
			Color = self._draggerContext:getSelectionBoxColor(),
			LineThickness = self._draggerContext:getHoverLineThickness(),
			Container = self._draggerContext:getGuiParent(),
		}) :: React.Node
	end

	return React.createElement("Folder", {}, children)
end

function MoveHandles.mouseDown(self: Class, mouseRay: Ray, handleId: string)
	local boundingBox = self._boundingBox
	self._draggingHandleId = handleId

	if boundingBox then
		self._draggingOriginalBoundingBoxCFrame = boundingBox.CFrame
	else
		self._draggingOriginalBoundingBoxCFrame = nil
	end

	-- Calculate fraction of the way along the handle to "stick" the cursor to
	if self._handles[handleId] then
		self:_setupMoveAtCurrentBoundingBox(mouseRay)

		local handleProps = self._handles[handleId]
		local handleOffset, handleLength =
			MoveHandleView.getHandleDimensionForScale(handleProps.Scale)

		local startDist = self._startDistance or 0
		self._draggingHandleFrac = (startDist - handleOffset) / handleLength
	end

	self._implementation:beginDrag(self._selectionWrapper:get(), self._selectionInfo)
end

function MoveHandles._setupMoveAtCurrentBoundingBox(self: Class, mouseRay: Ray)
	local boundingBox = self._boundingBox
	local draggingHandleId = self._draggingHandleId
	local offset = draggingHandleId and MoveHandleDefinitions[draggingHandleId].Offset

	if boundingBox and offset then
		local axis = (boundingBox.CFrame * offset).LookVector
		self._axis = axis

		local hasDistance, distance = self:_getDistanceAlongAxis(mouseRay)
		self._startDistance = hasDistance and distance or 0.0
		self._lastGlobalTransformForRender = CFrame.identity
	end
end

function MoveHandles._setMidMoveBoundingBox(self: Class, newBoundingBoxCFrame: CFrame)
	self._boundingBox.CFrame = newBoundingBoxCFrame
end

--[[
	Returns the distance the mouse cursor was dragged along handle axis
]]
function MoveHandles._getDistanceAlongAxis(self: Class, mouseRay: Ray): (boolean, number)
	local draggedFrame = self._draggingOriginalBoundingBoxCFrame or CFrame.identity
	local dragStartPosition = draggedFrame.Position
	local dragDirection = self._axis.Unit
	return computeDraggedDistance(dragStartPosition, dragDirection, mouseRay)
end

--[[
	We want to keep the mouse cursor snapped to a point a constant fraction of
	the way down the handle length over the whole duration of a move. This is
	non-trivial, as the distance along the handle depends on the scale of the
	handle, but the scale of the handle depends on how far it has been moved
	relative to the camera.

	We must solve a non-linear equation satisfying the constraint:
		fraction of distance along handle at new location
		equals
		fraction of distance along handle at start location

	Do this using a binary search over the potential solution space.
]]
function MoveHandles._solveForAdjustedDistance(self: Class, unadjustedDistance: number): number
	local draggingHandleId = self._draggingHandleId

	if not draggingHandleId or not self._handles[draggingHandleId] then
		return unadjustedDistance
	end

	local handleRotation = MoveHandleDefinitions[draggingHandleId].Offset

	local function getScaleForDistance(distance: number)
		local bboxCFrame = self._draggingOriginalBoundingBoxCFrame or CFrame.identity
		local startDist = self._startDistance or 0
		local axis = self._axis

		local boundingBoxAtDistance = bboxCFrame + (axis * (distance - startDist))
		local baseCFrameAtDistance = boundingBoxAtDistance * handleRotation

		return self._draggerContext:getHandleScale(baseCFrameAtDistance.Position)
	end

	local function getHandleFracForDistance(distance: number)
		local scale = getScaleForDistance(distance)
		local startDist = self._startDistance or 0

		local handleOffset, handleLength = MoveHandleView.getHandleDimensionForScale(scale)
		local intoDist = unadjustedDistance - distance + startDist

		return (intoDist - handleOffset) / handleLength
	end

	local function getHandleLengthForDistance(distance: number)
		local _, handleLength = MoveHandleView.getHandleDimensionForScale(getScaleForDistance(distance))
		return handleLength
	end

	-- Establish the bounds on the binary search for a good distance. Using
	-- max(originalLength, newLength) is a bit of a hack.
	-- abs(originalLength - newLength) is the more mathematically appropriate
	-- expression for how much unadjustedDistance might be off by, but it's too
	-- "sharp", and slightly misses including the solution we're looking for
	-- sometimes. Using the larger interval with max works too, it's just doing
	-- slightly more work than it should really have to.
	local originalHandleLength = getHandleLengthForDistance(0)
	local currentHandleLength = getHandleLengthForDistance(unadjustedDistance)
	local handleSizeDifference = math.max(originalHandleLength, currentHandleLength)
	local minPossibleDistance = unadjustedDistance - handleSizeDifference
	local maxPossibleDistance = unadjustedDistance + handleSizeDifference
	local fracAtMin = getHandleFracForDistance(minPossibleDistance)
	local fracAtMax = getHandleFracForDistance(maxPossibleDistance)

	-- Do the binary search
	local iterationCount = 0

	while math.abs(minPossibleDistance - maxPossibleDistance) > 0.0001 and iterationCount < 32 do
		local mid = 0.5 * (minPossibleDistance + maxPossibleDistance)
		local fracAtMid = getHandleFracForDistance(mid)
		local handleFrac = self._draggingHandleFrac or 0

		if (handleFrac - fracAtMid) * (fracAtMax - fracAtMin) > 0 then
			minPossibleDistance = mid
			fracAtMin = fracAtMid
		else
			maxPossibleDistance = mid
			fracAtMax = fracAtMid
		end
	end

	return minPossibleDistance
end

function MoveHandles._getSnappedDelta(self: Class, delta: number)
	local snapPoints

	if self._implementation.getSnapPoints then
		snapPoints = self._implementation:getSnapPoints()
	end

	if snapPoints then
		-- We deliberately don't use self:_getBasisOffset() here, because we want to snap
		-- the actual basis point of the selection to the snap points, not the position the
		-- handles are visually at.
		local bboxCFrame = self._draggingOriginalBoundingBoxCFrame or CFrame.identity
		local basisOffset = self._basisOffset or CFrame.identity

		local basePoint = (bboxCFrame * basisOffset).Position
		local axis = self._axis or Vector3.zero

		local maxDistanceAlongAxis = -math.huge
		local minDistanceAlongAxis = math.huge

		local closenessToDelta = math.huge
		local bestDistanceAlongAxis = math.huge

		for _, point in ipairs(snapPoints) do
			local dist = (point.Position - basePoint):Dot(axis)
			maxDistanceAlongAxis = math.max(maxDistanceAlongAxis, dist)
			minDistanceAlongAxis = math.min(minDistanceAlongAxis, dist)

			local absDist = math.abs(dist - delta)
			if absDist < closenessToDelta then
				closenessToDelta = absDist
				bestDistanceAlongAxis = dist
			end
		end

		if delta > maxDistanceAlongAxis or delta < minDistanceAlongAxis then
			-- Point is outside of the bounds of the snap points, use the grid
			-- snap instead if it is closer than the snap point.
			local gridSnappedDelta = self._draggerContext:snapToGridSize(delta)
			local closenessToGrid = math.abs(gridSnappedDelta - delta)

			if closenessToDelta < closenessToGrid then
				return bestDistanceAlongAxis
			else
				return gridSnappedDelta
			end
		else
			-- Point is within the bounds of the snap points, use the distance
			-- to the closest snap point.
			return bestDistanceAlongAxis
		end
	else
		return self._draggerContext:snapToGridSize(delta)
	end
end

function MoveHandles.mouseDrag(self: Class, mouseRay: Ray)
	local hasDistance, distance = self:_getDistanceAlongAxis(mouseRay)

	if not hasDistance then
		return
	end

	local handleId = self._draggingHandleId

	if not (handleId and self._handles[handleId]) then
		return
	end

	local startDist = self._startDistance or 0
	local delta = self:_solveForAdjustedDistance(distance) - startDist
	local snappedDelta = self:_getSnappedDelta(delta)

	-- Let the implementation figure out what global transform can actually be
	-- applied (because there may be collisions / constraints involved)
	local axis = self._axis or Vector3.zero

	local actualGlobalTransform = self._implementation:updateDrag(CFrame.new(axis * snappedDelta))
	assert(actualGlobalTransform ~= nil, "Did not return a transform from updateDrag.")

	-- Set the new resulting CFrame
	local bboxCFrame = self._draggingOriginalBoundingBoxCFrame or CFrame.identity
	self:_setMidMoveBoundingBox(actualGlobalTransform * bboxCFrame)
	self._lastGlobalTransformForRender = actualGlobalTransform
end

function MoveHandles.mouseUp(self: Class, mouseRay: Ray)
	self._draggingHandleId = nil
	local newSelectionInfoHint = self._implementation:endDrag()
	self._schema.addUndoWaypoint(self._draggerContext, "Axis Move Selection")
	return newSelectionInfoHint
end

function MoveHandles._updateHandles(self: Class)
	local selectionInfo = self._selectionInfo

	if selectionInfo and selectionInfo:isEmpty() then
		self._handles = {}
	else
		for handleId, handleDef in pairs(MoveHandleDefinitions) do
			local bboxCFrame = self._boundingBox.CFrame
			local handleBaseCFrame = bboxCFrame * self:_getBasisOffset() * handleDef.Offset

			self._handles[handleId] = {
				Outset = self._props.Outset,
				Axis = handleBaseCFrame,
				Color = handleDef.Color,
				Scale = self._draggerContext:getHandleScale(handleBaseCFrame.Position),
				AlwaysOnTop = ALWAYS_ON_TOP,
				Thin = false,
				Hovered = false,
			}
		end
	end
end

return MoveHandles


-- Libraries
local DraggerFramework = script.Parent.Parent
local Shared = DraggerFramework.Parent
local React = require(Shared.React)

-- Dragger Framework
local Utility = DraggerFramework.Utility
local Colors = require(Utility.Colors)
local Math = require(Utility.Math)
local roundRotation = require(Utility.roundRotation)

local Components = DraggerFramework.Components
local RotateHandleView = require(Components.RotateHandleView)
local StandaloneSelectionBox = require(Components.StandaloneSelectionBox)

local Implementation = DraggerFramework.Implementation
local DraggerContext = require(Implementation.DraggerContext)
local DraggerToolModel = require(Implementation.DraggerToolModel)

-- The minimum rotate increment to display snapping increments for (below this
-- increment there are so many points that they become visual noise)
local MIN_ROTATE_INCREMENT = 5.0

local RIGHT_ANGLE = math.pi / 2
local RIGHT_ANGLE_EXACT_THRESHOLD = 0.001

local RotateHandles = {}
RotateHandles.__index = RotateHandles

--[[
	Axis of rotation is the CFrame right vector.
	RadiusOffset slightly bumps the arc radii so that we can control which one
	shows up on top where they intersect.
]]

type RotateHandleDef = {
	Offset: CFrame,
	Color: Color3,
	RadiusOffset: number,
	HideWhenTempPart: boolean?,
}

type Props = {
	Summonable: boolean?,
	ShowBoundingBox: boolean?,
}

type HandleProps = RotateHandleView.Props
type DraggerContext = DraggerContext.Class
type DraggerToolModel = DraggerToolModel.Class

export type Implementation = {
	beginDrag: <T>(self: T, selection: { [number]: Instance }, selectionInfo: any) -> (),
	updateDrag: <T>(self: T, globalTransform: CFrame) -> CFrame,
	endDrag: <T>(self: T) -> any?,
	render: <T>(self: T, handleCFrame: CFrame) -> React.ReactElement<any>,
}

type RotateHandles = {
	_draggerContext: DraggerContext,
	_handles: { [string]: HandleProps },
	_props: Props,
	_implementation: Implementation,

	_draggingHandleId: string?,
	_handleCFrame: CFrame,
	_startAngle: number,
	_draggingLastGoodDelta: number,
	_originalBoundingBoxCFrame: CFrame?,
	_lastGlobalTransformForRender: CFrame?,

	_boundingBox: {
		CFrame: CFrame,
		Size: Vector3,
	},

	_basisOffset: CFrame,
	_selectionInfo: any,
	_selectionWrapper: any,
	_tabKeyDown: boolean,

	_schema: any,
	_scale: number,
}

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

local RotateHandleDefinitions: { [string]: RotateHandleDef } = {
	XAxis = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.xAxis, Vector3.yAxis, Vector3.zAxis),
		Color = Colors.X_AXIS,
		RadiusOffset = 0.00,
		HideWhenTempPart = true,
	},

	YAxis = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.yAxis, Vector3.zAxis, Vector3.xAxis),
		Color = Colors.Y_AXIS,
		RadiusOffset = 0.01,
	},

	ZAxis = {
		Offset = CFrame.fromMatrix(Vector3.zero, Vector3.zAxis, Vector3.xAxis, Vector3.yAxis),
		Color = Colors.Z_AXIS,
		RadiusOffset = 0.02,
		HideWhenTempPart = true,
	},
}

local function isRightAngle(angleDelta: number)
	local snappedTo90 = math.floor((angleDelta / RIGHT_ANGLE) + 0.5) * RIGHT_ANGLE
	return math.abs(snappedTo90 - angleDelta) < RIGHT_ANGLE_EXACT_THRESHOLD
end

local function getRotationTransform(mainCFrame: CFrame, axisVector: Vector3, delta: number, rotateIncrement: number)
	local localAxis = mainCFrame:VectorToObjectSpace(axisVector)
	local rotationCFrame = CFrame.fromAxisAngle(localAxis, delta)

	-- Special case rotations in 90 degree increments as a permutation of
	-- the identity matrix rather than numerically calculating an axis
	-- rotation which would introduce floating point error.
	if rotateIncrement > 0 and isRightAngle(delta) then
		-- Since we know that this is already almost a right angle rotation
		-- thanks to the isRightAngle check, we can find the pure
		-- permutation rotation matrix simply by rounding the rotation
		-- matrix elements to the nearest integer.
		rotationCFrame = roundRotation(rotationCFrame)
	end

	-- Convert the rotation to a global space transformation
	return mainCFrame * rotationCFrame * mainCFrame:Inverse()
end

--[[
	Find the angle around the rotation axis where the mouse ray intersects the
	plane perpendicular to the rotation axis.
]]
local function rotationAngleFromRay(cframe: CFrame, unitRay: Ray): number?
	local t = Math.intersectRayPlane(unitRay.Origin, unitRay.Direction, cframe.Position, cframe.RightVector)

	if t >= 0 then
		local mouseWorld = unitRay.Origin + unitRay.Direction * t
		local direction = (mouseWorld - cframe.Position).Unit

		local rx = cframe.LookVector:Dot(direction)
		local ry = cframe.UpVector:Dot(direction)

		-- Remap into [0, 2pi] for better snapping behavior with not
		-- evenly divisible snapping angles.
		local theta = math.atan2(ry, rx)

		if theta < 0 then
			return 2 * math.pi + theta
		else
			return theta
		end
	end

	return nil
end

local function snapToRotateIncrementIfNeeded(angle: number, rotateIncrement: number)
	if rotateIncrement > 0 then
		local angleIncrement = math.rad(rotateIncrement)
		local snappedAngle = math.floor(angle / angleIncrement + 0.5) * angleIncrement

		local deltaFromCompleteRotation = math.abs(angle - math.pi * 2)
		local deltaFromSnapPoint = math.abs(angle - snappedAngle)

		if deltaFromCompleteRotation < deltaFromSnapPoint then
			-- For rotate increments which don't evenly divide the
			-- circle, there won't be a snap point at 360 degrees, so
			-- this if statement manually adds a special case for that
			-- additional snap point.
			return 0
		else
			return snappedAngle
		end
	else
		return angle
	end
end

function RotateHandles.new(draggerContext: DraggerContext, props: Props?, implementation: any): Class
	local self = {}

	self._draggerContext = draggerContext
	self._handles = {}

	self._props = props or {
		Summonable = true,
	}

	self._handleCFrame = CFrame.identity
	self._startAngle = 0

	self._implementation = implementation
	self._tabKeyDown = false

	return setmetatable(self, RotateHandles) :: Class
end

function RotateHandles.update(self: Class, draggerToolModel: DraggerToolModel, selectionInfo: any)
	if not self._draggingHandleId then
		local cframe, offset, size = selectionInfo:getBoundingBox()

		self._boundingBox = {
			Size = size,
			CFrame = cframe * CFrame.new(offset),
		}

		self._basisOffset = CFrame.new(-offset)
		self._selectionInfo = selectionInfo
		self._selectionWrapper = draggerToolModel:getSelectionWrapper()
		self._schema = draggerToolModel:getSchema()
		self._scale = self._draggerContext:getHandleScale(self._boundingBox.CFrame.Position)
	end

	self:_updateHandles()
end

function RotateHandles.shouldBiasTowardsObjects(self: Class): boolean
	return false
end

function RotateHandles.hitTest(self: Class, mouseRay: Ray, ignoreExtraThreshold: boolean)
	local closestHandleId, closestHandleDistance = nil, math.huge

	for handleId, handleProps in pairs(self._handles) do
		local distance = RotateHandleView.hitTest(handleProps, mouseRay)

		if distance and distance < closestHandleDistance then
			closestHandleDistance = distance
			closestHandleId = handleId
		end
	end

	local alwaysOnTop = true
	return closestHandleId, closestHandleDistance, alwaysOnTop
end

function RotateHandles.render(self: Class, hoveredHandleId: string?)
	local children = {} :: {
		[string]: React.ReactElement<any>,
	}

	local increment = self._draggerContext:getRotateIncrement()
	local tickAngle

	if increment >= MIN_ROTATE_INCREMENT then
		tickAngle = math.rad(increment)
	end

	if self._draggingHandleId and self._handles[self._draggingHandleId] then
		local handleProps = self._handles[self._draggingHandleId]

		children[self._draggingHandleId] = React.createElement<<any>>(RotateHandleView.render, {
			HandleCFrame = handleProps.HandleCFrame,
			Color = handleProps.Color,
			Shading = Enum.AdornShading.XRay,
			StartAngle = self._startAngle - self._draggingLastGoodDelta,
			EndAngle = self._startAngle,
			Scale = self._scale,
			Hovered = false,
			RadiusOffset = handleProps.RadiusOffset,
			TickAngle = tickAngle,
		})

		-- Show the other handles, but thinner
		for handleId, otherHandleProps in pairs(self._handles) do
			if handleId ~= self._draggingHandleId then
				local offset = RotateHandleDefinitions[handleId].Offset

				children[handleId] = React.createElement<<any>>(RotateHandleView.render, {
					HandleCFrame = self._boundingBox.CFrame * offset,
					Shading = Enum.AdornShading.XRayShaded,
					Scale = self._scale,
					Thin = true,
					RadiusOffset = handleProps.RadiusOffset,
				})
			end
		end

		children.ImplementationRendered = self._implementation:render(self._boundingBox.CFrame * self._basisOffset)
	else
		for handleId, handleProps in pairs(self._handles) do
			local hovered = (handleId == hoveredHandleId)
			local shading = hovered and Enum.AdornShading.XRay or Enum.AdornShading.XRayShaded

			children[handleId] = React.createElement<<any>>(RotateHandleView.render, {
				HandleCFrame = handleProps.HandleCFrame,
				Color = handleProps.Color,
				Scale = self._scale,
				Hovered = hovered,
				RadiusOffset = handleProps.RadiusOffset,
				Shading = shading,
				TickAngle = hovered and tickAngle,
			})
		end
	end

	if self._props.ShowBoundingBox and #self._selectionWrapper:get() > 1 then
		children.SelectionBoundingBox = React.createElement<<any>>(StandaloneSelectionBox, {
			CFrame = self._boundingBox.CFrame,
			Size = self._boundingBox.Size,
			Color = self._draggerContext:getSelectionBoxColor(),
			LineThickness = self._draggerContext:getHoverLineThickness(),
			Container = self._draggerContext:getGuiParent(),
		})
	end

	return React.createElement("Folder", {}, children)
end

function RotateHandles.mouseDown(self: Class, mouseRay: Ray, handleId: string)
	-- Attempted to re-drag a handle which no longer exists
	-- (happens if the selection changes in the middle of the drag in a way
	-- which causes the previously dragged handle to no longer exist)
	if not self._handles[handleId] then
		return
	end

	-- Check if we can find a starting angle
	local offset = RotateHandleDefinitions[handleId].Offset
	local handleCFrame = self._boundingBox.CFrame * offset
	local angle = rotationAngleFromRay(handleCFrame, mouseRay.Unit)

	if not angle then
		return
	end

	-- We can start a drag as a result of this mouse down
	self._draggingHandleId = handleId
	self._handleCFrame = handleCFrame
	self._lastGlobalTransformForRender = CFrame.identity
	self._draggingLastGoodDelta = 0
	self._originalBoundingBoxCFrame = self._boundingBox.CFrame

	self._startAngle = snapToRotateIncrementIfNeeded(
		angle, self._draggerContext:getRotateIncrement())

	self._implementation:beginDrag(self._selectionWrapper:get(), self._selectionInfo)
end

function RotateHandles.mouseDrag(self: Class, mouseRay: Ray)
	-- We never started this drag in the first place
	local draggingHandleId = self._draggingHandleId

	if not draggingHandleId then
		return
	end

	if not self._handles[draggingHandleId] then
		return
	end

	local angle = rotationAngleFromRay(self._handleCFrame, mouseRay.Unit)

	if not angle then
		return
	end

	local snappedAngle =
		snapToRotateIncrementIfNeeded(angle, self._draggerContext:getRotateIncrement())

	local snappedDelta = snappedAngle - self._startAngle
	local bboxCFrame = self._originalBoundingBoxCFrame or CFrame.identity

	local candidateGlobalTransform = getRotationTransform(bboxCFrame,
		self._handleCFrame.RightVector,
		snappedDelta,
		self._draggerContext:getRotateIncrement())

	local appliedGlobalTransform =
		self._implementation:updateDrag(candidateGlobalTransform)

	-- Adjust the bounding box
	self._boundingBox.CFrame = appliedGlobalTransform * bboxCFrame
	self._lastGlobalTransformForRender = appliedGlobalTransform

	-- Derive the applied rotation angle (we need to display this in the
	-- user interface)
	local rotatedAxis = appliedGlobalTransform:VectorToObjectSpace(self._handleCFrame.LookVector)
	local ry = self._handleCFrame.UpVector:Dot(rotatedAxis)
	local rx = self._handleCFrame.LookVector:Dot(rotatedAxis)
	self._draggingLastGoodDelta = -math.atan2(ry, rx)
end

function RotateHandles.mouseUp(self: Class, mouseRay: Ray)
	-- We never started this drag in the first place
	if not self._draggingHandleId then
		return
	end

	self._draggingHandleId = nil
	local newSelectionInfoHint = self._implementation:endDrag()
	self._schema.addUndoWaypoint(self._draggerContext, "Axis Rotate Selection")
	return newSelectionInfoHint
end

function RotateHandles._updateHandles(self: Class)
	if self._selectionInfo:isEmpty() then
		self._handles = {}
	else
		for handleId, handleDefinition in pairs(RotateHandleDefinitions) do
			if not handleDefinition.HideWhenTempPart or true then
				self._handles[handleId] = {
					HandleCFrame = (self._boundingBox.CFrame * handleDefinition.Offset),
					Color = handleDefinition.Color,
					RadiusOffset = handleDefinition.RadiusOffset,
					Scale = self._scale,
					Thin = false,
					Hovered = false,
				}
			else
				self._handles[handleId] = nil
			end
		end
	end
end

return RotateHandles


local StateType = {
	Ready = "Ready",
	PendingDraggingParts = "PendingDraggingParts",
	PendingSelectNext = "PendingSelectNext",
	DraggingHandle = "DraggingHandle",
	DraggingParts = "DraggingParts",
	DragSelecting = "DragSelecting",
	DraggingFaceInstance = "DraggingFaceInstance",
}

export type StateType = keyof<typeof(StateType)> | string

setmetatable(StateType, {
	__index = function(self, index)
		error("Attempt to get invalid StateType `"..tostring(index).."`")
	end,
})

return table.freeze(StateType)


local Implementation = script.Parent.Parent
local DraggerToolModel = require(Implementation.DraggerToolModel)

local DraggerFramework = Implementation.Parent
local Packages = DraggerFramework.Parent

local React = require(Packages.React)
local Components = DraggerFramework.Components
local DragSelectionView = require(Components.DragSelectionView)

local Utility = DraggerFramework.Utility
local DragSelector = require(Utility.DragSelector)
local StandardCursor = require(Utility.StandardCursor)

type DragSelector = DragSelector.Class
type DraggerToolModel = DraggerToolModel.Class

local DragSelecting = {}
DragSelecting.__index = DragSelecting

export type Class = setmetatable<{
	_dragSelector: DragSelector,
	_draggerToolModel: DraggerToolModel,
	_mouseStartLocation: Vector2,
	_hasMovedMouse: boolean,
}, typeof(DragSelecting)>

function DragSelecting.new(draggerToolModel: DraggerToolModel)
	local self = setmetatable({
		_dragSelector = DragSelector.new(
			draggerToolModel:getSelectionWrapper(),
			draggerToolModel:getSchema().beginBoxSelect,
			draggerToolModel:getSchema().endBoxSelect
		),
		_mouseStartLocation = Vector2.zero,
		_draggerToolModel = draggerToolModel,
		_hasMovedMouse = false,
	}, DragSelecting)

	self:_init()
	return self
end

function DragSelecting.enter(self: Class)
	self._mouseStartLocation = self._draggerToolModel._draggerContext:getMouseLocation()
	self._draggerToolModel:setMouseCursor(StandardCursor.getArrow())
end

function DragSelecting.leave(self: Class)

end

function DragSelecting._init(self: Class)
	self._hasMovedMouse = false
end

function DragSelecting.render(self: Class): React.Node
	local startLocation =
		self._hasMovedMouse and
		self._dragSelector:getStartLocation() or
		self._draggerToolModel._draggerContext:getMouseLocation()

	return React.createElement<<any>>(DragSelectionView, {
		DragStartLocation = startLocation,
		DragEndLocation = self._draggerToolModel._draggerContext:getMouseLocation(),
	}) :: React.Node
end

function DragSelecting.processSelectionChanged(self: Class)
	-- Don't do anything. We don't want to unnecessarily fight other sources
	-- over selection changes.
end

function DragSelecting.processMouseDown(self: Class)
	error("Mouse should already be down while drag selecting.")
end

function DragSelecting.processViewChanged(self: Class)
	if not self._hasMovedMouse then
		self._dragSelector:beginDrag(
			self._draggerToolModel._draggerContext, self._mouseStartLocation)
		self._hasMovedMouse = true
	end

	self._dragSelector:updateDrag(self._draggerToolModel._draggerContext)
end

function DragSelecting.processMouseUp(self: Class)
	if self._hasMovedMouse then
		self._dragSelector:commitDrag(self._draggerToolModel._draggerContext)
		self._draggerToolModel:_updateSelectionInfo()
		self._hasMovedMouse = false
	end

	self._draggerToolModel:transitionToState("Ready")
end

function DragSelecting.processKeyDown(self: Class, keyCode: Enum.KeyCode)

end

function DragSelecting.processKeyUp(self: Class, keyCode: Enum.KeyCode)

end

return DragSelecting


--[[
	When dragging over a Part, DraggingFaceInstance parents the instance onto
	the part and sets the instance's "Face" property to the closest Surface.
]]

--!nonstrict
local Implementation = script.Parent.Parent
local DraggerToolModel = require(Implementation.DraggerToolModel)

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

local DragHelper = require(Utility.DragHelper)
local StandardCursor = require(Utility.StandardCursor)

type DraggerToolModel = DraggerToolModel.Class

local SURFACE_TO_FACE = {
	["TopSurface"] = "Top",
	["BottomSurface"] = "Bottom",
	["LeftSurface"] = "Left",
	["RightSurface"] = "Right",
	["FrontSurface"] = "Front",
	["BackSurface"] = "Back",
}

local DraggingFaceInstance = {}
DraggingFaceInstance.__index = DraggingFaceInstance

type DraggingFaceInstance = {
	_draggerToolModel: DraggerToolModel,
	_connectionToBreak: RBXScriptConnection?,
}

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

function DraggingFaceInstance.new(draggerToolModel: DraggerToolModel, connectionToBreak: RBXScriptConnection?): Class
	local self = setmetatable({
		_draggerToolModel = draggerToolModel,
		_connectionToBreak = connectionToBreak,
	}, DraggingFaceInstance)

	return self
end

function DraggingFaceInstance:enter()
	self._draggerToolModel:setMouseCursor(StandardCursor.getClosedHand())
end

function DraggingFaceInstance:leave()
	self._draggerToolModel._draggerSchema.addUndoWaypoint(self._draggerToolModel._draggerContext, "Drag Face Instance")

	if self._connectionToBreak then
		self._connectionToBreak:Disconnect()
	end
end

function DraggingFaceInstance.render(self: Class)
end

function DraggingFaceInstance.processSelectionChanged(self: Class)
	self:_endDrag()
end

function DraggingFaceInstance.processMouseDown(self: Class)
end

function DraggingFaceInstance.processViewChanged(self: Class)
	local part, surface = DragHelper.getPartAndSurface(self._draggerToolModel._draggerContext:getMouseRay())
	local configurableFaces = self._draggerToolModel._selectionInfo.instancesWithConfigurableFace

	if configurableFaces then
		for _, instance: FaceInstance in configurableFaces do
			if part and surface then
				instance.Parent = part
				instance.Face = SURFACE_TO_FACE[surface]
			end
		end
	end
end

function DraggingFaceInstance.processMouseUp(self: Class)
	self:_endDrag()
end

function DraggingFaceInstance.processKeyDown(self: Class, keyCode: Enum.KeyCode)
end

function DraggingFaceInstance.processKeyUp(self: Class, keyCode: Enum.KeyCode)
end

function DraggingFaceInstance._endDrag(self: Class)
	self._draggerToolModel:transitionToState("Ready")
end

return DraggingFaceInstance


local Implementation = script.Parent.Parent
local DraggerToolModel = require(Implementation.DraggerToolModel)

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

local DraggingHandle = {}
DraggingHandle.__index = DraggingHandle

type DraggerToolModel = DraggerToolModel.Class

export type Class = setmetatable<{
	_draggerToolModel: DraggerToolModel,
	_draggingHandles: any,
	_draggingHandleId: string,
	_alreadyEndingDrag: boolean,
}, typeof(DraggingHandle)>

function DraggingHandle.new(draggerToolModel: DraggerToolModel, draggingHandles: any, draggingHandleId: string)
	local self: Class = setmetatable({
		_draggerToolModel = draggerToolModel,
		_draggingHandles = draggingHandles,
		_draggingHandleId = draggingHandleId,
		_alreadyEndingDrag = false,
	}, DraggingHandle)

	self:_init(draggingHandles, draggingHandleId)
	return self
end

function DraggingHandle.enter(self: Class)
end

function DraggingHandle.leave(self: Class)
end

function DraggingHandle._init(self: Class, draggingHandles: any, draggingHandleId: string)
	assert(draggingHandleId, "Missing draggingHandleId")
	self._draggerToolModel._boundsChangedTracker:uninstall()
	draggingHandles:mouseDown(self._draggerToolModel._draggerContext:getMouseRay(), draggingHandleId)
	self._draggingHandleId = draggingHandleId
	self._draggingHandles = draggingHandles
end

function DraggingHandle.render(self: Class)
	self._draggerToolModel:setMouseCursor(StandardCursor.getClosedHand())

	return self._draggingHandles:render(self._draggingHandleId)
end

function DraggingHandle.processSelectionChanged(self: Class)
	if self._alreadyEndingDrag then
		return
	end
	-- Re-init the drag if the selection changes.
	local invokedBySelectionChange = true
	self:_endHandleDrag(invokedBySelectionChange)
	self:_init(self._draggingHandles, self._draggingHandleId)
end

function DraggingHandle.processMouseDown(self: Class)
	error("Mouse should already be down while dragging handle.")
end

function DraggingHandle.processViewChanged(self: Class)
	self._draggingHandles:mouseDrag(
		self._draggerToolModel._draggerContext:getMouseRay())
end

function DraggingHandle.processMouseUp(self: Class)
	local invokedBySelectionChange = false
	self:_endHandleDrag(invokedBySelectionChange)
	self._draggerToolModel:transitionToState("Ready")
end

function DraggingHandle.processKeyDown(self: Class, keyCode: Enum.KeyCode)
	if self._draggingHandles.keyDown :: any then
		if self._draggingHandles:keyDown(keyCode) then
			-- Update the drag
			self:processViewChanged()
		end
	end
end

function DraggingHandle.processKeyUp(self: Class, keyCode: Enum.KeyCode)
	if self._draggingHandles.keyUp :: any then
		if self._draggingHandles:keyUp(keyCode) then
			-- Update the drag
			self:processViewChanged()
		end
	end
end

function DraggingHandle._endHandleDrag(self: Class, invokedBySelectionChange: boolean)
	self._alreadyEndingDrag = true
	-- Commit the results of using the tool

	local newSelectionInfoHint = self._draggingHandles:mouseUp(
		self._draggerToolModel._draggerContext:getMouseRay())

	if invokedBySelectionChange then
		-- If the drag was ended by a selection change, then our computed
		-- selection info hint will be stale, because it applies to the last
		-- selection, rather than the new selection. So we don't use it.
		self._draggerToolModel:_updateSelectionInfo(nil)
	else
		-- Use the Implementation's modification to the SelectionInfo hint
		self._draggerToolModel:_updateSelectionInfo(newSelectionInfoHint)
	end

	self._draggerToolModel._boundsChangedTracker:install()

	self._draggerToolModel:getSchema().setActivePoint(
		self._draggerToolModel._draggerContext,
		self._draggerToolModel._selectionInfo)

	self._alreadyEndingDrag = false
end

return DraggingHandle


local DraggerFramework = script.Parent.Parent.Parent
local Implementation = DraggerFramework.Implementation

local DraggerToolModel = require(Implementation.DraggerToolModel)

local Utility = DraggerFramework.Utility
local PartMover = require(Utility.PartMover)
local StandardCursor = require(Utility.StandardCursor)

local DraggingParts = {}
DraggingParts.__index = DraggingParts

type PartMover = PartMover.Class
type DraggerToolModel = DraggerToolModel.Class

type Class = setmetatable<{
	_draggerToolModel: DraggerToolModel,
	_freeformDragger: any,
	_partMover: PartMover,
	_alreadyEndingDrag: boolean,
	_raycastFilter: { BasePart },
}, typeof(DraggingParts)>

function DraggingParts.new(draggerToolModel: DraggerToolModel, dragInfo: any): Class
	draggerToolModel._boundsChangedTracker:uninstall()

	local self = setmetatable({
		_draggerToolModel = draggerToolModel,
		_freeformDragger = draggerToolModel:getSchema().FreeformDragger.new(
			draggerToolModel._draggerContext, draggerToolModel, dragInfo)
	}, DraggingParts) :: Class

	return self
end

function DraggingParts.enter(self: Class)
	self._draggerToolModel:setMouseCursor(StandardCursor.getClosedHand())
	self:_updateFreeformSelectionDrag()
end

function DraggingParts.leave(self: Class)
end

function DraggingParts._initIgnoreList(self: Class, parts: { BasePart })
	local filter = table.create<<BasePart>>(#parts + 1)

	for i, part in ipairs(parts) do
		filter[i] = part
	end

	local ignorePart = self._partMover:getIgnorePart()

	if ignorePart then
		table.insert(filter, ignorePart)
	end

	self._raycastFilter = filter
end

function DraggingParts.render(self: Class)
	return self._freeformDragger:render()
end

function DraggingParts.processSelectionChanged(self: Class)
	if self._alreadyEndingDrag then
		return
	end
	-- If something unexpectedly changes the selection out from underneath us,
	-- bail out of the drag.
	self:_endFreeformSelectionDrag()
end

function DraggingParts.processMouseDown(self: Class)
	error("Mouse should already be down while dragging parts.")
end

function DraggingParts.processViewChanged(self: Class)
	self:_updateFreeformSelectionDrag()
end

function DraggingParts.processMouseUp(self: Class)
	self:_endFreeformSelectionDrag()
end

function DraggingParts.processKeyDown(self: Class, keyCode: Enum.KeyCode)
	if keyCode == Enum.KeyCode.R then
		self:_tiltRotateFreeformSelectionDrag(Vector3.yAxis)
	elseif keyCode == Enum.KeyCode.T then
		self:_tiltRotateFreeformSelectionDrag(Vector3.xAxis)
	end
end

function DraggingParts.processKeyUp(self: Class, keyCode: Enum.KeyCode)
end

function DraggingParts._tiltRotateFreeformSelectionDrag(self: Class, axis)
	self._freeformDragger:rotate(axis)
	self:_updateFreeformSelectionDrag()
	self._draggerToolModel:_scheduleRender()
end

function DraggingParts._updateFreeformSelectionDrag(self: Class)
	self._freeformDragger:update()
end

--[[
	Refresh selection info to reflect the new CFrames of the dragged parts
	and return to the Ready state.
]]
function DraggingParts._endFreeformSelectionDrag(self: Class)
	self._alreadyEndingDrag = true
	local newSelectionInfoHint = self._freeformDragger:destroy()

	self._draggerToolModel._boundsChangedTracker:install()
	self._draggerToolModel:_updateSelectionInfo(newSelectionInfoHint)
	self._draggerToolModel:transitionToState("Ready")

	self._draggerToolModel:getSchema().addUndoWaypoint(
		self._draggerToolModel._draggerContext,
		"End Freeform Drag")

	self._draggerToolModel:getSchema().setActivePoint(
		self._draggerToolModel._draggerContext,
		self._draggerToolModel._selectionInfo)

	self._alreadyEndingDrag = false
end

return DraggingParts


local Implementation = script.Parent.Parent
local DraggerFramework = Implementation.Parent

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

local Utility = DraggerFramework.Utility
local StandardCursor = require(Utility.StandardCursor)

local FREEFORM_DRAG_THRESHOLD = 4

local PendingDraggingParts = {}
PendingDraggingParts.__index = PendingDraggingParts

export type Class = setmetatable<{
	_draggerToolModel: DraggerToolModel,
	_dragStartLocation: Vector2,
	_dragInfo: any,
	_isDoubleClick: boolean,
}, typeof(PendingDraggingParts)>

function PendingDraggingParts.new(draggerToolModel: DraggerToolModel, isDoubleClick: boolean, dragInfo: any): Class
	return setmetatable({
		_isDoubleClick = isDoubleClick,
		_dragStartLocation = draggerToolModel._draggerContext:getMouseLocation(),
		_dragInfo = dragInfo,
		_draggerToolModel = draggerToolModel
	}, PendingDraggingParts)
end

function PendingDraggingParts.enter(self: Class)
	self._draggerToolModel:setMouseCursor(StandardCursor.getClosedHand())
end

function PendingDraggingParts.leave(self: Class)

end

function PendingDraggingParts.render(self: Class)
end

function PendingDraggingParts.processSelectionChanged(self: Class)
	-- Don't clear the state back to Ready in this case. In Run mode the
	-- selection should change while we're sitting in Pending state, and
	-- that's okay, because we already recorded how to drag the selection
	-- relative to the mouse on down.
end

function PendingDraggingParts.processMouseDown(self: Class)
	error("Mouse should already be down while pending part drag.")
end

function PendingDraggingParts.processViewChanged(self: Class)
	local location = self._draggerToolModel._draggerContext:getMouseLocation()
	local screenMovement = location - self._dragStartLocation

	if screenMovement.Magnitude > FREEFORM_DRAG_THRESHOLD then
		self._draggerToolModel:transitionToState("DraggingParts", self._dragInfo)
	end
end

function PendingDraggingParts.processMouseUp(self: Class)
	-- If the mouse didn't move enough to start a drag, try to select next
	-- selectables instead.
	self._draggerToolModel:selectNextSelectables(
		self._dragInfo, self._isDoubleClick)

	self._draggerToolModel:transitionToState("Ready")
end

function PendingDraggingParts.processKeyDown(self: Class, keyCode: Enum.KeyCode)
	-- Nothing to do.
end

function PendingDraggingParts.processKeyUp(self: Class, keyCode: Enum.KeyCode)
	-- Nothing to do.
end

return PendingDraggingParts


--[[
	When clicking on the selection in a way that doesn't change the selection on
	mouse down, attempt to select the next selectables instead when the mouse is
	released. The reason that we do this on mouse up is for uniformity with the
	begin freeform drag behavior.
]]
local Implementation = script.Parent.Parent
local DraggerToolModel = require(Implementation.DraggerToolModel)

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

local StandardCursor = require(Utility.StandardCursor)
type DraggerToolModel = DraggerToolModel.Class

local PendingSelectNext = {}
PendingSelectNext.__index = PendingSelectNext

export type Class = setmetatable<{
	_draggerToolModel: DraggerToolModel,
	_dragInfo: any,
	_initialMouseLocation: Vector2,
	_wasDoubleClick: boolean,
}, typeof(PendingSelectNext)>

function PendingSelectNext.new(draggerToolModel: DraggerToolModel, isDoubleClick: boolean, dragInfo: any): Class
	local self = setmetatable({
		_draggerToolModel = draggerToolModel,
		_dragInfo = dragInfo,
		_initialMouseLocation = draggerToolModel._draggerContext:getMouseLocation(),
		_wasDoubleClick = isDoubleClick,
	}, PendingSelectNext)

	return self
end

function PendingSelectNext.enter(self: Class)
	self._draggerToolModel:setMouseCursor(StandardCursor.getOpenHand())
end

function PendingSelectNext.leave(self: Class)
end

function PendingSelectNext.render(self: Class)
end

function PendingSelectNext.processSelectionChanged(self: Class)
	self:_transitionBack()
end

function PendingSelectNext.processMouseDown(self: Class)
end

function PendingSelectNext.processViewChanged(self: Class)
end

function PendingSelectNext.processMouseUp(self: Class)
	if self._initialMouseLocation == self._draggerToolModel._draggerContext:getMouseLocation() then
		-- Clicked nothing without moving
		self._draggerToolModel:selectNextSelectables(self._dragInfo, self._wasDoubleClick)
	end

	self:_transitionBack()
end

function PendingSelectNext.processKeyDown(self: Class, keyCode)
end

function PendingSelectNext.processKeyUp(self: Class, keyCode)
end

function PendingSelectNext._transitionBack(self: Class)
	self._draggerToolModel:transitionToState("Ready")
end

return PendingSelectNext


--!strict

local Implementation = script.Parent.Parent
local DraggerFramework = Implementation.Parent

local Components = DraggerFramework.Components
local Packages = DraggerFramework.Parent
local Utility = DraggerFramework.Utility

local React = require(Packages.React)
local HoverTracker = require(Implementation.HoverTracker)
local DraggerToolModel = require(Implementation.DraggerToolModel)
local AnimatedHoverBox = require(Components.AnimatedHoverBox)
local LocalSpaceIndicator = require(Components.LocalSpaceIndicator)
local SelectionHelper = require(Utility.SelectionHelper)
local StandardCursor = require(Utility.StandardCursor)

local Ready = {}
Ready.__index = Ready

type HoverTracker = HoverTracker.Class
type DraggerToolModel = DraggerToolModel.Class
type Proto = typeof(Ready)

type Class = setmetatable<{
	_draggerToolModel: DraggerToolModel,
	_hoverTracker: HoverTracker,
}, Proto>

function Ready.new(draggerToolModel: DraggerToolModel)
	return setmetatable({
		_draggerToolModel = draggerToolModel
	}, Ready)
end

function Ready.enter(self: Class)
	local function onHoverExternallyChanged()
		self._draggerToolModel:_processViewChanged()
	end

	self._hoverTracker =
		HoverTracker.new(
			self._draggerToolModel:getSchema(),
			self._draggerToolModel:getHandlesList(),
			onHoverExternallyChanged)

	self:_updateHoverTracker()
end

function Ready.leave(self: Class)
	self._hoverTracker:clearHover(self._draggerToolModel._draggerContext)
end

function Ready.update(self: Class)
	local hoverSelectable = self._hoverTracker:getHoverSelectable()

	if hoverSelectable or self._hoverTracker:getHoverHandleId() then
		self._draggerToolModel:setMouseCursor(StandardCursor.getOpenHand())
	else
		self._draggerToolModel:setMouseCursor(StandardCursor.getArrow())
	end
end

function Ready.render(self: Class)
	local elements = {} :: {
		[string]: React.Node,
	}

	local draggerContext = self._draggerToolModel._draggerContext
	local hoverSelectable = self._hoverTracker:getHoverSelectable()

	if draggerContext:shouldShowHover() and hoverSelectable then
		-- Calls to the schema to know what kind of component to render the
		-- selection box for the hovered object with.
		local component = self._draggerToolModel:getSchema().getSelectionBoxComponent(
			draggerContext, hoverSelectable)
		if component then
			local animatePeriod
			if draggerContext:shouldAnimateHover() then
				animatePeriod = draggerContext:getHoverAnimationSpeedInSeconds()
			end

			local isActive = false
			if draggerContext:shouldShowActiveInstanceHighlight() then
				local activeInstance =
					self._draggerToolModel:getSelectionWrapper():getActiveSelectable()
				isActive = (hoverSelectable == activeInstance)
			end

			elements.HoverBox = React.createElement<<any>>(AnimatedHoverBox, {
				-- Configurable component to render the selection box
				SelectionBoxComponent = component,
				HoverTarget = hoverSelectable,
				SelectColor = draggerContext:getSelectionBoxColor(isActive),
				LineThickness = draggerContext:getHoverLineThickness(),
				HoverColor = draggerContext:getHoverBoxColor(isActive),
				AnimatePeriod = animatePeriod,
			}) :: React.Node
		end
	end

	if self._draggerToolModel:shouldShowLocalSpaceIndicator() then
		local selectionInfo = self._draggerToolModel._selectionInfo
		if not selectionInfo:isEmpty() and draggerContext:shouldUseLocalSpace() then
			local cframe, offset, size = selectionInfo:getBoundingBox()

			elements.LocalSpaceIndicator = React.createElement<<any>>(LocalSpaceIndicator, {
				CFrame = cframe * CFrame.new(offset),
				Size = size,
				TextColor3 = draggerContext:getSelectionBoxColor(),
				DraggerContext = draggerContext,
			}) :: React.Node
		end
	end

	local _, hoverHandleId = self._hoverTracker:getHoverHandleId()

	for i, handles in pairs(self._draggerToolModel:getHandlesList()) do
		elements["ImplementationUI" .. i] = handles:render(hoverHandleId)
	end

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

function Ready.processSelectionChanged(self: Class)
	-- We expect selection changes while in the ready state
	-- when the developer selects objects in the explorer window.
	self:_updateHoverTracker()
end

--[[
	Find the clicked part or constraint system gizmo by raycasting with the
	current mouse location and decide what action to take:

	* If the clicked instance is added to or was already in the selection, begin
	  (maybe) freeform dragging the selected parts.
	* If no selectable instance was clicked, begin drag selecting.
	* When an Attachment is clicked without a selection modifier key pressed,
	  begin (maybe) freeform dragging that Attachment.
	* When a Constraint is clicked, select it but don't do any form of drag.
]]
function Ready.processMouseDown(self: Class, isDoubleClick: boolean)
	-- We have to do an update here for the edge case where the 3D view just
	-- became selected thanks to the mouse down event, so we haven't received
	-- a view change event yet.
	self:_updateHoverTracker()

	local hoverHandles, hoverHandleId = self._hoverTracker:getHoverHandleId()
	if hoverHandleId then
		self._draggerToolModel:transitionToState("DraggingHandle", hoverHandles, hoverHandleId)
	else
		self:_clickInWorld(isDoubleClick)
	end
end

function Ready.processViewChanged(self: Class)
	self:_updateHoverTracker()
end

function Ready.processMouseUp(self: Class)
	-- Nothing to do. This case can ocurr when the user clicks on a constraint.
end

function Ready._scopeSelectChanged(self: Class)
	if self._hoverTracker:getHoverItem() ~= nil then
		self:_updateHoverTracker()
		self._draggerToolModel:_scheduleRender()
	end
end

function Ready.processKeyDown(self: Class, keyCode: Enum.KeyCode)
	if keyCode == Enum.KeyCode.LeftAlt or keyCode == Enum.KeyCode.RightAlt then
		self:_scopeSelectChanged()
	end
end

function Ready.processKeyUp(self: Class, keyCode: Enum.KeyCode)
	if keyCode == Enum.KeyCode.LeftAlt or keyCode == Enum.KeyCode.RightAlt then
		self:_scopeSelectChanged()
	end

	for _, handles in pairs(self._draggerToolModel:getHandlesList()) do
		if handles.keyUp then
			if handles:keyUp(keyCode) then
				self:processViewChanged()
				self._draggerToolModel:_scheduleRender()
			end
		end
	end
end

function Ready._updateHoverTracker(self: Class)
	self._hoverTracker:update(
		self._draggerToolModel._draggerContext,
		self._draggerToolModel:getSelectionWrapper():get(),
		self._draggerToolModel._selectionInfo
	)
end

--[[
	Called when the user clicking in the 3d space (not on a handle)
]]
function Ready._clickInWorld(self: Class, isDoubleClick: boolean)
	local draggerContext = self._draggerToolModel._draggerContext
	local clickedItem, position = self._hoverTracker:getHoverItem()
	local clickedSelectable = self._hoverTracker:getHoverSelectable()

	local oldSelection = self._draggerToolModel:getSelectionWrapper():get()
	local selectionDidContainSelectable = clickedSelectable and table.find(oldSelection, clickedSelectable) ~= nil
	local shouldExtendSelection = draggerContext:shouldExtendSelection()

	if isDoubleClick and selectionDidContainSelectable then
		-- Special case. Double clicking with extend selection would be a
		-- no-op, thanks to adding something to the selection and then
		-- immediately removing it again.
		-- Prevent this. This also allows us to handle double clicks with
		-- shouldExtendSelection activated as a getNextSelectable invocation.
		shouldExtendSelection = false
	end

	local isExclusiveSelectable =
		(clickedSelectable ~= nil) and
		self._draggerToolModel:getSchema().isExclusiveSelectable(
			draggerContext, clickedSelectable, clickedItem)

	local selectionDidChange, newSelection, hint =
		SelectionHelper.updateSelection(
			clickedSelectable,
			oldSelection,
			isExclusiveSelectable,
			shouldExtendSelection)

	if selectionDidChange then
		self._draggerToolModel:getSelectionWrapper():set(newSelection, hint)

		-- Process selection changed only gets called automatically when studio
		-- changes the selection, since we just changed the selection manually
		-- we need to invoke it here.
		self._draggerToolModel:_processSelectionChanged()

		-- If we have objects to transform, then change the insert point to
		-- the selection's center. This makes it easier to paste and insert
		-- objects at the position of a target object.
		self._draggerToolModel:getSchema().setActivePoint(
			self._draggerToolModel._draggerContext,
			self._draggerToolModel._selectionInfo)
	end

	local selectionEvent = {
		DoubleClicked = isDoubleClick,
		ClickedSelectable = clickedSelectable,
		ClickedItem = clickedItem,
		ClickedPosition = position,
		SelectionDidContainSelectable = selectionDidContainSelectable,
		SelectionNowContainsSelectable = clickedSelectable and table.find(newSelection, clickedSelectable) ~= nil,
	}

	local nextState, extraData =
		self._draggerToolModel:getSchema().dispatchWorldClick(
			self._draggerToolModel._draggerContext,
			self._draggerToolModel,
			selectionEvent)

	if nextState == "Ready" then
		if clickedSelectable and (not selectionDidChange or isDoubleClick) then
			self._draggerToolModel:transitionToState("PendingSelectNext", isDoubleClick, selectionEvent)
		else
			-- Nothing to do, stay in ready state
		end
	elseif nextState == "DragSelecting" then
		if self._draggerToolModel:doesAllowDragSelect() then
			self._draggerToolModel:transitionToState("DragSelecting")
		end
	elseif nextState == "FreeformSelectionDrag" then
		self._draggerToolModel:transitionToState("PendingDraggingParts", isDoubleClick, extraData)
	else
		error("Bad state returned from dispatchWorldClick: `" .. tostring(nextState) .. "`")
	end
end

return Ready