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

local MinimapButtonBar = {
	Entries = {},
	Buttons = {},
	Page = 0,
}
SlickUI.MinimapButtonBar = MinimapButtonBar

local EXCLUDED_BUTTONS = {
	AddonCompartmentFrame = true,
	ExpansionLandingPageMinimapButton = true,
	GarrisonLandingPageMinimapButton = true,
	GameTimeFrame = true,
	MiniMapMailFrame = true,
	MiniMapTracking = true,
	MiniMapTrackingButton = true,
	MiniMapWorldMapButton = true,
	MinimapTrackingButton = true,
	MinimapZoneTextButton = true,
	MinimapZoomIn = true,
	MinimapZoomOut = true,
	QueueStatusButton = true,
	TimeManagerClockButton = true,
}

local function Clamp(value, minimum, maximum)
	return math.max(minimum, math.min(maximum, value))
end

local function SavePoints(frame)
	local points = {}
	for index = 1, frame:GetNumPoints() do
		points[index] = { frame:GetPoint(index) }
	end
	return points
end

local function RestorePoints(frame, points)
	frame:ClearAllPoints()
	for _, point in ipairs(points or {}) do
		frame:SetPoint(unpack(point))
	end
end

local function IsEligibleDirectButton(button)
	if not button or button.SlickUIOrganizerTool or not button.IsObjectType or not button:IsObjectType("Button") then
		return false
	end

	local name = button:GetName()
	if name and EXCLUDED_BUTTONS[name] then
		return false
	end

	if name and name:find("^LibDBIcon10_") then
		return true
	end

	local parent = button:GetParent()
	-- Non-LDB addon buttons traditionally attach directly to Minimap. Do not
	-- sweep MinimapCluster itself because it also owns Blizzard's mail, queue,
	-- tracking, and other protected controls.
	if parent ~= Minimap then
		return false
	end

	local width, height = button:GetWidth() or 0, button:GetHeight() or 0
	return width >= 16 and width <= 64 and height >= 16 and height <= 64 and button:GetScript("OnClick") ~= nil
end

function MinimapButtonBar:GetSettings()
	return SlickUI.DB.minimapButtonBar
end

function MinimapButtonBar:CreateFrame()
	if self.Frame then return self.Frame end

	local frame = CreateFrame("Frame", "SlickUIMinimapButtonBar", UIParent, "BackdropTemplate")
	frame:SetFrameStrata("MEDIUM")
	frame:SetFrameLevel(5)
	frame:SetClampedToScreen(true)
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:EnableMouseWheel(true)
	frame:RegisterForDrag("LeftButton")
	frame:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	frame:SetBackdropColor(0.025, 0.03, 0.04, 0.88)
	frame:SetBackdropBorderColor(unpack(Theme.Colors.border))
	Theme:RegisterAccentFrame(frame)
	frame:SetScript("OnDragStart", function(self)
		if not MinimapButtonBar:GetSettings().locked and not InCombatLockdown() then
			MinimapButtonBar.Dragging = true
			self:StartMoving()
		end
	end)
	frame:SetScript("OnDragStop", function(self)
		if not MinimapButtonBar.Dragging then return end
		self:StopMovingOrSizing()
		MinimapButtonBar.Dragging = false
		local centerX, centerY = self:GetCenter()
		local parentX, parentY = UIParent:GetCenter()
		if centerX and centerY and parentX and parentY then
			local settings = MinimapButtonBar:GetSettings()
			settings.x = centerX - parentX
			settings.y = centerY - parentY
			self:ClearAllPoints()
			self:SetPoint("CENTER", UIParent, "CENTER", settings.x, settings.y)
		end
	end)
	frame:SetScript("OnMouseWheel", function(_, delta)
		MinimapButtonBar:Scroll(delta)
	end)

	local lock = CreateFrame("Button", nil, frame)
	lock.SlickUIOrganizerTool = true
	lock:SetSize(22, 22)
	lock:SetFrameLevel(frame:GetFrameLevel() + 5)
	lock:SetNormalTexture("Interface\\Buttons\\LockButton-Locked-Up")
	lock:SetPushedTexture("Interface\\Buttons\\LockButton-Locked-Down")
	lock:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
	lock:SetScript("OnClick", function()
		MinimapButtonBar:SetLocked(not MinimapButtonBar:GetSettings().locked)
	end)
	lock:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
		GameTooltip:AddLine(MinimapButtonBar:GetSettings().locked and "Unlock Minimap Button Bar" or "Lock Minimap Button Bar")
		GameTooltip:AddLine("Unlock to drag the bar.", 0.75, 0.75, 0.75)
		GameTooltip:Show()
	end)
	lock:SetScript("OnLeave", GameTooltip_Hide)
	frame.LockButton = lock

	local collapse = CreateFrame("Button", nil, frame, "BackdropTemplate")
	collapse.SlickUIOrganizerTool = true
	collapse:SetSize(20, 32)
	collapse:SetFrameLevel(frame:GetFrameLevel() + 6)
	collapse:SetBackdrop({ bgFile = Theme.Texture, edgeFile = Theme.Texture, edgeSize = 1 })
	collapse:SetBackdropColor(0.025, 0.03, 0.04, 0.94)
	collapse:SetBackdropBorderColor(unpack(Theme.Colors.border))
	Theme:RegisterAccentFrame(collapse)
	local collapseText = Theme:CreateText(collapse, 15)
	collapseText:SetPoint("CENTER", 0, 0)
	collapseText:SetTextColor(unpack(Theme.Colors.goldText))
	collapse.Text = collapseText
	collapse:SetScript("OnClick", function()
		MinimapButtonBar:SetCollapsed(not MinimapButtonBar:GetSettings().collapsed)
	end)
	collapse:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_LEFT")
		GameTooltip:AddLine(MinimapButtonBar:GetSettings().collapsed and "Expand Minimap Button Bar" or "Collapse Minimap Button Bar")
		GameTooltip:Show()
	end)
	collapse:SetScript("OnLeave", GameTooltip_Hide)
	frame.CollapseButton = collapse

	local pageText = Theme:CreateText(frame, 9)
	pageText:SetTextColor(unpack(Theme.Colors.mutedText))
	pageText:SetJustifyH("CENTER")
	frame.PageText = pageText

	self.Frame = frame
	return frame
end

function MinimapButtonBar:SetCollapsed(collapsed)
	self:GetSettings().collapsed = collapsed == true
	if InCombatLockdown() then
		local frame = self.Frame
		if not frame then return end
		if collapsed then
			frame:EnableMouse(false)
			frame:EnableMouseWheel(false)
			frame.LockButton:Hide()
			frame.PageText:Hide()
			for _, button in ipairs(self.Buttons) do
				local entry = self.Entries[button]
				if entry and not entry.hiddenByBar and not button:IsProtected() then
					entry.hiddenForCombatCollapse = true
					button:Hide()
				end
			end
		else
			frame:EnableMouse(true)
			frame:EnableMouseWheel(true)
			frame.LockButton:Show()
			frame.PageText:Show()
			for _, button in ipairs(self.Buttons) do
				local entry = self.Entries[button]
				if entry and entry.hiddenForCombatCollapse and not button:IsProtected() then
					entry.hiddenForCombatCollapse = nil
					button:Show()
				end
			end
		end
		self.PendingRefresh = true
		self:RefreshTools()
		return
	end
	self:Arrange()
end

function MinimapButtonBar:SetLocked(locked)
	local settings = self:GetSettings()
	settings.locked = locked == true
	self:RefreshTools()
	if SlickUI.Options and SlickUI.Options.RefreshMinimapButtonBarControls then
		SlickUI.Options:RefreshMinimapButtonBarControls()
	end
	SlickUI:Print(settings.locked and "Minimap Button Bar locked." or "Minimap Button Bar unlocked; drag its background to move it.")
end

function MinimapButtonBar:RefreshTools()
	if not self.Frame then return end
	local settings = self:GetSettings()
	local lock = self.Frame.LockButton
	lock:SetNormalTexture(settings.locked and "Interface\\Buttons\\LockButton-Locked-Up" or "Interface\\Buttons\\LockButton-Unlocked-Up")
	lock:SetPushedTexture(settings.locked and "Interface\\Buttons\\LockButton-Locked-Down" or "Interface\\Buttons\\LockButton-Unlocked-Down")
	if settings.collapsed then
		self.Frame:SetBackdropColor(0, 0, 0, 0)
		self.Frame:SetBackdropBorderColor(0, 0, 0, 0)
	elseif settings.locked then
		self.Frame:SetBackdropColor(0.025, 0.03, 0.04, 0.88)
		self.Frame:SetBackdropBorderColor(unpack(Theme.Colors.border))
	else
		self.Frame:SetBackdropColor(0.025, 0.03, 0.04, 0.88)
		self.Frame:SetBackdropBorderColor(0.25, 0.85, 1, 1)
	end
	self.Frame.CollapseButton.Text:SetText(settings.collapsed and "<" or ">")
end

function MinimapButtonBar:RememberButton(button, name)
	if self.Entries[button] then return end

	self.Entries[button] = {
		name = name or button:GetName() or tostring(button),
		parent = button:GetParent(),
		points = SavePoints(button),
		width = button:GetWidth(),
		height = button:GetHeight(),
		scale = button:GetScale(),
		strata = button:GetFrameStrata(),
		level = button:GetFrameLevel(),
		wasShown = button:IsShown(),
	}

	if not button:GetScript("OnMouseWheel") then
		button:EnableMouseWheel(true)
		button:HookScript("OnMouseWheel", function(_, delta)
			if SlickUI.DB.minimapButtonBar.enabled then
				MinimapButtonBar:Scroll(delta)
			end
		end)
		self.Entries[button].wheelHook = true
	end
end

function MinimapButtonBar:DiscoverButtons()
	local discovered = {}
	local iconLibrary = LibStub("LibDBIcon-1.0", true)
	if iconLibrary and iconLibrary.objects then
		for name, button in pairs(iconLibrary.objects) do
			if button then
				self:RememberButton(button, name)
				if not button.db or not button.db.hide then
					discovered[button] = true
				end
			end
		end
	end

	if Minimap and Minimap.GetChildren then
		for _, button in ipairs({ Minimap:GetChildren() }) do
			if IsEligibleDirectButton(button) then
				self:RememberButton(button)
				local entry = self.Entries[button]
				if entry.hiddenByBar or button:IsShown() then
					discovered[button] = true
				end
			end
		end
	end

	-- Buttons already organized by SlickUI are no longer direct children of
	-- Minimap. Keep them in discovery across periodic refreshes.
	if self.Frame then
		for button, entry in pairs(self.Entries) do
			if button:GetParent() == self.Frame and (entry.hiddenByBar or button:IsShown()) then
				discovered[button] = true
			end
		end
	end

	wipe(self.Buttons)
	for button in pairs(discovered) do
		table.insert(self.Buttons, button)
	end
	table.sort(self.Buttons, function(left, right)
		return string.lower(self.Entries[left].name) < string.lower(self.Entries[right].name)
	end)
end

function MinimapButtonBar:GetCapacity()
	local settings = self:GetSettings()
	return Clamp(math.floor(settings.rows or 1), 1, 3) * Clamp(math.floor(settings.length or 8), 3, 20)
end

function MinimapButtonBar:Scroll(delta)
	local capacity = self:GetCapacity()
	local pages = math.max(1, math.ceil(#self.Buttons / capacity))
	self.Page = Clamp(self.Page - (delta > 0 and 1 or -1), 0, pages - 1)
	self:Arrange()
end

function MinimapButtonBar:Arrange()
	if self.Dragging then return end
	local frame = self:CreateFrame()
	local settings = self:GetSettings()
	if not settings.enabled then
		self:ReleaseButtons()
		frame:Hide()
		return
	end

	if InCombatLockdown() then
		self.PendingRefresh = true
		return
	end

	local rows = Clamp(math.floor(settings.rows or 1), 1, 3)
	local length = Clamp(math.floor(settings.length or 8), 3, 20)
	local size = Clamp(math.floor(settings.buttonSize or 31), 22, 42)
	local spacing = Clamp(math.floor(settings.spacing or 2), 0, 10)
	local cell = size + spacing
	local horizontal = settings.orientation ~= "VERTICAL"
	local capacity = rows * length
	local pages = math.max(1, math.ceil(#self.Buttons / capacity))
	self.Page = Clamp(self.Page, 0, pages - 1)

	local toolSpace = 34
	local width = 8 + (horizontal and length or rows) * cell - spacing
	local height = 8 + (horizontal and rows or length) * cell - spacing
	if horizontal then width = width + toolSpace else height = height + toolSpace end
	frame:SetSize(width, height)
	frame:ClearAllPoints()
	frame:SetPoint("CENTER", UIParent, "CENTER", settings.x or 500, settings.y or 250)
	frame:Show()
	frame.CollapseButton:ClearAllPoints()
	frame.CollapseButton:SetPoint("LEFT", frame, "RIGHT", 3, 0)
	frame.CollapseButton:Show()

	if settings.collapsed then
		frame:EnableMouse(false)
		frame:EnableMouseWheel(false)
		frame.LockButton:Hide()
		frame.PageText:Hide()
		for index, button in ipairs(self.Buttons) do
			local entry = self.Entries[button]
			entry.hiddenForCombatCollapse = nil
			entry.hiddenByBar = true
			button:Hide()
		end
		self:RefreshTools()
		return
	end

	frame:EnableMouse(true)
	frame:EnableMouseWheel(true)
	frame.LockButton:Show()
	frame.PageText:Show()

	frame.LockButton:ClearAllPoints()
	frame.PageText:ClearAllPoints()
	if horizontal then
		frame.LockButton:SetPoint("RIGHT", frame, "RIGHT", -4, 0)
		frame.PageText:SetPoint("RIGHT", frame.LockButton, "LEFT", -1, 0)
		frame.PageText:SetWidth(18)
	else
		frame.LockButton:SetPoint("BOTTOM", frame, "BOTTOM", 0, 4)
		frame.PageText:SetPoint("BOTTOM", frame.LockButton, "TOP", 0, 1)
		frame.PageText:SetWidth(width - 8)
	end
	frame.PageText:SetText(pages > 1 and string.format("%d/%d", self.Page + 1, pages) or "")

	local first = self.Page * capacity + 1
	local last = first + capacity - 1
	for index, button in ipairs(self.Buttons) do
		local entry = self.Entries[button]
		entry.hiddenForCombatCollapse = nil
		if entry.wheelHook then button:EnableMouseWheel(true) end
		if index >= first and index <= last then
			local visibleIndex = index - first
			local column, row
			if horizontal then
				column = visibleIndex % length
				row = math.floor(visibleIndex / length)
			else
				row = visibleIndex % length
				column = math.floor(visibleIndex / length)
			end
			-- Reparenting is required for reliable mouse hit testing. Merely
			-- anchoring a Minimap child outside the minimap can leave it visible
			-- while its clickable region remains clipped by the old parent.
			button:SetParent(frame)
			button:ClearAllPoints()
			button:SetPoint("TOPLEFT", frame, "TOPLEFT", 4 + column * cell, -4 - row * cell)
			button:SetSize(size, size)
			button:SetScale(1)
			button:SetFrameStrata("MEDIUM")
			button:SetFrameLevel(frame:GetFrameLevel() + 2)
			entry.hiddenByBar = false
			button:Show()
		else
			entry.hiddenByBar = true
			button:Hide()
		end
	end
	self:RefreshTools()
end

function MinimapButtonBar:ReleaseButtons()
	local iconLibrary = LibStub("LibDBIcon-1.0", true)
	for button, entry in pairs(self.Entries) do
		if not InCombatLockdown() or not button:IsProtected() then
			button:SetParent(entry.parent or Minimap)
			button:SetSize(entry.width, entry.height)
			button:SetScale(entry.scale)
			button:SetFrameStrata(entry.strata)
			button:SetFrameLevel(entry.level)
			RestorePoints(button, entry.points)
			entry.hiddenByBar = false
			if button.db and button.db.hide then
				button:Hide()
			elseif entry.wasShown then
				button:Show()
			end
			if entry.wheelHook then button:EnableMouseWheel(false) end
		end
	end

	if iconLibrary and iconLibrary.objects then
		for name, button in pairs(iconLibrary.objects) do
			if self.Entries[button] and iconLibrary.Refresh then
				iconLibrary:Refresh(name, button.db)
			end
		end
	end
end

function MinimapButtonBar:Refresh()
	self:DiscoverButtons()
	self:Arrange()
end

function MinimapButtonBar:ApplySettings()
	self.Page = 0
	self:Refresh()
end

function MinimapButtonBar:Initialize()
	self:CreateFrame()
	self.EventFrame = CreateFrame("Frame")
	self.EventFrame:RegisterEvent("PLAYER_LOGIN")
	self.EventFrame:RegisterEvent("ADDON_LOADED")
	self.EventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
	self.EventFrame:SetScript("OnEvent", function(_, event)
		if event == "PLAYER_REGEN_ENABLED" and MinimapButtonBar.PendingRefresh then
			MinimapButtonBar.PendingRefresh = false
		end
		C_Timer.After(0.25, function() MinimapButtonBar:Refresh() end)
	end)

	local elapsed = 0
	self.EventFrame:SetScript("OnUpdate", function(_, delta)
		elapsed = elapsed + delta
		if elapsed >= 2 then
			elapsed = 0
			if not MinimapButtonBar.Dragging then MinimapButtonBar:Refresh() end
		end
	end)
	C_Timer.After(1, function() MinimapButtonBar:Refresh() end)
end

SlickUI:RegisterModule("MinimapButtonBar", MinimapButtonBar)
