local COMPAT, _, T = select(4, GetBuildInfo()), ...
local XU, MODERN, type = T.exUI, COMPAT > 10e4, type
local assert, getWidgetData, newWidgetData, setWidgetData, AddObjectMethods, CallObjectScript = XU:GetImpl()

local HOLD_HOVER_HINT_DURATION, ICON_FILE_NAMES, LookupIconName = 0.2, nil
local GetFileIDFromPath = _G.GetFileIDFromPath or function(path)
	return nil
end -- Fallback
local IconSelector, IconSelectorData, internal = {}, {}, {}
local IconSelectorProps = {
	api = IconSelector,
	scripts = { "OnIconSelect", "OnEditFocusGained", "OnEditFocusLost" },
	lastVisibleIcon = -1,
	selectedAsset = nil,
	viewIndexOffset = 0,
	firstAsset = nil,
	firstAssetValue = nil,
	firstAssetIsAtlas = false,
	columns = 14,
	rows = 7,
	cellWidth = 36,
	cellHeight = 36,
	pendingGridSync = true,
	manualInputHintText = "",
}
AddObjectMethods({ "IconSelector" }, IconSelectorProps)

function IconSelector:SetSelectedAsset()
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	return d.selectedAsset
end
function IconSelector:SetSelectedAsset(asset)
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	assert(
		asset == nil or type(asset) == "string" or type(asset) == "number",
		"Syntax: IconSelector:SetSelectedAsset(asset)"
	)
	d.selectedAsset = asset
	internal.RenderView(d, d.viewIndexOffset)
	internal.SyncHintText(d)
end
function IconSelector:SetFirstAsset(value, overrideAsset)
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	local asset = overrideAsset or value
	assert(
		asset == nil or type(asset) == "number" or type(asset) == "string",
		"Syntax: IconSelector:SetFirstAsset(value[, overrideAsset])"
	)
	d.firstAssetIsAtlas = type(asset) == "string"
		and not GetFileIDFromPath(asset)
		and C_Texture.GetAtlasInfo(asset)
		and true
	d.firstAssetValue, d.firstAsset = value, asset
	internal.RenderView(d, d.viewIndexOffset)
end
function IconSelector:SetManualInputHintText(text)
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	assert(type(text) == "string", 'Syntax: IconSelector:SetManualInputHintText("text")')
	d.manualInputHintText = text
	internal.SyncHintText(d)
end
function IconSelector:SetGridSize(rows, cols)
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	assert(
		type(rows) == "number" and rows > 0 and rows % 1 == 0 and type(cols) == "number" and cols > 0 and cols % 1 == 0,
		"Syntax: IconSelector:SetGridSize(rows, cols)"
	)
	d.pendingGridSync, d.rows, d.columns = true, rows, cols
	if d.proto.super.IsShown(d.self) then
		internal.ConfigureIconGrid(d)
	end
end
function IconSelector:FocusManualInput()
	local d = assert(getWidgetData(self, IconSelectorData), "Invalid object type")
	d.manualInput:SetFocus()
end
function IconSelector:IsSearchPossible()
	return not not (ICON_FILE_NAMES or LookupIconName and LookupIconName(0) or ICON_FILE_NAMES)
end

local GetAllIcons
do
	local function GetAbilityIcons(into)
		local AB = T.ActionBook and T.ActionBook:compatible(2, 40)
		local cc, ni, _, ic = AB and AB:GetNumCategories() >= 1 and AB:GetCategoryContents(1), #into + 1
		for i = 1, cc and #cc or 0 do
			_, _, ic = AB:GetActionDescription(cc(i))
			if ic then
				into[ni], ni = ic, ni + 1
			end
		end
	end
	function GetAllIcons()
		local a, m, ni = {}, {}, 1

		local function AddIcons(source)
			if not source then
				return
			end
			for i = 1, #source do
				local ai = source[i]
				-- Safe ID generation: prefer FileID, fallback to string path as ID if needed
				local fid = ai
				if type(ai) == "string" then
					local lookedUp = GetFileIDFromPath(ai)
					if lookedUp then
						fid = lookedUp
					end
				end

				if fid and m[fid] == nil then
					a[ni], m[fid], m[ai], ni = ai, ni, ni, ni + 1
				end
			end
		end

		-- Standard Macro Icons
		AddIcons(GetMacroIcons and GetMacroIcons() or (C_Macro and C_Macro.GetMacroIcons()))
		AddIcons(GetMacroItemIcons and GetMacroItemIcons() or (C_Macro and C_Macro.GetMacroItemIcons()))

		-- Fallback: If still empty, add many common ones to ensure grid isn't blank
		if #a < 5 then
			for i = 132096, 132300 do -- Large range of spell icons
				AddIcons({ i })
			end
		end

		GetAllIcons = function()
			return a
		end
		return a
	end
end
function LookupIconName(fid)
	LookupIconName = nil
	if
		select(5, C_AddOns.GetAddOnInfo("IconFileNames")) == "DEMAND_LOADED"
		and not C_AddOns.IsAddOnLoaded("IconFileNames")
	then
		C_AddOns.LoadAddOn("IconFileNames")
	end
	ICON_FILE_NAMES = _G.ICON_FILE_NAMES
	ICON_FILE_NAMES = type(ICON_FILE_NAMES) == "table" and ICON_FILE_NAMES or nil
	return ICON_FILE_NAMES and ICON_FILE_NAMES[fid]
end

function internal:OnHide()
	local d = getWidgetData(self, IconSelectorData)
	d.hoverIcon, d.hoverIconLT = nil
	self:GetParent():Hide()
end
function internal:OnIconClick(_button, _down)
	local d, checked = getWidgetData(self:GetParent(), IconSelectorData), self:GetChecked()
	local idx = checked and self:GetID() + d.viewIndexOffset or nil
	local tex = idx and (idx == 0 and d.firstAssetValue or idx > 0 and d.iconList[idx] or nil)
	if d.selectedButton then
		d.selectedButton:SetChecked(nil)
	end
	d.selectedAsset, d.selectedButton = tex, checked and self or nil
	PlaySound(SOUNDKIT.U_CHAT_SCROLL_BUTTON)
	CallObjectScript(d.self, "OnIconSelect", tex)
end
function internal:OnIconEnter()
	local d = getWidgetData(self:GetParent(), IconSelectorData)
	d.hoverIcon, d.hoverIconLT = self, nil
	internal.SyncHintText(d)
end
function internal:OnUpdateTick()
	local d = getWidgetData(self, IconSelectorData)
	if d.hoverIconLT and d.hoverIconLT + HOLD_HOVER_HINT_DURATION > GetTime() then
		return
	elseif d.hoverIconLT then
		d.hoverIcon = nil
	end
	self:SetScript("OnUpdate", nil)
	internal.SyncHintText(d)
end
function internal:OnIconLeave()
	local d = getWidgetData(self:GetParent(), IconSelectorData)
	if d.hoverIcon == self then
		d.hoverIconLT = GetTime()
		d.clipRoot:SetScript("OnUpdate", internal.OnUpdateTick)
	end
end
function internal.CreateIconButton(parent, pool, id)
	local f, sz = CreateFrame("CheckButton", nil, parent, nil, id), 32
	f:SetSize(sz, sz)
	f:SetNormalTexture("")
	f:SetHighlightTexture("Interface/Buttons/ButtonHilight-Square")
	f:GetHighlightTexture():SetBlendMode("ADD")
	f:SetCheckedTexture("Interface/Buttons/CheckButtonHilight")
	f:GetCheckedTexture():SetBlendMode("ADD")
	f:SetPushedTexture("Interface/Buttons/UI-Quickslot-Depress")
	f:SetScript("OnClick", internal.OnIconClick)
	f:SetScript("OnEnter", internal.OnIconEnter)
	f:SetScript("OnLeave", internal.OnIconLeave)
	local tex = f:CreateTexture(nil, "ARTWORK")
	tex:SetAllPoints()
	local bg = f:CreateTexture(nil, "BACKGROUND", nil, -1)
	bg:SetTexture("Interface/Buttons/UI-EmptySlot-Disabled")
	bg:SetPoint("CENTER", tex, "CENTER")
	bg:SetSize(1.5 * sz, 1.5 * sz)
	local edge = f:CreateTexture(nil, "OVERLAY", nil, -1)
	edge:SetTexture("Interface/Buttons/UI-Quickslot2")
	edge:SetSize(1.625 * sz, 1.625 * sz)
	edge:SetPoint("CENTER", tex, "CENTER", 0.25, -0.25)
	if MODERN then
		local m = f:CreateMaskTexture()
		m:SetTexture("Interface/FrameGeneral/UIFrameIconMask")
		m:SetAllPoints(tex)
		tex:AddMaskTexture(m)
	end
	f.tex, f.background, f.edge = tex, bg, edge
	pool[id] = f
	return f
end
function internal:OnScroll(value, interaction)
	local d, co = getWidgetData(self, IconSelectorData), value % 1
	d.origin:SetPoint("TOPLEFT", 0, co * d.cellHeight)
	internal.RenderView(d, (value - co) * d.columns, true)
	if interaction and d.hoverIconLT then
		internal.SyncHintText(d)
	end
end
function internal.RenderView(d, value, allowSkip)
	if allowSkip and d.viewIndexOffset == value then
		return
	end
	local icons, icontex, sel, selectedButton = d.pool, d.iconList, d.selectedAsset
	for i = 0, d.lastVisibleIcon do
		local ico, tex =
			icons[i].tex,
			i == 0 and value == 0 and (d.firstAsset or "Interface/Icons/INV_Misc_QuestionMark") or icontex[i + value]
		icons[i]:SetShown(not not tex)
		if tex then
			if i == 0 and value == 0 and d.firstAssetIsAtlas then
				ico:SetAtlas(tex)
			else
				ico:SetTexture(tex)
			end
			local check = sel and (tex == sel or ico:GetTexture() == sel)
			icons[i]:SetChecked(check)
			selectedButton = check and icons[i] or selectedButton
		end
	end
	d.viewIndexOffset, d.selectedButton = value, selectedButton
end
function internal.ConfigureIconGrid(d)
	local sb, pool, icontex, origin = d.scrollBar, d.pool, d.iconList, d.origin
	local rows, cols, cw, ch = d.rows, d.columns, d.cellWidth, d.cellHeight
	sb:SetStepsPerPage(rows, math.min(math.max(1, rows - 2), 5))
	sb:SetWindowRange(rows)
	sb:SetMinMaxValues(0, math.max(0, math.ceil((#icontex - cols * rows) / cols)))
	d.self:SetSize(42 + cols * cw, 40 + ch * rows)
	local usedIcons = (cols * (rows + 1)) - 1
	for i = 0, usedIcons do
		local w = pool[i] or internal.CreateIconButton(d.clipRoot, d.pool, i)
		w:SetPoint("TOPLEFT", origin, (i % cols) * cw, -ch * math.floor(i / cols))
	end
	for i = usedIcons + 1, d.lastVisibleIcon do
		pool[i]:Hide()
	end
	d.lastVisibleIcon, d.pendingGridSync = usedIcons, nil
end
function internal:OnShow()
	local d = getWidgetData(self, IconSelectorData)
	internal.SetIconList(d, GetAllIcons(), nil, -1)
	if d.pendingGridSync then
		internal.ConfigureIconGrid(d)
	end
	d.scrollBar:SetValue(0, true)
	local p, kbf = d.self:GetParent(), GetCurrentKeyBoardFocus()
	d.self:SetFrameLevel(math.max(d.self:GetFrameLevel(), p and p:GetFrameLevel() + 200))
	if kbf then
		kbf:ClearFocus()
	end
end
function internal.SyncHintText(d)
	if d.manualInput:HasFocus() then
		d.manualInputHint:Hide()
		d.manualInputFS:Show()
	elseif d.hoverIcon and d.hoverIcon:IsMouseMotionFocus() then
		d.manualInputHint:Show()
		local idx = d.hoverIcon:GetID() + d.viewIndexOffset
		local asset = idx == 0 and d.firstAsset or d.iconList[idx]
		local fn = ICON_FILE_NAMES and ICON_FILE_NAMES[asset] or LookupIconName and securecall(LookupIconName, asset)
		d.manualInputHint:SetText(fn and asset .. " |cff606060[|r" .. fn .. "|cff606060]|r" or asset or "")
		d.manualInputFS:Hide()
	elseif d.selectedAsset then
		d.manualInput:SetText(d.selectedAsset)
		d.manualInputFS:Show()
		d.manualInputHint:Hide()
	else
		d.manualInputHint:SetText(d.manualInputHintText)
		d.manualInputHint:Show()
		d.manualInput:SetText("")
	end
end
function internal:OnEditFocusChange()
	local d = getWidgetData(self, IconSelectorData)
	internal.SyncHintText(d)
	CallObjectScript(d.self, self:HasFocus() and "OnEditFocusGained" or "OnEditFocusLost", self)
end
function internal:OnEnterPressed()
	local d, text = getWidgetData(self, IconSelectorData), self:GetText()
	if IsAltKeyDown() and (ICON_FILE_NAMES or LookupIconName and securecall(LookupIconName, 0) or ICON_FILE_NAMES) then
		return internal.FilterIcons(d, text, self)
	end
	if text:match("%S") then
		local fid0, nt = GetFileIDFromPath(text), tonumber(text) or 0
		local fid1 = not fid0 and GetFileIDFromPath("Interface/Icons/" .. text)
		local path = fid0 and (fid0 < 0 and text or fid0)
			or fid1 and (fid1 < 0 and "Interface/Icons/" .. text or fid1)
			or C_Texture.GetAtlasInfo(text) and text
			or nt > 0 and nt
			or C_Spell.GetSpellTexture(text)
		if not path then
			return self:HighlightText()
		end
		d.selectedAsset = path
		CallObjectScript(d.self, "OnIconSelect", path)
	end
	self:ClearFocus()
end
function internal:OnEscapePressed()
	self:ClearFocus()
end
function internal.FilterIcons(d, query, _editbox)
	local of, nf = d.filter, query:lower():gsub("[.%%%[%]%-+*?()]", ""):match(".+")
	if of == nf then
		return
	elseif not nf then
		internal.SetIconList(d, GetAllIcons(), nil)
	else
		local t, ni, p, smatch = {}, 1, nf:gsub(" +", ".*"), nf.match
		local ot = nf:match(of or "") and d.iconList or GetAllIcons()
		for i = 1, #ot do
			local fn = ICON_FILE_NAMES[ot[i]]
			if fn and smatch(fn, p) then
				t[ni], ni = ot[i], ni + 1
			end
		end
		internal.SetIconList(d, t, nf)
	end
	internal.RenderView(d, d.viewIndexOffset)
end
function internal.SetIconList(d, iconList, filter, viewIndexOffset)
	d.iconList, d.filter, d.viewIndexOffset = iconList, filter, viewIndexOffset or d.viewIndexOffset
	d.scrollBar:SetMinMaxValues(0, math.ceil((#d.iconList - d.columns * d.rows) / d.columns))
end

local function CreateIconSelector(name, parent, outerTemplate, id)
	local f, d, t, a = CreateFrame("Frame", name, parent, outerTemplate, id)
	d = newWidgetData(f, IconSelectorData, IconSelectorProps)
	d.pool, d.backdrop =
		{}, XU:Create("Backdrop", f, {
			bgFile = "Interface/ChatFrame/ChatFrameBackground",
			edgeFile = "Interface/DialogFrame/UI-DialogBox-Border",
			tile = true,
			tileSize = 32,
			edgeSize = 32,
			insets = { left = 11, right = 11, top = 11, bottom = 10 },
			bgColor = 0xd8000000,
		})
	f:EnableMouse(1)
	f:SetToplevel(1)
	f:Hide()
	t = CreateFrame("Frame", nil, f)
	t:SetPoint("TOPLEFT", 12, -32)
	t:SetPoint("BOTTOMRIGHT", -31, 12)
	t:SetClipsChildren(true)
	t:Show()
	t:SetScript("OnHide", internal.OnHide)
	t:SetScript("OnShow", internal.OnShow)
	setWidgetData(t, IconSelectorData, d)
	t, d.clipRoot = CreateFrame("Frame", nil, t), t
	t:SetSize(1, 1)
	t:SetPoint("TOPLEFT")
	t:Show()
	t, d.origin = XU:Create("ScrollBar", nil, f), t
	t:SetStyle("common")
	t:SetPoint("TOPRIGHT", -9, -28)
	t:SetPoint("BOTTOMRIGHT", -9, 9)
	t:SetWheelScrollTarget(d.clipRoot, -2, -5, -2, -1)
	t:SetCoverTarget(d.clipRoot)
	t:SetScript("OnValueChanged", internal.OnScroll)
	setWidgetData(t, IconSelectorData, d)
	t, d.scrollBar = XU:Create("LineInput", nil, f), t
	t:SetStyle("chat")
	t:SetPoint("TOPLEFT", 11, -7)
	t:SetPoint("TOPRIGHT", -34, -7)
	setWidgetData(t, IconSelectorData, d)
	t:SetScript("OnEditFocusGained", internal.OnEditFocusChange)
	t:SetScript("OnEditFocusLost", internal.OnEditFocusChange)
	t:SetScript("OnEnterPressed", internal.OnEnterPressed)
	t:SetScript("OnEscapePressed", internal.OnEscapePressed)
	d.manualInputFS = t:GetRegions()
	a, d.manualInput = t:CreateFontString(nil, "OVERLAY", "GameFontHighlight"), t
	a:SetPoint("CENTER")
	a:SetTextColor(0.85, 0.85, 0.85)
	d.manualInputHint = a
	t = CreateFrame("Button", nil, f, "UIPanelCloseButton")
	if MODERN then
		t:SetPoint("TOPRIGHT", -6.5, -5.5)
		t:SetSize(24, 24)
		t:SetHitRectInsets(2, 2, 2, 3)
		a = t:CreateMaskTexture()
		a:SetTexture("Interface/common/common-iconmask")
		a:SetPoint("TOPLEFT", 2.4, -2.4)
		a:SetPoint("BOTTOMRIGHT", -2.8, 4)
		t:GetNormalTexture():AddMaskTexture(a)
		t:GetPushedTexture():AddMaskTexture(a)
		t:GetHighlightTexture():AddMaskTexture(a)
		a = t:CreateMaskTexture()
		a:SetTexture("Interface/common/common-mask-diamond")
		a:SetSize(30, 30)
		a:SetPoint("RIGHT", 3, 0.5)
		t:GetNormalTexture():AddMaskTexture(a)
		t:GetPushedTexture():AddMaskTexture(a)
		t:GetHighlightTexture():AddMaskTexture(a)
	else
		t:SetPoint("TOPRIGHT", -2, -1)
		t:SetHitRectInsets(4, 4, 4, 6)
	end
	t, d.closeButton = t:CreateTexture(nil, "BACKGROUND", nil, -5), t
	t:SetTexture("Interface/ChatFrame/UI-ChatInputBorder-Mid2")
	t:SetPoint("BOTTOMLEFT", d.manualInput, "BOTTOMRIGHT", 7, -6.25)
	t:SetSize(18.75, 8)
	t:SetTexCoord(0, 1, 0.75, 1)
	return f
end

XU:RegisterFactory("IconSelector", CreateIconSelector)
