local run = game:GetService("RunService")
local inputService = game:GetService("UserInputService")
local collectionService = _G.Tags
local mouseModule = require(_G.Modules.MouseModule)
local methodModule = require(_G.Modules.MethodModule)
local partStateModule = require(_G.Modules.PartStateModule)
local selectionBox = script.SelectionBox
local adjacentBox = script.AdjacentBox
local statGui
if game.Players.LocalPlayer then
	statGui = game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("StatGui")
end
local gui
local current = nil

local mode = "place"
local realMode = "place"
local rotSnap = 15
local rotSnapKey = 45.
local moveSnap = 0.5
local modelScaleSnap = 0.1
local touchModeConnect = true
local touchingBoxes = {}

local controlModifier = false -- embed
local shiftModifier = false -- lower snap

local ModeButtons = {
	clone = nil,
	resize = nil
}

local pointer = nil

local function temp()
	local a = Instance.new("Part")
	a.Parent = game.Workspace
	a.Size = Vector3.new(1,1,1)
	a.Anchored = true
	a.CanCollide = false
	a.Transparency = 0.5
	a.Material = Enum.Material.Neon
	return a
end

local a = temp()
local b = temp()

local modeEnd = function()
	if current.axes then current.axes:Destroy() end
end
local focusAxes = function()
	current.axes:ScaleTo(current.size.Magnitude*1.3)
	methodModule.setItemCFrame(current.axes,current.cframe)
	for _,part in pairs(current.axes:GetChildren()) do
		if not current.axis then 
			part.Transparency = 0.5 
		else
			part.Transparency = 1
		end
	end
	if current.axis then current.axis.Transparency = 0 end
end
local createAxes = function(name)
	current.axes = _G.Storage.Assets[name]:Clone()
	current.axes.Parent = game.Workspace
	current.axes.PrimaryPart.Shape = Enum.PartType.Ball
	focusAxes()
end
local getBounds = function()
	local cframe,size
	if current.model:IsA("Model") then
		cframe,size = current.model:GetBoundingBox()
	else
		cframe = current.model.CFrame
		size = current.model.size
	end	
	current.cframe = cframe
	current.size = size
end
local styleModel = function()
	local function style(part)
		part.Anchored = true
		part.CanCollide = false
	end
	if current.model:IsA("BasePart") then style(current.model) end
	for _,part in pairs(current.model:GetChildren()) do
		if part:IsA("BasePart") then
			style(part)
		end
	end
end
local distanceAlong = function(o : Vector3,n : Vector3,p : Vector3) -- origin, normal, point
	local k = (n.X*(p.X-o.X)+n.Y*(p.Y-o.Y)+n.Z*(p.Z-o.Z))--/(n.x*n.x+n.y*n.y+n.z*n.z) -- is 1
	return (n*k+o-p).Magnitude
end
local processPointer = function(inputObject)
	local position = inputObject.Position
	local result,ray = mouseModule.getMouseHit() -- get whatever we're hovering
	current.location = position
	current.ray = ray
	current.result = result
end

local function axisClick(inputObject) -- For whenever we click in a mode supporting axis
	if current.axes then -- if there are axes (there should be)
		processPointer(inputObject)
		if current.result then
			if current.result.Instance.Parent == current.axes then -- if it's an axis
				current.axis = current.result.Instance -- set it
				pointer = inputObject -- set the inputObject for current selection
			end
		end
	end
end

local function round(v : Vector3,snap) -- just round a vector based on the snapping
	snap = snap or moveSnap
	return Vector3.new(math.round(v.X/snap)*snap,math.round(v.Y/snap)*snap,math.round(v.Z/snap)*snap)
end

local function mask(a : Vector3,b : Vector3) -- multiply each axis together
	return Vector3.new(a.X*b.X,a.Y*b.Y,a.Z*b.Z)
end

local function clamp(a : Vector3, min : Vector3, max : Vector3)
	return Vector3.new(math.clamp(a.X,min.X,max.X),math.clamp(a.Y,min.Y,max.Y),math.clamp(a.Z,min.Z,max.Z))
end

local function rayProjectToRay(ray,axis,root)
	local n2 = ray.Direction
	local o2 = ray.Origin
	local n1 = (axis.Position - axis.Parent.PrimaryPart.Position).Unit
	local o1 = root or current.axis.Position
	-- using more vector magic to find the closest point on the axis line to the ray of the pointer
	local s = n2:Dot(n1)
	return o1+n1*((n1:Dot(o2-o1)+s*(n2:Dot(o1-o2)))/(1-s*s))
end

local function axisDragClick(inputObject)
	axisClick(inputObject)
	if current.axis then
		local p = rayProjectToRay(current.ray,current.axis)
		current.modelOrigin = current.touchingCFrame
		current.dragOrigin = current.axes.PrimaryPart.CFrame:PointToObjectSpace(p)
		current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame)
	end
end

local function rayProjectToPlane(ray,axis)
	-- using some vector magic to find the point of intersection between the ray of the pointer and the plane of rotation (arc handle plane)
	return ray.Origin+(axis.CFrame.RightVector:Dot(axis.Position-ray.Origin))/(axis.CFrame.RightVector:Dot(ray.Direction))*ray.Direction
end

local function axisRotateClick(inputObject)
	axisClick(inputObject)
	if current.axis then
		local p = rayProjectToPlane(current.ray,current.axis)
		current.dragOrigin = current.axis.CFrame:PointToObjectSpace(p)
		current.modelOrigin = current.touchingCFrame
		current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame)
		current.initialRotation = current.rotation
	end
end

local modeStart = {}
modeStart.clone = function()
	modeEnd()
end
modeStart.rotate = function()
	modeEnd()
	createAxes("RotateAxes")
end
modeStart.resize = function()
	modeEnd()
	createAxes("ResizeAxes")
	for _,item in pairs(current.axes:GetChildren()) do
		local axis = item.Name:match("Part(.)")
		if axis then
			if (current.scaleTerms or Vector3.one)[axis] == 0 then axis:Destroy() end -- if the scale terms say that this axis should not be scaled
			if current.model:IsA("Model") then item.Color = Color3.fromRGB(13, 105, 172) end
		end
	end
end
modeStart.move = function()
	modeEnd()
	createAxes("MoveAxes")
end
modeStart.place = function()
	modeEnd()
end

local function setMode(newMode)
	for _,other_button in pairs(_G.PlaceModeFrame:GetChildren()) do
		if other_button:IsA("TextButton") then
			other_button.BackgroundColor3 = Color3.new(1,1,1)
			if (current.disabled or {})[other_button.Name] then other_button.Visible = false else other_button.Visible = true end
		end
	end
	realMode = newMode
	mode = newMode
	gui.Confirm.Text = "Place"
	if not current.touching and newMode ~= "place" then
		mode = "place"
		gui.Confirm.Text = "Confirm"
	end
	_G.PlaceModeFrame[realMode].BackgroundColor3 = Color3.new(1, 0.956863, 0.345098)
	_G.PlaceModeFrame[mode].BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
	modeStart[mode]()
end

local function confirmPlace(inputObject,forced)
	if not current then return end
	if realMode ~= mode then setMode(realMode) return end
	if (inputObject and inputObject.UserInputType == Enum.UserInputType.MouseButton1) or not inputObject then
		current:confirm(forced)
		if not current then return end
		methodModule.setItemCFrame(current.model,CFrame.new())
		current.touching = nil
		current.touchingCFrame = CFrame.new()
		setMode(realMode)
	end
end

local modeSnap = {
	
}

local modeClick = {}
modeClick.place = function(inputObject)
	confirmPlace(inputObject,false)
end
modeClick.clone = function(inputObject)
	setMode("place")
end
modeClick.move = function(inputObject)
	axisDragClick(inputObject)
	current.moveOffset = current.newMoveOffset or Vector3.zero
end
modeClick.rotate = axisRotateClick
modeClick.resize = function(inputObject)
	axisDragClick(inputObject)
	current.startSize = current.size
	if current.model:IsA("Model") then current.startScale = current.model:GetScale() end
end
local modeLoop = {}
modeLoop.default = function()
	if current.touching and current.touching:IsA("BasePart") then
		methodModule.setItemCFrame(current.model,current.touching.CFrame:ToWorldSpace(current.touchingCFrame or CFrame.identity))
	end
	statGui.Enabled = not not statGui.Adornee
	getBounds()
end
modeLoop.clone = function()
	current:clone(current.touching)
	modeLoop.default()
end
modeLoop.rotate = function()
	modeLoop.default()
	focusAxes()
	if pointer then 
		processPointer(pointer)
		local p = rayProjectToPlane(current.ray,current.axis)
		local plane = current.touching.CFrame:ToWorldSpace(current.axisOrigin)
		local toDrag = plane.Rotation:PointToWorldSpace(current.dragOrigin)
		local toPointer = p-plane.Position
		local angle = math.round(toDrag:Angle(toPointer,plane.RightVector)/math.rad(rotSnap))*math.rad(rotSnap)
		local modelCFrame = current.touching.CFrame:ToWorldSpace(current.modelOrigin) -- cframe of axes/object in the world 
		local newCFrame = plane.Rotation:ToWorldSpace(CFrame.Angles(angle,0,0)*plane.Rotation:ToObjectSpace(modelCFrame.Rotation)) + modelCFrame.Position -- put it back into object space
		current.rotation = current.initialRotation * newCFrame:ToObjectSpace(modelCFrame):Inverse()
		current.touchingCFrame = current.touching.CFrame:ToObjectSpace(newCFrame) -- need to add rotation
		statGui.TextLabel.Text = tonumber(math.round(math.deg(angle)*1000)/1000)
		statGui.Adornee = current.model
	else 
		statGui.Adornee = nil
	end
end
local function psumag(v)
	return v.X + v.Y + v.Z
end
modeLoop.resize = function()
	modeLoop.default()
	focusAxes()
	if pointer then
		processPointer(pointer)
		local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin)
		local start = origin:PointToWorldSpace(current.dragOrigin) -- the start of the scale drag
		local diff = (rayProjectToRay(current.ray,current.axis)-start) -- difference between start and end
		local scaleChange = origin.Rotation:PointToObjectSpace(diff)
		if current.model:IsA("Model") then
			local before = mask(current.startSize,current.dragOrigin.Unit) -- the size in this axis before scaling
			local after = psumag(mask(scaleChange,current.dragOrigin.Unit)) -- the size in this axis after scaling
			local scale = math.round(((((after*math.sign(psumag(before)))/psumag(before))+1)/modelScaleSnap)*current.startScale)*modelScaleSnap
			scale = math.clamp(scale,0.5,current.model:GetAttribute("MaxSize") or 3)
			current.model:ScaleTo(scale)
			current.model:SetAttribute("Size",Vector3.new(scale,0,0))
			current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(((scale/current.startScale)-1)*before*0.5)
			statGui.TextLabel.Text = tonumber(math.round(scale*1000)/1000)
		else
			local change = round(mask(scaleChange,current.scaleTerms or Vector3.one))
			current.model.Size = clamp(current.startSize + mask(change,current.dragOrigin.Unit),Vector3.one*0.1,current.model:GetAttribute("MaxSize") or Vector3.one*20)
			current.model:SetAttribute("Size",current.model.Size)
			current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(change/2)
			statGui.TextLabel.Text = tonumber(math.round(mask(current.model.Size,current.dragOrigin.Unit).Magnitude*1000)/1000)
		end
		statGui.Adornee = current.model
	else
		statGui.Adornee = nil
	end
	modeLoop.default() -- the resize system has this weird lag, i cannot be asked fixing it unfortunately
end
modeLoop.move = function()
	modeLoop.default()
	focusAxes()
	if pointer then
		processPointer(pointer)
		local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin)
		local start = origin:PointToWorldSpace(current.dragOrigin)
		local diff = (rayProjectToRay(current.ray,current.axis,start)-start) -- difference between start and end
		diff = current.touching.CFrame.Rotation:PointToObjectSpace(diff)
		local offset = psumag(mask(current.moveOffset,diff.Unit))
		local len = math.abs(psumag(mask(current.size,origin:ToObjectSpace(current.axis.CFrame).RightVector))) 
		diff = diff.Unit * math.round(math.clamp(diff.Magnitude,-len*0.5-offset,len*0.5-offset)/moveSnap) * moveSnap
		current.touchingCFrame = current.modelOrigin + diff
		current.newMoveOffset = current.moveOffset + diff
		statGui.TextLabel.Text = tonumber(math.round(diff.Magnitude*1000)/1000)
		statGui.Adornee = current.model
	else
		statGui.Adornee = nil
	end
end
modeLoop.place = function()
	local result,ray,update = mouseModule.getMouseHit()
	if result and result.Instance and update then
		current.touching = result.Instance
		if current.touching.Locked then adjacentBox.Adornee = nil end
		if not current.touching then return end
		if not current.touching.Position then print(current.touching) return end
		local rotation = (current.touching.CFrame - current.touching.Position)
		rotation = rotation * current.rotation
		local s = current.size*0.5 -- So half the size of the part, rotated correctly
		local max = 0
		local points = {Vector3.new(s.X,s.Y,s.Z),Vector3.new(s.X,-s.Y,s.Z),Vector3.new(-s.X,s.Y,s.Z),Vector3.new(-s.X,-s.Y,s.Z),Vector3.new(s.X,s.Y,-s.Z),Vector3.new(s.X,-s.Y,-s.Z),Vector3.new(-s.X,s.Y,-s.Z),Vector3.new(-s.X,-s.Y,-s.Z)}
		for _,point in pairs(points) do
			max = math.max(max,result.Normal:Dot(rotation:PointToWorldSpace(point)))
		end
		local push = max
		local snap = moveSnap
		if shiftModifier then snap = moveSnap * 0.5 end
		local depth = (Ray.new(result.Position,-result.Normal):ClosestPoint(current.touching.Position)-result.Position).Magnitude
		local flat = current.touching.CFrame:PointToWorldSpace(round(current.touching.CFrame:PointToObjectSpace(result.Position - result.Normal * depth),snap))
		if controlModifier or current.model:HasTag("Node") then push = 0 end
		local cframe = rotation + flat + result.Normal * (depth + push) -- Add the rotation, Snapped movement perpendicular to normal and the Offset/Pushback
		if current.model:HasTag("Node") then
			local part
			if current.touching:GetAttribute("Nodes") then
				part = current.touching
			elseif current.touching.Parent:GetAttribute("Nodes") then
				part = current.touching.Parent
			end
			if part then
				local ideal
				local current_distance = 1
				for _,node in ipairs(partStateModule.get_points(part)) do
					local distance = (node - result.Position).Magnitude
					if distance < current_distance then
						current_distance = distance
						if part:IsA("Model") then 
							ideal = part.PrimaryPart
						else
							ideal = part
						end
						if current.model:IsA("Model") then
							cframe = rotation + node
						else
							cframe = current.model.CFrame.Rotation + node
						end
						current.touching = ideal
					end
				end
			end
		end
		if current.touching:HasTag("Node") then
			if current.model:IsA("Model") then
				cframe = rotation + current.touching.Position
			else
				cframe = current.model.CFrame.Rotation + current.touching.Position
			end
		end
		methodModule.setItemCFrame(current.model,cframe)
		current.touchingCFrame = current.touching.CFrame:ToObjectSpace(cframe)
		current.newMoveOffset = Vector3.zero
		adjacentBox.Adornee = current.touching
	end
	getBounds()
end

local function render()
	if not current then return end
	controlModifier = inputService:IsKeyDown(Enum.KeyCode.LeftControl) or inputService:IsKeyDown(Enum.KeyCode.LeftMeta)
	shiftModifier = inputService:IsKeyDown(Enum.KeyCode.LeftShift)
	modeLoop[mode]()
end

--[[
config:
 - Can Resize
 - Confirm Callback
]]
local placetouchbuttoncolor
local function toggleTouching()
	touchModeConnect = not touchModeConnect
	if touchModeConnect then
		_G.PlaceTouchButton.BackgroundColor3 = placetouchbuttoncolor
	else
		_G.PlaceTouchButton.BackgroundColor3 = Color3.new(1,1,1)
	end
end
function begin(model,config)
	if current then current:stop() end
	gui.Visible = true
	current = config or {}
	current.stop = function(config)
		if not config then warn("No config passed to PlacementModule; current:stop(). Using current.") config = current end
		if not config then return end
		if config.axes then config.axes:Destroy() end
		if config.stopped then config:stopped() end
		current = nil
		selectionBox.Adornee = nil
		adjacentBox.Adornee = nil
		for _,box in pairs(touchingBoxes) do
			box:Destroy()
		end
		touchingBoxes = {}
		gui.Visible = false
	end
	model.Parent = game.Workspace.Hidden
	if model:IsA("Model") then
		model.PrimaryPart.Anchored = true
	else
		model.Anchored = true
	end
	ModeButtons.resize.Visible = config.resize
	if not config.resize and mode == "resize" then mode = "place" end
	if not config.rotate and mode == "rotate" then mode = "place" end
	_G.PlaceTouchButton.Visible = config.touch 
	selectionBox.Adornee = model
	current.model = model
	if not current.model then error("urmum") end
	current.connections = current.connections or {}
	current.rotation = current.model:GetAttribute("Rotation") or CFrame.identity
	setMode(realMode)
	styleModel()
	getBounds()
	modeStart[mode]()
	return config
end
function init(_gui)
	gui = _gui
	_G:WaitFor("PlaceModeFrame") _G:WaitFor("PlaceSnapFrame") _G:WaitFor("PlaceCancelButton")
	statGui.Enabled = false
	placetouchbuttoncolor = _G.PlaceTouchButton.BackgroundColor3
	gui.Visible = false
	run:BindToRenderStep("placementmodule",Enum.RenderPriority.Last.Value,render)
	local touchOverlapParams = OverlapParams.new()
	touchOverlapParams.MaxParts = 3
	touchOverlapParams.FilterType = Enum.RaycastFilterType.Include
	touchOverlapParams.FilterDescendantsInstances = {game.Workspace.Objects}
	touchOverlapParams.RespectCanCollide = true
	touchOverlapParams.Tolerance = 0.05
	run.Heartbeat:Connect(function() -- Touching parts loop
		if current and current.model and current.touch then -- in placement
			local loop_current = current
			local connections = {}
			if touchModeConnect then
				local items
				if current.model:IsA("Model") then -- Check all potential subparts
					items = current.model:GetChildren()
				elseif current.model:IsA("BasePart") then
					items = {current.model}
				end
				local count = 0
				for _,item in pairs(items) do
					if item:IsA("BasePart") then
						for _,part in pairs(workspace:GetPartsInPart(item,touchOverlapParams)) do -- Get touching parts
							if current.connections[part] then connections[part] = part continue end
							local object = _G.Methods.getOwnedOrNil(_G.Player,part) -- Check if this is owned
							if object then
								connections[object] = true -- Add to the current touching ones
								current.connections[object] = { -- Add to the final list
									part = object
								}
								if current.model ~= item then current.connections[object].name = item.Name end
								
								--if part == current.touching then continue end
								local box = object:FindFirstChild("TouchingBox")
								if not box then box = script.TouchingBox:Clone() box.Parent = object end
								touchingBoxes[object] = box
							end
						end
					end
					if count > 3 then wait() count = 0 else count = count + 1 end -- Dangerous! Things can change after this (wait)
					if current ~= loop_current then return end -- Current is invalid now so stop!
				end
			elseif current.touching then
				local connection = {
					part = current.touching
				}
				current.connections[current.touching] = connection
				connections[current.touching] = connection
			end
			for index,connection in pairs(current.connections) do -- Remove all invalid connections too
				if not connections[index] then
					if touchingBoxes[index] then
						touchingBoxes[index]:Destroy()
						touchingBoxes[index] = nil
					end
					current.connections[index] = nil 
				end
			end
		end
	end)
	_G.PlaceTouchButton.Activated:Connect(toggleTouching)
	inputService.InputBegan:Connect(function(inputObject,irrelevant)
		if not current then return end
		if irrelevant then return end
		local sign = 1
		if shiftModifier then sign = -1 end
		if inputObject.UserInputType == Enum.UserInputType.Keyboard then
			if inputObject.KeyCode == Enum.KeyCode.R then
				if controlModifier then
					_G.PlaceSnapInputFrame.RotSnap:CaptureFocus()
				else
					current.rotation = current.rotation * CFrame.Angles(math.rad(rotSnapKey*sign),0,0)	
				end
			elseif inputObject.KeyCode == Enum.KeyCode.T then
				if controlModifier then
					_G.PlaceSnapInputFrame.MoveSnap:CaptureFocus()
				else
					current.rotation = current.rotation * CFrame.Angles(0,math.rad(rotSnapKey*sign),0)
				end
			elseif inputObject.KeyCode == Enum.KeyCode.Y then
				if controlModifier then
					_G.PlacePolyInputFrame.PolyThicknessInputBox:CaptureFocus()
				else
					current.rotation = current.rotation * CFrame.Angles(0,0,math.rad(rotSnapKey*sign))
				end
			elseif inputObject.KeyCode == Enum.KeyCode.G  and not controlModifier then
				current.rotation = CFrame.identity
			elseif inputObject.KeyCode == Enum.KeyCode.E then
				setMode("place")
			elseif inputObject.KeyCode == Enum.KeyCode.Q and current.rotate then
				setMode("rotate")
			elseif inputObject.KeyCode == Enum.KeyCode.Z and current.resize then
				setMode("resize")
			elseif inputObject.KeyCode == Enum.KeyCode.X then
				setMode("move")
			elseif inputObject.KeyCode == Enum.KeyCode.U and current.touch then
				toggleTouching()
			elseif inputObject.KeyCode == Enum.KeyCode.Backspace then
				if current and current.cancel then current:cancel() end
			end
		elseif inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
			if not shiftModifier then modeClick[mode](inputObject) else confirmPlace(inputObject,true) end
		end
	end)
	inputService.TouchStarted:Connect(function(object)
		modeClick[mode](object)
	end)
	inputService.InputEnded:Connect(function(inputObject,irrelevant)
		if current and inputObject == pointer then
			pointer = nil
			current.axis = nil
		end
	end)
	gui.Confirm.Activated:Connect(function()
		confirmPlace(nil,true)
	end)
	_G.PlaceCancelButton.Activated:Connect(function()
		if current and current.cancel then current:cancel() end
	end)
	for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do
		if button:IsA("TextButton") then
			ModeButtons[button.Name] = button
			if mode == button.Name then
				button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
			end
			button.Activated:Connect(function()
				setMode(button.Name)
				modeStart[mode]()
			end)
		end
	end
	local moveSnapInput = _G.PlaceSnapInputFrame.MoveSnap
	local function evaluateSnapInput(text)
		return text:match("([%d%.]+).-")
	end
	_G.PlaceSnapInputFrame.MoveSnap.FocusLost:Connect(function()
		local num = evaluateSnapInput(moveSnapInput.Text)
		if num then moveSnap = tonumber(num) end
		moveSnapInput.Text = tostring(moveSnap).." stud"
	end)
	local rotSnapInput = _G.PlaceSnapInputFrame.RotSnap
	_G.PlaceSnapInputFrame.RotSnap.FocusLost:Connect(function()
		local num = evaluateSnapInput(rotSnapInput.Text)
		if num then rotSnap = tonumber(num) end
		rotSnapInput.Text = tostring(rotSnap).." deg"
	end)
	moveSnapInput.Text = tostring(moveSnap).." stud"
	rotSnapInput.Text = tostring(rotSnap).." deg"

	local snap_buttons = {}
	for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do
		if button:IsA("TextButton") then
			table.insert(snap_buttons,button)
			if mode == button.Name then
				button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333)
			end
			button.Activated:Connect(function()
				setMode(button.Name)
				modeStart[mode]()
			end)
		end
	end
end

return getfenv()