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

local RangeDisplay = {
	Elapsed = 0,
	FontSizes = { 14, 16, 18, 20, 24, 28, 32, 36, 42, 48 },
}
SlickUI.RangeDisplay = RangeDisplay

-- These items are only used to produce a safe range bracket when WoW does
-- not expose exact world positions for the current target. Unsupported item
-- checks return nil and are skipped.
local ItemRangeChecks = {
	{ yards = 5, item = 8149 },
	{ yards = 8, item = 34368 },
	{ yards = 10, item = 32321 },
	{ yards = 15, item = 33069 },
	{ yards = 20, item = 10645 },
	{ yards = 25, item = 24268 },
	{ yards = 30, item = 835 },
	{ yards = 35, item = 18904 },
	{ yards = 40, item = 28767 },
	{ yards = 45, item = 23836 },
	{ yards = 60, item = 32825 },
	{ yards = 80, item = 35278 },
}

local function IsReadable(value)
	if canaccessvalue and not canaccessvalue(value) then return false end
	if issecretvalue and issecretvalue(value) then return false end
	return value ~= nil
end

local function SafeNumber(value)
	return IsReadable(value) and type(value) == "number" and value or nil
end

local function UnitExistsSafely(unit)
	local ok, exists = pcall(UnitExists, unit)
	return ok and IsReadable(exists) and exists == true
end

local function ExactTargetRange()
	if type(UnitPosition) ~= "function" then return nil end

	local playerOK, playerX, playerY, _, playerInstance = pcall(UnitPosition, "player")
	local targetOK, targetX, targetY, _, targetInstance = pcall(UnitPosition, "target")
	playerX, playerY = SafeNumber(playerX), SafeNumber(playerY)
	targetX, targetY = SafeNumber(targetX), SafeNumber(targetY)
	if not playerOK or not targetOK or not playerX or not playerY or not targetX or not targetY then return nil end
	if not IsReadable(playerInstance) or not IsReadable(targetInstance) or playerInstance ~= targetInstance then return nil end
	if targetX == 0 and targetY == 0 then return nil end

	local deltaX, deltaY = targetX - playerX, targetY - playerY
	local distance = math.sqrt((deltaX * deltaX) + (deltaY * deltaY))
	return SafeNumber(distance)
end

local function ItemRangeResult(itemID)
	if not C_Item or type(C_Item.IsItemInRange) ~= "function" then return nil end
	local ok, result = pcall(C_Item.IsItemInRange, itemID, "target")
	if not ok or not IsReadable(result) then return nil end
	if result == true or result == 1 then return true end
	if result == false or result == 0 then return false end
	return nil
end

local function BracketedTargetRange()
	local largestValid
	for _, check in ipairs(ItemRangeChecks) do
		local inRange = ItemRangeResult(check.item)
		if inRange ~= nil then
			largestValid = check.yards
			if inRange then return "< " .. check.yards .. " yd", check.yards end
		end
	end
	if largestValid then return "> " .. largestValid .. " yd", largestValid + 1 end
	return nil
end

function RangeDisplay:GetConfig()
	return SlickUI.DB.rangeDisplay
end

function RangeDisplay:GetRangeText()
	if not UnitExistsSafely("target") then return "Range: --", nil end

	local exact = ExactTargetRange()
	if exact then
		local rounded = math.floor(exact + 0.5)
		return "Range: " .. rounded .. " yd", rounded
	end

	local bracket, distance = BracketedTargetRange()
	return bracket and ("Range: " .. bracket) or "Range: ?", distance
end

function RangeDisplay:SetTextColor(distance)
	if not distance then
		self.Text:SetTextColor(unpack(Theme.Colors.mutedText))
	elseif distance <= 10 then
		self.Text:SetTextColor(0.25, 1.00, 0.35, 1)
	elseif distance <= 30 then
		self.Text:SetTextColor(1.00, 0.86, 0.20, 1)
	elseif distance <= 40 then
		self.Text:SetTextColor(1.00, 0.55, 0.12, 1)
	else
		self.Text:SetTextColor(1.00, 0.25, 0.18, 1)
	end
end

function RangeDisplay:Update()
	if not self.Frame or not self.Frame:IsShown() then return end
	local text, distance = self:GetRangeText()
	self.Text:SetText(text)
	self:SetTextColor(distance)
	self.Frame:SetSize(math.max(150, self.Text:GetStringWidth() + 28), math.max(36, self.Text:GetStringHeight() + 16))
end

function RangeDisplay:SavePosition()
	local frameX, frameY = self.Frame:GetCenter()
	local parentX, parentY = UIParent:GetCenter()
	frameX, frameY = SafeNumber(frameX), SafeNumber(frameY)
	parentX, parentY = SafeNumber(parentX), SafeNumber(parentY)
	if not frameX or not frameY or not parentX or not parentY then return end
	local config = self:GetConfig()
	config.x, config.y = frameX - parentX, frameY - parentY
end

function RangeDisplay:ApplyPosition()
	local config = self:GetConfig()
	self.Frame:ClearAllPoints()
	self.Frame:SetPoint("CENTER", UIParent, "CENTER", config.x or 0, config.y or 150)
end

function RangeDisplay:SetFontSize(size)
	size = tonumber(size)
	if not size then return end
	self:GetConfig().fontSize = size
	Theme:SetFont(self.Text, size)
	self:Update()
end

function RangeDisplay:ShowFontMenu(owner)
	if not MenuUtil or type(MenuUtil.CreateContextMenu) ~= "function" then return end
	MenuUtil.CreateContextMenu(owner or self.Frame, function(_, root)
		root:SetTag("SLICKUI_RANGE_FONT")
		root:CreateTitle("Range Check Font Size")
		for _, size in ipairs(self.FontSizes) do
			root:CreateRadio(tostring(size) .. " pt", function()
				return RangeDisplay:GetConfig().fontSize == size
			end, function()
				RangeDisplay:SetFontSize(size)
			end)
		end
	end)
end

function RangeDisplay:SetShown(shown)
	local config = self:GetConfig()
	config.shown = shown == true
	self.Frame:SetShown(config.shown)
	if config.shown then self:Update() end
	if SlickUI.InformationBars then SlickUI.InformationBars:UpdateBlocks() end
end

function RangeDisplay:Toggle()
	self:SetShown(not self:GetConfig().shown)
end

function RangeDisplay:GetWidgetText()
	return self:GetConfig().shown and "Range: On" or "Range: Off"
end

function RangeDisplay:BuildTooltip(tooltip)
	tooltip:AddLine("Range Check", 1, 0.82, 0.2)
	tooltip:AddLine("Shows the distance to your current target as text on the screen.", 1, 1, 1, true)
	tooltip:AddLine(" ")
	tooltip:AddLine("Left-click: Show or hide", 0.65, 0.85, 1)
	tooltip:AddLine("Right-click: Choose font size", 0.65, 0.85, 1)
	tooltip:AddLine("Drag the range text to move it.", 0.65, 0.85, 1)
end

function RangeDisplay:HandleWidgetClick(owner, button)
	if button == "RightButton" then
		self:ShowFontMenu(owner)
	else
		self:Toggle()
	end
end

function RangeDisplay:CreateFrame()
	local frame = CreateFrame("Button", "SlickUIRangeDisplay", UIParent)
	frame:SetFrameStrata("MEDIUM")
	frame:SetClampedToScreen(true)
	frame:SetMovable(true)
	frame:EnableMouse(true)
	frame:RegisterForDrag("LeftButton")
	frame:RegisterForClicks("RightButtonUp")

	local text = Theme:CreateText(frame, self:GetConfig().fontSize or 24)
	text:SetPoint("CENTER")
	text:SetJustifyH("CENTER")
	frame.Text = text
	self.Frame, self.Text = frame, text

	frame:SetScript("OnDragStart", function(self)
		if InCombatLockdown() then
			SlickUI:Print("Range display movement is unavailable during combat.")
			return
		end
		self:StartMoving()
	end)
	frame:SetScript("OnDragStop", function(self)
		self:StopMovingOrSizing()
		RangeDisplay:SavePosition()
	end)
	frame:SetScript("OnClick", function(self, button)
		if button == "RightButton" then RangeDisplay:ShowFontMenu(self) end
	end)
	frame:SetScript("OnEnter", function(self)
		GameTooltip:SetOwner(self, "ANCHOR_TOP")
		GameTooltip:AddLine("Range Check", 1, 0.82, 0.2)
		GameTooltip:AddLine("Drag to move. Right-click for font size.", 1, 1, 1)
		GameTooltip:Show()
	end)
	frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
	frame:SetScript("OnUpdate", function(_, elapsed)
		RangeDisplay.Elapsed = RangeDisplay.Elapsed + elapsed
		if RangeDisplay.Elapsed >= 0.15 then
			RangeDisplay.Elapsed = 0
			RangeDisplay:Update()
		end
	end)

	self:ApplyPosition()
	frame:SetShown(self:GetConfig().shown)
	self:Update()
end

function RangeDisplay:Initialize()
	self:CreateFrame()
end

SlickUI:RegisterModule("RangeDisplay", RangeDisplay)
