local _, ns = ...
local SlickUI = ns.SlickUI

local Movers = {
	Registered = {},
	GridLines = {},
}
SlickUI.Movers = Movers

local function RaiseEditModeControl(frame)
	-- Mover overlays and externally discovered frames can also occupy
	-- FULLSCREEN_DIALOG. TOOLTIP plus an explicit level keeps the edit-mode
	-- controls readable and clickable regardless of creation order.
	frame:SetFrameStrata("TOOLTIP")
	frame:SetFrameLevel(1000)
end

local function RoundToStep(value, step)
	return math.floor(value / step + 0.5) * step
end

local function GetEffectiveSize(frame)
	local parentScale = UIParent:GetEffectiveScale()
	local scale = frame:GetEffectiveScale() / parentScale
	return frame:GetWidth() * scale, frame:GetHeight() * scale
end

local function GetRelativeCenter(frame)
	local centerX, centerY = frame:GetCenter()
	local parentX, parentY = UIParent:GetCenter()
	if not centerX or not centerY or not parentX or not parentY then return nil end
	return centerX - parentX, centerY - parentY
end

local function FindNearestOffset(candidates, threshold)
	local best
	for _, offset in ipairs(candidates) do
		if math.abs(offset) <= threshold and (not best or math.abs(offset) < math.abs(best)) then
			best = offset
		end
	end
	return best or 0
end

local function ApplyPosition(frame, key)
	local position = SlickUI.DB.positions[key] or SlickUI.Defaults.positions[key]
	frame:ClearAllPoints()
	frame:SetPoint("CENTER", UIParent, "CENTER", position.x, position.y)
end

local function SavePosition(frame, key)
	local x, y = GetRelativeCenter(frame)
	if not x or not y then return end

	SlickUI.DB.positions[key] = SlickUI.DB.positions[key] or {}
	SlickUI.DB.positions[key].x = x
	SlickUI.DB.positions[key].y = y
end

function Movers:CreateGrid()
	if self.Grid then return self.Grid end
	local grid = CreateFrame("Frame", "SlickUIEditModeGrid", UIParent)
	grid:SetAllPoints(UIParent)
	grid:SetFrameStrata("BACKGROUND")
	grid:SetFrameLevel(1)
	grid:EnableMouse(false)
	grid:Hide()
	self.Grid = grid
	return grid
end

function Movers:RefreshGrid()
	local grid = self:CreateGrid()
	for _, line in ipairs(self.GridLines) do line:Hide() end
	if SlickUI.DB.locked or not SlickUI.DB.editMode.snapToGrid then
		grid:Hide()
		return
	end

	local step = math.max(8, SlickUI.DB.editMode.gridSize or 20)
	local width, height = UIParent:GetWidth(), UIParent:GetHeight()
	local lineIndex = 0
	local function AcquireLine()
		lineIndex = lineIndex + 1
		local line = self.GridLines[lineIndex]
		if not line then
			line = grid:CreateTexture(nil, "BACKGROUND")
			line:SetColorTexture(1.00, 0.67, 0.12, 0.16)
			self.GridLines[lineIndex] = line
		end
		line:ClearAllPoints()
		line:Show()
		return line
	end

	local x = 0
	while x <= width / 2 do
		for _, signedX in ipairs(x == 0 and { 0 } or { x, -x }) do
			local line = AcquireLine()
			line:SetPoint("TOP", grid, "TOP", signedX, 0)
			line:SetPoint("BOTTOM", grid, "BOTTOM", signedX, 0)
			line:SetWidth(x == 0 and 2 or 1)
			line:SetColorTexture(1.00, 0.67, 0.12, x == 0 and 0.42 or 0.16)
		end
		x = x + step
	end

	local y = 0
	while y <= height / 2 do
		for _, signedY in ipairs(y == 0 and { 0 } or { y, -y }) do
			local line = AcquireLine()
			line:SetPoint("LEFT", grid, "LEFT", 0, signedY)
			line:SetPoint("RIGHT", grid, "RIGHT", 0, signedY)
			line:SetHeight(y == 0 and 2 or 1)
			line:SetColorTexture(1.00, 0.67, 0.12, y == 0 and 0.42 or 0.16)
		end
		y = y + step
	end
	grid:Show()
end

function Movers:CreateExitButton()
	if self.ExitButton then return self.ExitButton end
	local button = CreateFrame("Button", "SlickUICloseEditModeButton", UIParent, "UIPanelButtonTemplate")
	button:SetSize(170, 30)
	button:SetPoint("TOP", UIParent, "TOP", 0, -34)
	RaiseEditModeControl(button)
	button:SetText("Close SlickUI Edit Mode")
	if button:GetFontString() then SlickUI.Theme:SetFont(button:GetFontString(), 11) end
	button:SetScript("OnClick", function() Movers:SetLocked(true) end)
	button:Hide()
	self.ExitButton = button
	return button
end

function Movers:OpenBlizzardEditMode()
	if InCombatLockdown() then
		SlickUI:Print("Blizzard Edit Mode cannot be opened during combat.")
		return
	end
	-- End SlickUI editing first so its overlays and grid cannot compete with
	-- Blizzard's selection handles or alter the layout while Blizzard saves it.
	self:SetLocked(true)
	if SlickUI.ActionBars and not SlickUI.ActionBars:BeginBlizzardEditMode() then return end
	C_Timer.After(0, function()
		if C_AddOns and C_AddOns.LoadAddOn then pcall(C_AddOns.LoadAddOn, "Blizzard_EditMode") end
		if EditModeManagerFrame then
			if not EditModeManagerFrame.SlickUIActionBarHandoffHooked then
				EditModeManagerFrame:HookScript("OnHide", function()
					if SlickUI.ActionBars then SlickUI.ActionBars:EndBlizzardEditMode() end
				end)
				EditModeManagerFrame.SlickUIActionBarHandoffHooked = true
			end
			ShowUIPanel(EditModeManagerFrame)
		else
			if SlickUI.ActionBars then SlickUI.ActionBars:EndBlizzardEditMode() end
			SlickUI:Print("Blizzard Edit Mode is not available yet.")
		end
	end)
end

function Movers:CreateBlizzardEditButton()
	if self.BlizzardEditButton then return self.BlizzardEditButton end
	local button = CreateFrame("Button", "SlickUIOpenBlizzardEditModeButton", UIParent, "UIPanelButtonTemplate")
	button:SetSize(170, 30)
	button:SetPoint("RIGHT", self:CreateExitButton(), "LEFT", -10, 0)
	RaiseEditModeControl(button)
	button:SetText("Blizzard Edit Mode")
	if button:GetFontString() then SlickUI.Theme:SetFont(button:GetFontString(), 11) end
	button:SetScript("OnClick", function() Movers:OpenBlizzardEditMode() end)
	button:Hide()
	self.BlizzardEditButton = button
	return button
end

function Movers:IsMoverOverlayVisible(key)
	local hidden = SlickUI.DB and SlickUI.DB.editMode and SlickUI.DB.editMode.hiddenMovers
	return not hidden or hidden[key] ~= true
end

function Movers:SetMoverOverlayVisible(key, visible)
	SlickUI.DB.editMode.hiddenMovers = SlickUI.DB.editMode.hiddenMovers or {}
	if visible then
		SlickUI.DB.editMode.hiddenMovers[key] = nil
	else
		SlickUI.DB.editMode.hiddenMovers[key] = true
	end
	local frame = self.Registered[key]
	if frame and frame.SlickMoverOverlay then
		frame.SlickMoverOverlay:EnableMouse(not SlickUI.DB.locked and visible)
		frame.SlickMoverOverlay:SetShown(not SlickUI.DB.locked and visible)
	end
	if SlickUI.ExternalFrames then
		local entry = SlickUI.ExternalFrames.Frames[key]
		if entry and entry.overlay then
			entry.overlay:EnableMouse(not SlickUI.DB.locked and visible)
			entry.overlay:SetShown(not SlickUI.DB.locked and visible)
		end
		if key == "MinimapCluster" and SlickUI.ExternalFrames.MinimapProxy then
			SlickUI.ExternalFrames.MinimapProxy:SetShown(not SlickUI.DB.locked and visible)
		end
		if key == "Blizzard Alerts" and SlickUI.ExternalFrames.AlertAnchor then
			SlickUI.ExternalFrames.AlertAnchor:EnableMouse(not SlickUI.DB.locked and visible)
			SlickUI.ExternalFrames.AlertAnchor:SetShown(not SlickUI.DB.locked and visible)
		end
	end
end

function Movers:GetMoverOverlayNames()
	local names, seen = {}, {}
	local function Add(key)
		if not seen[key] then names[#names + 1], seen[key] = key, true end
	end
	for key in pairs(self.Registered) do Add(key) end
	if SlickUI.ExternalFrames then
		for key in pairs(SlickUI.ExternalFrames.Frames) do Add(key) end
		if SlickUI.ExternalFrames.MinimapProxy then Add("MinimapCluster") end
		if SlickUI.ExternalFrames.AlertAnchor then Add("Blizzard Alerts") end
	end
	table.sort(names)
	return names
end

function Movers:CreateVisibilityButton()
	if self.VisibilityButton then return self.VisibilityButton end
	local button = CreateFrame("Button", "SlickUIMoverVisibilityButton", UIParent, "UIPanelButtonTemplate")
	button:SetSize(160, 30)
	button:SetPoint("LEFT", self:CreateExitButton(), "RIGHT", 10, 0)
	RaiseEditModeControl(button)
	button:SetText("Mover Visibility")
	if button:GetFontString() then SlickUI.Theme:SetFont(button:GetFontString(), 10) end
	local panel = CreateFrame("Frame", "SlickUIMoverVisibilityPanel", UIParent, "BackdropTemplate")
	panel:SetSize(300, 430)
	panel:SetPoint("TOP", button, "BOTTOM", 0, -4)
	-- Mover overlays use FULLSCREEN_DIALOG. TOOLTIP keeps this control panel
	-- readable and clickable above every overlay, including external movers.
	panel:SetFrameStrata("TOOLTIP")
	panel:SetFrameLevel(1000)
	panel:SetClampedToScreen(true)
	panel:SetBackdrop({ bgFile = SlickUI.Theme.Texture, edgeFile = SlickUI.Theme.Texture, edgeSize = 2 })
	panel:SetBackdropColor(0.02, 0.025, 0.04, 0.98)
	panel:SetBackdropBorderColor(1, 0.67, 0.12, 1)
	local title = SlickUI.Theme:CreateText(panel, 11)
	title:SetPoint("TOPLEFT", 10, -9)
	title:SetText("Temporarily show movers")
	title:SetTextColor(1, 0.82, 0.12)

	local scroll = CreateFrame("ScrollFrame", nil, panel, "UIPanelScrollFrameTemplate")
	scroll:SetPoint("TOPLEFT", 7, -30)
	scroll:SetPoint("BOTTOMRIGHT", -28, 8)
	local child = CreateFrame("Frame", nil, scroll)
	child:SetSize(252, 1)
	scroll:SetScrollChild(child)
	panel.Rows = {}
	panel.ScrollFrame = scroll
	panel.ScrollChild = child
	local function ScrollVisibilityList(_, delta)
		local maximum = math.max(0, child:GetHeight() - scroll:GetHeight())
		scroll:SetVerticalScroll(math.max(0, math.min(maximum, scroll:GetVerticalScroll() - (delta * 24))))
	end
	for _, wheelFrame in ipairs({ panel, scroll, child }) do
		wheelFrame:EnableMouseWheel(true)
		wheelFrame:SetScript("OnMouseWheel", ScrollVisibilityList)
	end
	panel.ScrollWheel = ScrollVisibilityList
	panel:Hide()
	self.VisibilityPanel = panel
	button:SetScript("OnClick", function()
		if panel:IsShown() then
			panel:Hide()
		else
			Movers:RefreshVisibilityPanel()
			panel:Show()
		end
	end)
	button:Hide()
	self.VisibilityButton = button
	return button
end

function Movers:RefreshVisibilityPanel()
	local panel = self.VisibilityPanel
	if not panel then return end
	local names = self:GetMoverOverlayNames()
	for index, key in ipairs(names) do
		local row = panel.Rows[index]
		if not row then
			row = CreateFrame("Button", nil, panel.ScrollChild)
			row:SetSize(248, 20)
			row:SetPoint("TOPLEFT", 0, -((index - 1) * 20))
			local check = CreateFrame("CheckButton", nil, row, "UICheckButtonTemplate")
			check:SetSize(18, 18)
			check:SetPoint("LEFT", 1, 0)
			check:SetHitRectInsets(0, 0, 0, 0)
			local text = SlickUI.Theme:CreateText(row, 9)
			text:SetPoint("LEFT", check, "RIGHT", 3, 0)
			text:SetPoint("RIGHT", row, "RIGHT", -3, 0)
			text:SetJustifyH("LEFT")
			text:SetWordWrap(false)
			row.Check = check
			row.Text = text
			local function ToggleMover(self)
				local owner = self.MoverRow or self
				if not owner.MoverKey then return end
				local visible = not Movers:IsMoverOverlayVisible(owner.MoverKey)
				Movers:SetMoverOverlayVisible(owner.MoverKey, visible)
				owner.Check:SetChecked(visible)
			end
			row:SetScript("OnClick", function(self)
				ToggleMover(self)
			end)
			check.MoverRow = row
			check:SetScript("OnClick", ToggleMover)
			row:EnableMouseWheel(true)
			row:SetScript("OnMouseWheel", panel.ScrollWheel)
			check:EnableMouseWheel(true)
			check:SetScript("OnMouseWheel", panel.ScrollWheel)
			panel.Rows[index] = row
		end
		row.MoverKey = key
		row.Text:SetText(key)
		row.Check:SetChecked(self:IsMoverOverlayVisible(key))
		row:Show()
	end
	for index = #names + 1, #panel.Rows do panel.Rows[index]:Hide() end
	panel.ScrollChild:SetHeight(math.max(panel.ScrollFrame:GetHeight(), #names * 20))
	local maximum = math.max(0, panel.ScrollChild:GetHeight() - panel.ScrollFrame:GetHeight())
	panel.ScrollFrame:SetVerticalScroll(math.min(panel.ScrollFrame:GetVerticalScroll(), maximum))
end

function Movers:HideSettingsWindow()
	if SettingsPanel and SettingsPanel:IsShown() then
		if SettingsPanel.Close then
			SettingsPanel:Close(true)
		else
			SettingsPanel:Hide()
		end
	elseif InterfaceOptionsFrame and InterfaceOptionsFrame:IsShown() then
		HideUIPanel(InterfaceOptionsFrame)
	end
end

function Movers:SnapFrame(frame, key)
	local x, y = GetRelativeCenter(frame)
	if not x or not y then return end
	local edit = SlickUI.DB.editMode
	local gridSize = math.max(8, edit.gridSize or 20)
	if edit.snapToGrid then
		x, y = RoundToStep(x, gridSize), RoundToStep(y, gridSize)
	end

	if edit.snapToFrames then
		local width, height = GetEffectiveSize(frame)
		local threshold = math.max(6, math.min(16, gridSize / 2))
		for otherKey, other in pairs(self.Registered) do
			if otherKey ~= key and other:IsShown() then
				local otherX, otherY = GetRelativeCenter(other)
				if otherX and otherY then
					local otherWidth, otherHeight = GetEffectiveSize(other)
					local left, right = x - width / 2, x + width / 2
					local top, bottom = y + height / 2, y - height / 2
					local otherLeft, otherRight = otherX - otherWidth / 2, otherX + otherWidth / 2
					local otherTop, otherBottom = otherY + otherHeight / 2, otherY - otherHeight / 2
					local xOffset = FindNearestOffset({ otherLeft - left, otherRight - right, otherRight - left, otherLeft - right, otherX - x }, threshold)
					local yOffset = FindNearestOffset({ otherTop - top, otherBottom - bottom, otherBottom - top, otherTop - bottom, otherY - y }, threshold)
					x, y = x + xOffset, y + yOffset
				end
			end
		end
	end

	frame:ClearAllPoints()
	frame:SetPoint("CENTER", UIParent, "CENTER", x, y)
	SavePosition(frame, key)
end

function Movers:Register(frame, key, label)
	self.Registered[key] = frame
	frame:SetMovable(true)
	frame:SetClampedToScreen(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetScript("OnDragStart", function(self)
		if SlickUI.DB.locked or InCombatLockdown() then
			return
		end

		self:StartMoving()
	end)
	frame:SetScript("OnDragStop", function(self)
		self:StopMovingOrSizing()
		if InCombatLockdown() then return end
		Movers:SnapFrame(self, key)
	end)

	local overlay = CreateFrame("Frame", nil, frame)
	overlay:SetAllPoints(frame)
	overlay:SetFrameLevel(frame:GetFrameLevel() + 20)
	overlay:EnableMouse(false)
	overlay:RegisterForDrag("LeftButton")
	overlay:SetScript("OnDragStart", function()
		if SlickUI.DB.locked or InCombatLockdown() then return end
		frame:StartMoving()
	end)
	overlay:SetScript("OnDragStop", function()
		frame:StopMovingOrSizing()
		if InCombatLockdown() then return end
		Movers:SnapFrame(frame, key)
	end)

	local shade = overlay:CreateTexture(nil, "BACKGROUND")
	shade:SetAllPoints()
	-- Keep the full frame draggable without layering a heavy translucent fill
	-- over every mover. Dense overlapping fills create substantial GPU overdraw.
	shade:SetColorTexture(1, 0.65, 0.08, 0.06)

	local text = SlickUI.Theme:CreateText(overlay, 12)
	text:SetPoint("CENTER")
	text:SetText(label)
	text:SetTextColor(unpack(SlickUI.Theme.Colors.goldText))

	overlay:Hide()
	frame.SlickMoverOverlay = overlay
	frame.SlickMoverKey = key
	ApplyPosition(frame, key)
	if frame.SlickMoverOnly then
		frame:EnableMouse(not SlickUI.DB.locked)
		frame:SetFrameStrata(SlickUI.DB.locked and "LOW" or "DIALOG")
	end
end

function Movers:SetLocked(locked)
	if not SlickUI:IsAvailableOutOfCombat("Frame locking") then
		return
	end

	SlickUI.DB.locked = locked and true or false
	if SlickUI.ActionBars then SlickUI.ActionBars:SetSlickEditMode(not SlickUI.DB.locked) end
	for _, frame in pairs(SlickUI.UnitFrames) do
		if frame.SlickMoverOverlay then
			local visible = not SlickUI.DB.locked and self:IsMoverOverlayVisible(frame.SlickMoverKey)
			frame.SlickMoverOverlay:EnableMouse(visible)
			frame.SlickMoverOverlay:SetShown(visible)
		end
		if frame.SlickMoverOnly then
			frame:EnableMouse(not SlickUI.DB.locked)
			frame:SetFrameStrata(SlickUI.DB.locked and "LOW" or "DIALOG")
		end
	end
	if SlickUI.DB.locked then
		if SlickUI.ExternalFrames then SlickUI.ExternalFrames:EndEditMode() end
		self:CreateGrid():Hide()
		self:CreateExitButton():Hide()
		self:CreateBlizzardEditButton():Hide()
		self:CreateVisibilityButton():Hide()
		if self.VisibilityPanel then self.VisibilityPanel:Hide() end
	else
		self:RefreshGrid()
		self:CreateExitButton():Show()
		self:CreateBlizzardEditButton():Show()
		self:CreateVisibilityButton():Show()
		self:HideSettingsWindow()
		if SlickUI.ExternalFrames then SlickUI.ExternalFrames:BeginEditMode() end
	end
end

function Movers:SetGridEnabled(enabled)
	SlickUI.DB.editMode.snapToGrid = enabled == true
	self:RefreshGrid()
end

function Movers:SetFrameSnapEnabled(enabled)
	SlickUI.DB.editMode.snapToFrames = enabled == true
end

function Movers:SetGridSize(size)
	SlickUI.DB.editMode.gridSize = math.max(8, math.min(100, tonumber(size) or 20))
	self:RefreshGrid()
end

function Movers:ResetPositions()
	if not SlickUI:IsAvailableOutOfCombat("Position reset") then
		return
	end

	for key, defaults in pairs(SlickUI.Defaults.positions) do
		SlickUI.DB.positions[key] = { x = defaults.x, y = defaults.y }
		local frame = SlickUI.UnitFrames[key]
		if frame then
			ApplyPosition(frame, key)
		end
	end
	if SlickUI.ExternalFrames then SlickUI.ExternalFrames:ResetPositions() end

	SlickUI:Print("Frame positions reset.")
end

function Movers:ApplySavedPositions()
	if InCombatLockdown() then return end
	for key, frame in pairs(SlickUI.UnitFrames) do
		if frame and (SlickUI.DB.positions[key] or SlickUI.Defaults.positions[key]) then
			ApplyPosition(frame, key)
		end
	end
end

function Movers:Initialize()
	self:CreateGrid()
	self:CreateExitButton()
	self:CreateBlizzardEditButton()
	self:CreateVisibilityButton()
	self.EventFrame = CreateFrame("Frame")
	self.EventFrame:RegisterEvent("DISPLAY_SIZE_CHANGED")
	self.EventFrame:RegisterEvent("UI_SCALE_CHANGED")
	self.EventFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
	self.EventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
	self.EventFrame:SetScript("OnEvent", function(_, event)
		if event == "PLAYER_REGEN_DISABLED" and not SlickUI.DB.locked then
			SlickUI.DB.locked = true
			if SlickUI.ExternalFrames then SlickUI.ExternalFrames:EndEditMode() end
			Movers.Grid:Hide()
			Movers.ExitButton:Hide()
			Movers.BlizzardEditButton:Hide()
			Movers.VisibilityButton:Hide()
			for _, frame in pairs(SlickUI.UnitFrames) do
				if frame.SlickMoverOverlay then frame.SlickMoverOverlay:Hide() end
			end
		elseif event == "PLAYER_REGEN_ENABLED" then
			Movers:SetLocked(SlickUI.DB.locked)
		else
			Movers:RefreshGrid()
		end
	end)
end

SlickUI:RegisterModule("Movers", Movers)
