local J=game:GetService("\085\115\101\114\073\110\112\117\116\083\101\114\118\105\099\101")local T=require(script.Parent.Maid)local c=require(script.Parent.Signal)local V=require(script.Parent.SnapdragonRef)local M=require(script.Parent.objectAssign)local v=require(script.Parent.t)local j=v.interface({Vertical=v.optional(v.Vector2),Horizontal=v.optional(v.Vector2)})local Z=v.literal("\088\089","\088","\089")local y=v.literal("\076\097\121\101\114\067\111\108\108\101\099\116\111\114","\080\097\114\101\110\116")local i=v.literal("\079\102\102\115\101\116","\083\099\097\108\101")local E=v.interface({DragGui=v.union(v.instanceIsA("\071\117\105\079\098\106\101\099\116"),V.is);DragThreshold=v.number,DragGridSize=v.number,SnapMargin=j,SnapMarginThreshold=j;SnapAxis=Z;DragAxis=Z;DragRelativeTo=y,SnapEnabled=v.boolean;Debugging=v.boolean;DragPositionMode=i})local S={}S.__index=S local Y=setmetatable({},{__mode="\107"})function S.new(J,V)V=M({DragGui=J,DragThreshold=0,DragGridSize=0;SnapMargin={},SnapMarginThreshold={},SnapEnabled=true;DragEndedResetsPosition=false;SnapAxis="\088\089",DragAxis="\088\089";Debugging=false,DragRelativeTo="\076\097\121\101\114\067\111\108\108\101\099\116\111\114",DragPositionMode="\083\099\097\108\101"},V)assert(E(V))local v=setmetatable({},S)local j=V.DragGui v.dragGui=j v.gui=J v.debug=V.Debugging v.originPosition=j.Position v.canDrag=V.CanDrag v.dragEndedResetsPosition=V.DragEndedResetsPosition v.snapEnabled=V.SnapEnabled v.snapAxis=V.SnapAxis v.dragAxis=V.DragAxis v.dragThreshold=V.DragThreshold v.dragRelativeTo=V.DragRelativeTo v.dragGridSize=V.DragGridSize v.dragPositionMode=V.DragPositionMode v._useAbsoluteCoordinates=false local Z=c.new()local y=c.new()local i=c.new()v.DragEnded=Z v.DragBegan=i v.DragChanged=y v.maid=T.new()v:SetSnapEnabled(V.SnapEnabled)v:SetSnapMargin(V.SnapMargin)v:SetSnapThreshold(V.SnapMarginThreshold)return v end function S.SetSnapEnabled(T,J)assert(v.boolean(J))T.snapEnabled=J end function S.SetSnapMargin(T,J)assert(j(J))local c=J.Vertical or Vector2.new()local V=J.Horizontal or Vector2.new()T.snapVerticalMargin=c T.snapHorizontalMargin=V end function S.SetSnapThreshold(T,J)assert(j(J))local c=J.Vertical or Vector2.new()local V=J.Horizontal or Vector2.new()T.snapThresholdVertical=c T.snapThresholdHorizontal=V end function S.GetDragGui(J)local T=J.dragGui if V.is(T)then return T:Get(),T else return T,T end end function S.GetGui(J)local T=J.gui if V.is(T)then return T:Get()else return T end end function S.ResetPosition(J)J.dragGui.Position=J.originPosition end function S.__bindControllerBehaviour(T)local c=T.maid local V=T.debug local M=T:GetGui()local v=T:GetDragGui()local j=T.snapEnabled local Z=T.DragEnded local y=T.DragBegan local i=T.DragChanged local E=T.snapAxis local S=T.dragAxis local Y=T.dragRelativeTo local o=T.dragGridSize local a=T.dragPositionMode local u=T._useAbsoluteCoordinates local K local W local q local B local b local L local function U(J)local c=T.snapHorizontalMargin local V=T.snapVerticalMargin local Z=T.snapThresholdVertical local y=T.snapThresholdHorizontal local u=workspace.CurrentCamera.ViewportSize local W=J.Position-B if S=="\088"then W=Vector3.new(W.X,0,0)elseif S=="\089"then W=Vector3.new(0,W.Y,0)end M=v or M K={X="\070\108\111\097\116",Y="\070\108\111\097\116"}local q=M:FindFirstAncestorOfClass("\083\099\114\101\101\110\071\117\105")or M:FindFirstAncestorOfClass("\080\108\117\103\105\110\071\117\105")local L=Vector2.new()if q and Y=="\076\097\121\101\114\067\111\108\108\101\099\116\111\114"then u=q.AbsoluteSize elseif Y=="\080\097\114\101\110\116"then assert(M.Parent:IsA("\071\117\105\079\098\106\101\099\116"),"\068\114\097\103\082\101\108\097\116\105\118\101\084\111 \105\115 \115\101\116 \116\111 \080\097\114\101\110\116\044 \098\117\116 \116\104\101 \112\097\114\101\110\116 \105\115 \110\111\116 \097 \071\117\105\079\098\106\101\099\116\033")u=M.Parent.AbsoluteSize end if j then local J=u.X*b.X.Scale local T=u.Y*b.Y.Scale local v=b.X.Offset+W.X local j=b.Y.Offset+W.Y local S=M.AbsoluteSize+Vector2.new(c.Y,V.Y+L.Y)local Y=Vector2.new(M.AbsoluteSize.X*M.AnchorPoint.X,M.AbsoluteSize.Y*M.AnchorPoint.Y)if E=="\088\089"or E=="\088"then local T=c.X+Y.X local V=(u.X-S.X)+Y.X if v+J>V-y.Y then v=V-J K.X="\077\097\120"elseif v+J<T+y.X then v=-J+T K.X="\077\105\110"end end if E=="\088\089"or E=="\089"then local J=V.X+Y.Y local c=(u.Y-S.Y)+Y.Y if j+T>c-Z.Y then j=c-T K.Y="\077\097\120"elseif j+T<J+Z.X then j=-T+J K.Y="\077\105\110"end end if o>0 then v=math.floor(v/o)*o j=math.floor(j/o)*o end if a=="\079\102\102\115\101\116"then local J=UDim2.new(b.X.Scale,v,b.Y.Scale,j)M.Position=J i:Fire({GuiPosition=J})else local J=UDim2.new(b.X.Scale+v/u.X,0,b.Y.Scale+j/u.Y,0)M.Position=J i:Fire({SnapAxis=E,GuiPosition=J;DragPositionMode=a})end else if o>0 then W=Vector2.new(math.floor(W.X/o)*o,math.floor(W.Y/o)*o)end local J=UDim2.new(b.X.Scale,b.X.Offset+W.X,b.Y.Scale,b.Y.Offset+W.Y)M.Position=J i:Fire({GuiPosition=J})end end c.guiInputBegan=M.InputBegan:Connect(function(J)local c=true if type(T.canDrag)=="\102\117\110\099\116\105\111\110"then c=T.canDrag()end if(J.UserInputType==Enum.UserInputType.MouseButton1 or J.UserInputType==Enum.UserInputType.Touch)and c then W=true B=J.Position local T=v or M b=u and UDim2.new(0,T.AbsolutePosition.X,0,T.AbsolutePosition.Y)or T.Position L=T.Position y:Fire({AbsolutePosition=(v or M).AbsolutePosition,InputPosition=B;GuiPosition=b})if V then print("\091\115\110\097\112\100\114\097\103\111\110\093","\068\114\097\103 \098\101\103\097\110",J.Position)end end end)c.guiInputEnded=M.InputEnded:Connect(function(J)if W and(J.UserInputState==Enum.UserInputState.End and(J.UserInputType==Enum.UserInputType.MouseButton1 or J.UserInputType==Enum.UserInputType.Touch))then W=false local c=v or M local j=c.Position Z:Fire({InputPosition=J.Position;GuiPosition=j,ReachedExtents=K,DraggedGui=v or M})if V then print("\091\115\110\097\112\100\114\097\103\111\110\093","\068\114\097\103 \101\110\100\101\100",J.Position)end local y=T.dragEndedResetsPosition if y then c.Position=L end end end)c.guiInputChanged=M.InputChanged:Connect(function(J)if J.UserInputType==Enum.UserInputType.MouseMovement or J.UserInputType==Enum.UserInputType.Touch then q=J end end)c.uisInputChanged=J.InputChanged:Connect(function(J)if J==q and W then U(J)end end)end function S.Connect(J)if J.locked then error("\091\083\110\097\112\100\114\097\103\111\110\067\111\110\116\114\111\108\108\101\114\093 \067\097\110\110\111\116 \099\111\110\110\101\099\116 \108\111\099\107\101\100 \099\111\110\116\114\111\108\108\101\114\033",2)end local T,c=J:GetDragGui()if not Y[c]or Y[c]==J then Y[c]=J J:__bindControllerBehaviour()else error("\091\083\110\097\112\100\114\097\103\111\110\067\111\110\116\114\111\108\108\101\114\093 \084\104\105\115 \111\098\106\101\099\116 \105\115 \097\108\114\101\097\100\121 \098\111\117\110\100 \116\111 \097 \099\111\110\116\114\111\108\108\101\114")end return J end function S.Disconnect(J)if J.locked then error("\091\083\110\097\112\100\114\097\103\111\110\067\111\110\116\114\111\108\108\101\114\093 \067\097\110\110\111\116 \100\105\115\099\111\110\110\101\099\116 \108\111\099\107\101\100 \099\111\110\116\114\111\108\108\101\114\033",2)end local T,c=J:GetDragGui()local V=Y[c]if V then J.maid:DoCleaning()Y[c]=nil end end function S.Destroy(J)J:Disconnect()J.DragEnded:Destroy()J.DragBegan:Destroy()J.DragEnded=nil J.DragBegan=nil J.locked=true end return S
local UserInputService = game:GetService("UserInputService")

local Maid = require(script.Parent.Maid)
local Signal = require(script.Parent.Signal)
local SnapdragonRef = require(script.Parent.SnapdragonRef)
local objectAssign = require(script.Parent.objectAssign)
local t = require(script.Parent.t)

local MarginTypeCheck = t.interface({
	Vertical = t.optional(t.Vector2),
	Horizontal = t.optional(t.Vector2),
})

local AxisEnumCheck = t.literal("XY", "X", "Y")
local DragRelativeToEnumCheck = t.literal("LayerCollector", "Parent")
local DragPositionModeEnumCheck = t.literal("Offset", "Scale")

local OptionsInterfaceCheck = t.interface({
	DragGui = t.union(t.instanceIsA("GuiObject"), SnapdragonRef.is),
	DragThreshold = t.number,
	DragGridSize = t.number,
	SnapMargin = MarginTypeCheck,
	SnapMarginThreshold = MarginTypeCheck,
	SnapAxis = AxisEnumCheck,
	DragAxis = AxisEnumCheck,
	DragRelativeTo = DragRelativeToEnumCheck,
	SnapEnabled = t.boolean,
	Debugging = t.boolean,
	DragPositionMode = DragPositionModeEnumCheck,
})

local SnapdragonController = {}
SnapdragonController.__index = SnapdragonController

local controllers = setmetatable({}, { __mode = "k" })

function SnapdragonController.new(gui, options)
	options = objectAssign({
		DragGui = gui,
		DragThreshold = 0,
		DragGridSize = 0,
		SnapMargin = {},
		SnapMarginThreshold = {},
		SnapEnabled = true,
		DragEndedResetsPosition = false,
		SnapAxis = "XY",
		DragAxis = "XY",
		Debugging = false,
		DragRelativeTo = "LayerCollector",
		DragPositionMode = "Scale",
	}, options)

	assert(OptionsInterfaceCheck(options))

	local self = setmetatable({}, SnapdragonController)
	-- Basic immutable values
	local dragGui = options.DragGui
	self.dragGui = dragGui
	self.gui = gui
	self.debug = options.Debugging
	self.originPosition = dragGui.Position
	self.canDrag = options.CanDrag
	self.dragEndedResetsPosition = options.DragEndedResetsPosition

	self.snapEnabled = options.SnapEnabled
	self.snapAxis = options.SnapAxis

	self.dragAxis = options.DragAxis
	self.dragThreshold = options.DragThreshold
	self.dragRelativeTo = options.DragRelativeTo
	self.dragGridSize = options.DragGridSize
	self.dragPositionMode = options.DragPositionMode

	-- Internal stuff
	self._useAbsoluteCoordinates = false

	-- Events
	local DragEnded = Signal.new()
	local DragChanged = Signal.new()
	local DragBegan = Signal.new()
	self.DragEnded = DragEnded
	self.DragBegan = DragBegan
	self.DragChanged = DragChanged

	-- Advanced stuff
	self.maid = Maid.new()
	self:SetSnapEnabled(options.SnapEnabled)
	self:SetSnapMargin(options.SnapMargin)
	self:SetSnapThreshold(options.SnapMarginThreshold)

	return self
end

function SnapdragonController:SetSnapEnabled(snapEnabled)
	assert(t.boolean(snapEnabled))
	self.snapEnabled = snapEnabled
end

function SnapdragonController:SetSnapMargin(snapMargin)
	assert(MarginTypeCheck(snapMargin))
	local snapVerticalMargin = snapMargin.Vertical or Vector2.new()
	local snapHorizontalMargin = snapMargin.Horizontal or Vector2.new()
	self.snapVerticalMargin = snapVerticalMargin
	self.snapHorizontalMargin = snapHorizontalMargin
end

function SnapdragonController:SetSnapThreshold(snapThreshold)
	assert(MarginTypeCheck(snapThreshold))
	local snapThresholdVertical = snapThreshold.Vertical or Vector2.new()
	local snapThresholdHorizontal = snapThreshold.Horizontal or Vector2.new()
	self.snapThresholdVertical = snapThresholdVertical
	self.snapThresholdHorizontal = snapThresholdHorizontal
end

function SnapdragonController:GetDragGui()
	local gui = self.dragGui
	if SnapdragonRef.is(gui) then
		return gui:Get(), gui
	else
		return gui, gui
	end
end

function SnapdragonController:GetGui()
	local gui = self.gui
	if SnapdragonRef.is(gui) then
		return gui:Get()
	else
		return gui
	end
end

function SnapdragonController:ResetPosition()
	self.dragGui.Position = self.originPosition
end

function SnapdragonController:__bindControllerBehaviour()
	local maid = self.maid
	local debug = self.debug

	local gui = self:GetGui()
	local dragGui = self:GetDragGui()
	local snap = self.snapEnabled
	local DragEnded = self.DragEnded
	local DragBegan = self.DragBegan
	local DragChanged = self.DragChanged
	local snapAxis = self.snapAxis
	local dragAxis = self.dragAxis
	local dragRelativeTo = self.dragRelativeTo
	local dragGridSize = self.dragGridSize
	local dragPositionMode = self.dragPositionMode

	local useAbsoluteCoordinates = self._useAbsoluteCoordinates

	local reachedExtents

	local dragging
	local dragInput
	local dragStart
	local startPos
	local guiStartPos

	local function update(input)
		local snapHorizontalMargin = self.snapHorizontalMargin
		local snapVerticalMargin = self.snapVerticalMargin
		local snapThresholdVertical = self.snapThresholdVertical
		local snapThresholdHorizontal = self.snapThresholdHorizontal

		local screenSize = workspace.CurrentCamera.ViewportSize
		local delta = input.Position - dragStart

		if dragAxis == "X" then
			delta = Vector3.new(delta.X, 0, 0)
		elseif dragAxis == "Y" then
			delta = Vector3.new(0, delta.Y, 0)
		end

		gui = dragGui or gui
		reachedExtents = {
			X = "Float",
			Y = "Float",
		}

		local host = gui:FindFirstAncestorOfClass("ScreenGui") or gui:FindFirstAncestorOfClass("PluginGui")
		local topLeft = Vector2.new()
		if host and dragRelativeTo == "LayerCollector" then
			screenSize = host.AbsoluteSize
		elseif dragRelativeTo == "Parent" then
			assert(gui.Parent:IsA("GuiObject"), "DragRelativeTo is set to Parent, but the parent is not a GuiObject!")
			screenSize = gui.Parent.AbsoluteSize
		end

		if snap then
			local scaleOffsetX = screenSize.X * startPos.X.Scale
			local scaleOffsetY = screenSize.Y * startPos.Y.Scale
			local resultingOffsetX = startPos.X.Offset + delta.X
			local resultingOffsetY = startPos.Y.Offset + delta.Y
			local absSize = gui.AbsoluteSize + Vector2.new(snapHorizontalMargin.Y, snapVerticalMargin.Y + topLeft.Y)

			local anchorOffset =
				Vector2.new(gui.AbsoluteSize.X * gui.AnchorPoint.X, gui.AbsoluteSize.Y * gui.AnchorPoint.Y)

			if snapAxis == "XY" or snapAxis == "X" then
				local computedMinX = snapHorizontalMargin.X + anchorOffset.X
				local computedMaxX = screenSize.X - absSize.X + anchorOffset.X

				if (resultingOffsetX + scaleOffsetX) > computedMaxX - snapThresholdHorizontal.Y then
					resultingOffsetX = computedMaxX - scaleOffsetX
					reachedExtents.X = "Max"
				elseif (resultingOffsetX + scaleOffsetX) < computedMinX + snapThresholdHorizontal.X then
					resultingOffsetX = -scaleOffsetX + computedMinX
					reachedExtents.X = "Min"
				end
			end

			if snapAxis == "XY" or snapAxis == "Y" then
				local computedMinY = snapVerticalMargin.X + anchorOffset.Y
				local computedMaxY = screenSize.Y - absSize.Y + anchorOffset.Y

				if (resultingOffsetY + scaleOffsetY) > computedMaxY - snapThresholdVertical.Y then
					resultingOffsetY = computedMaxY - scaleOffsetY
					reachedExtents.Y = "Max"
				elseif (resultingOffsetY + scaleOffsetY) < computedMinY + snapThresholdVertical.X then
					resultingOffsetY = -scaleOffsetY + computedMinY
					reachedExtents.Y = "Min"
				end
			end

			if dragGridSize > 0 then
				resultingOffsetX = math.floor(resultingOffsetX / dragGridSize) * dragGridSize
				resultingOffsetY = math.floor(resultingOffsetY / dragGridSize) * dragGridSize
			end

			if dragPositionMode == "Offset" then
				local newPosition = UDim2.new(startPos.X.Scale, resultingOffsetX, startPos.Y.Scale, resultingOffsetY)

				gui.Position = newPosition

				DragChanged:Fire({
					GuiPosition = newPosition,
				})
			else
				local newPosition = UDim2.new(
					startPos.X.Scale + (resultingOffsetX / screenSize.X),
					0,
					startPos.Y.Scale + (resultingOffsetY / screenSize.Y),
					0
				)

				gui.Position = newPosition

				DragChanged:Fire({
					SnapAxis = snapAxis,
					GuiPosition = newPosition,
					DragPositionMode = dragPositionMode,
				})
			end
		else
			if dragGridSize > 0 then
				delta = Vector2.new(
					math.floor(delta.X / dragGridSize) * dragGridSize,
					math.floor(delta.Y / dragGridSize) * dragGridSize
				)
			end

			local newPosition =
				UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
			gui.Position = newPosition
			DragChanged:Fire({
				GuiPosition = newPosition,
			})
		end
	end

	maid.guiInputBegan = gui.InputBegan:Connect(function(input)
		local canDrag = true
		if type(self.canDrag) == "function" then
			canDrag = self.canDrag()
		end

		if
			(input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch)
			and canDrag
		then
			dragging = true
			dragStart = input.Position
			local draggingGui = (dragGui or gui)
			startPos = useAbsoluteCoordinates
					and UDim2.new(0, draggingGui.AbsolutePosition.X, 0, draggingGui.AbsolutePosition.Y)
				or draggingGui.Position
			guiStartPos = draggingGui.Position
			DragBegan:Fire({
				AbsolutePosition = (dragGui or gui).AbsolutePosition,
				InputPosition = dragStart,
				GuiPosition = startPos,
			})
			if debug then
				print("[snapdragon]", "Drag began", input.Position)
			end
		end
	end)

	maid.guiInputEnded = gui.InputEnded:Connect(function(input)
		if
			dragging
			and input.UserInputState == Enum.UserInputState.End
			and (
				input.UserInputType == Enum.UserInputType.MouseButton1
				or input.UserInputType == Enum.UserInputType.Touch
			)
		then
			dragging = false

			local draggingGui = (dragGui or gui)
			local endPos = draggingGui.Position --useAbsoluteCoordinates
			--and UDim2.new(0, draggingGui.AbsolutePosition.X, 0, draggingGui.AbsolutePosition.Y)
			--or draggingGui.Position

			DragEnded:Fire({
				InputPosition = input.Position,
				GuiPosition = endPos,
				ReachedExtents = reachedExtents,
				DraggedGui = dragGui or gui,
			})
			if debug then
				print("[snapdragon]", "Drag ended", input.Position)
			end

			-- Enable the ability to "reset" the position automatically.
			-- This will be used for stuff like roact-dnd
			local dragEndedResetsPosition = self.dragEndedResetsPosition
			if dragEndedResetsPosition then
				draggingGui.Position = guiStartPos
			end
		end
	end)

	maid.guiInputChanged = gui.InputChanged:Connect(function(input)
		if
			input.UserInputType == Enum.UserInputType.MouseMovement
			or input.UserInputType == Enum.UserInputType.Touch
		then
			dragInput = input
		end
	end)

	maid.uisInputChanged = UserInputService.InputChanged:Connect(function(input)
		if input == dragInput and dragging then
			update(input)
		end
	end)
end

function SnapdragonController:Connect()
	if self.locked then
		error("[SnapdragonController] Cannot connect locked controller!", 2)
	end

	local _, ref = self:GetDragGui()

	if not controllers[ref] or controllers[ref] == self then
		controllers[ref] = self
		self:__bindControllerBehaviour()
	else
		error("[SnapdragonController] This object is already bound to a controller")
	end
	return self
end

function SnapdragonController:Disconnect()
	if self.locked then
		error("[SnapdragonController] Cannot disconnect locked controller!", 2)
	end

	local _, ref = self:GetDragGui()

	local controller = controllers[ref]
	if controller then
		self.maid:DoCleaning()
		controllers[ref] = nil
	end
end

function SnapdragonController:Destroy()
	self:Disconnect()
	self.DragEnded:Destroy()
	self.DragBegan:Destroy()
	self.DragEnded = nil
	self.DragBegan = nil
	self.locked = true
end

return SnapdragonController
