
---@type detailsframework
local detailsFramework = _G["DetailsFramework"]
if (not detailsFramework or not DetailsFrameworkCanLoad) then
	return
end

local CreateFrame = CreateFrame
local GetSpellInfo = GetSpellInfo
local GameTooltip = GameTooltip
local unpack = unpack

---mixin to use with DetailsFramework:Mixin(table, detailsFramework.SortFunctions)
---add methods to be used on scrollframes
---@class df_scrollboxmixin
detailsFramework.ScrollBoxFunctions = {
	--set a function to run right before the refresh function (scroll:Refresh())
	--this function receives the same parameters as the refresh function
	SetPreRefreshFunction = function(self, func)
		self.pre_refresh_func = func
	end,

	---refresh the scrollbox by resetting all lines created with :CreateLine(), then calling the refresh_func which was set at :CreateScrollBox()
	---@param self table
	---@return table
	Refresh = function(self)
		--hide all frames and tag as not in use
		self._LinesInUse = 0
		for index, frame in ipairs(self.Frames) do
			if (not self.DontHideChildrenOnPreRefresh) then
				frame:Hide()
			end
			frame._InUse = nil
		end

		local offset = 0
		if (self.IsFauxScroll) then
			self:UpdateFaux(#self.data, self.LineAmount, self.LineHeight)
			offset = self:GetOffsetFaux()
		end

		if (self.pre_refresh_func) then
			detailsFramework:Dispatch(self.pre_refresh_func, self, self.data, offset, self.LineAmount)
		end

		--call the refresh function
		detailsFramework:Dispatch(self.refresh_func, self, self.data, offset, self.LineAmount)

		--hide all frames that are not in use
		for index, frame in ipairs(self.Frames) do
			--the member _InUse is true when the line is used by the refresh function
			--this member is set to true when the code calls scrollBox:GetLine(index)
			if (not frame._InUse) then
				frame:Hide()
			else
				frame:Show()
			end
		end

		self:Show()

		local frameName = self:GetName()
		if (frameName) then
			if (self.HideScrollBar) then
				local scrollBar = _G[frameName .. "ScrollBar"]
				if (scrollBar) then
					scrollBar:Hide()
				end
			else
				--[=[ --maybe in the future I visit this again
				local scrollBar = _G[frameName .. "ScrollBar"]
				local height = self:GetHeight()
				local totalLinesRequired = #self.data
				local linesShown = self._LinesInUse

				local percent = linesShown / totalLinesRequired
				local thumbHeight = height * percent
				scrollBar.ThumbTexture:SetSize(12, thumbHeight)
				print("thumbHeight:", thumbHeight)
				--]=]
			end
		end
		return self.Frames
	end,

	OnVerticalScroll = function(self, offset)
		self:OnVerticalScrollFaux(offset, self.LineHeight, self.Refresh)
		return true
	end,

	---create a line within the scrollbox
	---@param self table is the scrollbox
	---@param func function|nil function to create the line object, this function will receive the line index as argument and return a table with the line object
	---@return table line object (table)
	CreateLine = function(self, func)
		if (not func) then
			func = self.CreateLineFunc
		end

		local okay, newLine = xpcall(func, geterrorhandler(), self, #self.Frames+1)
		if (okay) then
			if (not newLine) then
				error("ScrollFrame:CreateLine() function did not returned a line, use: 'return line'")
			end
			table.insert(self.Frames, newLine)
			newLine.Index = #self.Frames
			return newLine
		end
		return newLine
	end,

	CreateLines = function(self, callback, lineAmount)
		for i = 1, lineAmount do
			self:CreateLine(callback)
		end
	end,

	GetLine = function(self, lineIndex)
		local line = self.Frames[lineIndex]
		if (line) then
			line._InUse = true
		end

		self._LinesInUse = self._LinesInUse + 1
		return line
	end,

	SetData = function(self, data)
		self.data = data
		if (self.OnSetData) then
			detailsFramework:CoreDispatch((self:GetName() or "ScrollBox") .. ":OnSetData()", self.OnSetData, self, self.data)
		end
	end,

	GetData = function(self)
		return self.data
	end,

	GetFrames = function(self)
		return self.Frames
	end,

	GetLines = function(self) --alias of GetFrames
		return self.Frames
	end,

	GetNumFramesCreated = function(self)
		return #self.Frames
	end,

	GetNumFramesShown = function(self)
		return self.LineAmount
	end,

	SetNumFramesShown = function(self, newAmount)
		--hide frames which won't be used
		if (newAmount < #self.Frames) then
			for i = newAmount+1, #self.Frames do
				self.Frames[i]:Hide()
			end
		end
		--set the new amount
		self.LineAmount = newAmount
	end,

	SetFramesHeight = function(self, height)
		self.LineHeight = height
		self:OnSizeChanged()
		self:Refresh()
	end,

	OnSizeChanged = function(self)
		if (self.ReajustNumFrames) then
			--how many lines the scroll can show
			local amountOfFramesToShow = math.floor(self:GetHeight() / self.LineHeight)

			--how many lines the scroll already have
			local totalFramesCreated = self:GetNumFramesCreated()

			--how many lines are current shown
			local totalFramesShown = self:GetNumFramesShown()

			--the amount of frames increased
			if (amountOfFramesToShow > totalFramesShown) then
				for i = totalFramesShown+1, amountOfFramesToShow do
					--check if need to create a new line
					if (i > totalFramesCreated) then
						self:CreateLine(self.CreateLineFunc)
					end
				end

			--the amount of frames decreased
			elseif (amountOfFramesToShow < totalFramesShown) then
				--hide all frames above the new amount to show
				for i = totalFramesCreated, amountOfFramesToShow, -1 do
					if (self.Frames[i]) then
						self.Frames[i]:Hide()
					end
				end
			end

			--set the new amount of frames
			self:SetNumFramesShown(amountOfFramesToShow)
			--refresh lines
			self:Refresh()
		end
	end,

	--moved functions from blizzard faux scroll that are called from insecure code environment
	--this reduces the amount of taints while using the faux scroll frame
	GetOffsetFaux = function(self)
		return self.offset or 0
	end,

	OnVerticalScrollFaux = function(self, value, lineHeight, updateFunction)
		local scrollbar = self:GetChildFramesFaux()
		scrollbar:SetValue(value)
		self.offset = math.floor((value / lineHeight) + 0.5)

		if (updateFunction) then
			updateFunction(self)
		end
	end,

	GetChildFramesFaux = function(frame)
		local frameName = frame:GetName();
		if frameName then
			return _G[ frameName.."ScrollBar" ], _G[ frameName.."ScrollChildFrame" ], _G[ frameName.."ScrollBarScrollUpButton" ], _G[ frameName.."ScrollBarScrollDownButton" ];
		else
			return frame.ScrollBar, frame.ScrollChildFrame, frame.ScrollBar.ScrollUpButton, frame.ScrollBar.ScrollDownButton;
		end
	end,

	UpdateFaux = function(frame, numItems, numToDisplay, buttonHeight, button, smallWidth, bigWidth, highlightFrame, smallHighlightWidth, bigHighlightWidth, alwaysShowScrollBar)
		local scrollBar, scrollChildFrame, scrollUpButton, scrollDownButton = frame:GetChildFramesFaux();
		-- If more than one screen full of items then show the scrollbar
		local showScrollBar;
		if ( numItems > numToDisplay or alwaysShowScrollBar ) then
			frame:Show();
			showScrollBar = 1;
		else
			scrollBar:SetValue(0);
			frame:Hide();
		end
		if ( frame:IsShown() ) then
			local scrollFrameHeight = 0;
			local scrollChildHeight = 0;

			if ( numItems > 0 ) then
				scrollFrameHeight = (numItems - numToDisplay) * buttonHeight;
				scrollChildHeight = numItems * buttonHeight;
				if ( scrollFrameHeight < 0 ) then
					scrollFrameHeight = 0;
				end
				scrollChildFrame:Show();
			else
				scrollChildFrame:Hide();
			end
			local maxRange = (numItems - numToDisplay) * buttonHeight;
			if (maxRange < 0) then
				maxRange = 0;
			end
			scrollBar:SetMinMaxValues(0, maxRange);
			scrollBar:SetValueStep(buttonHeight);
			scrollBar:SetStepsPerPage(numToDisplay-1);
			scrollChildFrame:SetHeight(scrollChildHeight);

			-- Arrow button handling
			if ( scrollBar:GetValue() == 0 ) then
				scrollUpButton:Disable();
			else
				scrollUpButton:Enable();
			end
			if ((scrollBar:GetValue() - scrollFrameHeight) == 0) then
				scrollDownButton:Disable();
			else
				scrollDownButton:Enable();
			end

			-- Shrink because scrollbar is shown
			if ( highlightFrame ) then
				highlightFrame:SetWidth(smallHighlightWidth);
			end
			if ( button ) then
				for i=1, numToDisplay do
					_G[button..i]:SetWidth(smallWidth);
				end
			end
		else
			-- Widen because scrollbar is hidden
			if ( highlightFrame ) then
				highlightFrame:SetWidth(bigHighlightWidth);
			end
			if ( button ) then
				for i=1, numToDisplay do
					_G[button..i]:SetWidth(bigWidth);
				end
			end
		end

		return showScrollBar;
	end,
}


---@class df_gridscrollbox_options : table
---@field width number?
---@field height number?
---@field line_amount number?
---@field line_height number?
---@field columns_per_line number?
---@field auto_amount boolean?
---@field no_scroll boolean?
---@field vertical_padding number?
---@field no_backdrop boolean?

---@type df_gridscrollbox_options
local grid_scrollbox_options = {
	width = 600,
	height = 400,
	line_amount = 10,
	line_height = 30,
    columns_per_line = 4,
	no_scroll = false,
    vertical_padding = 1,
    no_backdrop = false,
}

---@class df_gridscrollbox : df_scrollbox

---create a scrollbox with a grid layout
---@param parent frame
---@param name string
---@param refreshFunc function
---@param data table
---@param createColumnFrameFunc function
---@param options df_gridscrollbox_options?
---@return unknown
function detailsFramework:CreateGridScrollBox(parent, name, refreshFunc, data, createColumnFrameFunc, options)
    options = options or {}

	--check values passed, get defaults and cast values due to the scrollbox require some values to be numbers
	local width = type(options.width) == "number" and options.width or grid_scrollbox_options.width
	---@cast width number

	local height = type(options.height) == "number" and options.height or grid_scrollbox_options.height
	---@cast height number

	local lineAmount = type(options.line_amount) == "number" and options.line_amount or grid_scrollbox_options.line_amount
	---@cast lineAmount number

	local lineHeight = type(options.line_height) == "number" and options.line_height or grid_scrollbox_options.line_height
	---@cast lineHeight number

    local columnsPerLine = options.columns_per_line or grid_scrollbox_options.columns_per_line
	local autoAmount = options.auto_amount
	local noScroll = options.no_scroll
	local noBackdrop = options.no_backdrop
    local verticalPadding = options.vertical_padding or grid_scrollbox_options.vertical_padding

    local createLineFunc = function(scrollBox, lineIndex)
        local line = CreateFrame("frame", "$parentLine" .. lineIndex, scrollBox)
        line:SetSize(width, lineHeight)
        line:SetPoint("top", scrollBox, "top", 0, -((lineIndex-1) * (lineHeight + verticalPadding)))
        line.optionFrames = {}

        for columnIndex = 1, columnsPerLine do
            --dispatch payload: line, lineIndex, columnIndex
            local optionFrame = createColumnFrameFunc(line, lineIndex, columnIndex)
            line.optionFrames[columnIndex] = optionFrame
            optionFrame:SetPoint("left", line, "left", (columnIndex-1) * (width/columnsPerLine), 0)
        end

        return line
    end

    local onSetData = function(self, data)
        local newData = {}

        for i = 1, #data, columnsPerLine do
            local thisColumnData = {}

            for o = 1, columnsPerLine do
                local index = i + (o-1)
                local thisData = data[index]
                if (thisData) then
                    thisColumnData[#thisColumnData+1] = thisData
                end
            end
            newData[#newData+1] = thisColumnData
        end

        self.data = newData
    end

    local refreshGrid = function(scrollBox, thisData, offset, totalLines)
        for i = 1, totalLines do
            local index = i + offset
            local lineData = thisData[index]

            if (lineData) then
                local line = scrollBox:GetLine(i)
                for o = 1, columnsPerLine do
                    local optionFrame = line.optionFrames[o]
                    local data = lineData[o]
                    if (data) then
                        detailsFramework:Dispatch(refreshFunc, optionFrame, data)
                        optionFrame:Show()
                        line:Show()
                    else
                        optionFrame:Hide()
                    end
                end
            end
        end
    end

    if (not name) then
        name = "DetailsFrameworkAuraScrollBox" .. math.random(1, 9999999)
    end

	local scrollBox = detailsFramework:CreateScrollBox(parent, name, refreshGrid, data, width, height, lineAmount, lineHeight, createLineFunc, autoAmount, noScroll, noBackdrop)
    scrollBox:CreateLines(createLineFunc, lineAmount)
    detailsFramework:ReskinSlider(scrollBox)
    scrollBox.OnSetData = onSetData
    onSetData(scrollBox, data)

	return scrollBox
end

--Need to test this and check the "same_name_spells_add(value)" on the OnEnter function
--also need to make sure this can work with any data (global, class, spec) and aura type (buff, debuff)

--aura scroll box
---@class df_aurascrollbox_options : table
---@field line_height number?
---@field line_amount number?
---@field width number?
---@field height number?
---@field vertical_padding number?
---@field show_spell_tooltip boolean
---@field remove_icon_border boolean
---@field no_scroll boolean
---@field no_backdrop boolean
---@field backdrop_onenter number[]?
---@field backdrop_onleave number[]?
---@field font_size number?
---@field title_text string?

local auraScrollDefaultSettings = {
    line_height = 18,
    line_amount = 18,
	width = 300,
	height = 500,
    vertical_padding = 1,
    show_spell_tooltip = false,
    remove_icon_border = true,
	no_scroll = false,
    no_backdrop = false,
    backdrop_onenter = {.8, .8, .8, 0.4},
    backdrop_onleave = {.8, .8, .8, 0.2},
    font_size = 12,
	title_text = "",
}

---@param parent frame
---@param name string?
---@param data table? --can be set later with :SetData()
---@param onAuraRemoveCallback function?
---@param options df_aurascrollbox_options?
function detailsFramework:CreateAuraScrollBox(parent, name, data, onAuraRemoveCallback, options)
    --hack the construction of the options table here, as the scrollbox is created much later
    options = options or {}
    local scrollOptions = {}
    detailsFramework.OptionsFunctions.BuildOptionsTable(scrollOptions, auraScrollDefaultSettings, options)
    options = scrollOptions.options

    local refreshAuraLines = function(self, data, offset, totalLines)
        for i = 1, totalLines do
            local index = i + offset
            local auraTable = data[index]
            if (auraTable) then
                local line = self:GetLine(i)
                local spellId, spellName, spellIcon, lowerSpellName, bAddedBySpellName = unpack(auraTable)

                line.SpellID = spellId
                line.SpellName = spellName
                line.SpellNameLower = lowerSpellName
                line.SpellIcon = spellIcon
                line.Flag = bAddedBySpellName

                if (bAddedBySpellName) then
                    line.name:SetText(spellName)
                else
                    line.name:SetText(spellName .. " (" .. spellId .. ")")
                end

                line.icon:SetTexture(spellIcon)
                if (options.remove_icon_border) then
                    line.icon:SetTexCoord(.1, .9, .1, .9)
                else
                    line.icon:SetTexCoord(0, 1, 0, 1)
                end
            end
        end
    end

    local onLeaveAuraLine = function(self)
        self:SetBackdropColor(unpack(options.backdrop_onleave))
        GameTooltip:Hide()
        GameCooltip:Hide()
    end

	local onEnterAuraLine = function(line)
        if (options.show_spell_tooltip and line.SpellID and GetSpellInfo(line.SpellID)) then
            GameTooltip:SetOwner(line, "ANCHOR_CURSOR")
            GameTooltip:SetSpellByID(line.SpellID)
            GameTooltip:AddLine(" ")
            GameTooltip:Show()
        end
        line:SetBackdropColor(unpack(options.backdrop_onenter))

		local bTrackByName = line.Flag --the user entered the spell name to track the spell (and not a spellId)
		local spellId = line.SpellID

		if (bTrackByName) then --the user entered the spell name to track the spell
			local spellsHashMap, spellsIndexTable, spellsWithSameName = detailsFramework:GetSpellCaches()
			if (spellsWithSameName) then
				local spellName, _, spellIcon = GetSpellInfo(spellId)
				if (spellName) then
					local spellNameLower = spellName:lower()
					local sameNameSpells = spellsWithSameName[spellNameLower]

					if (sameNameSpells) then
						GameCooltip:Preset(2)
						GameCooltip:SetOwner(line, "left", "right", 2, 0)
						GameCooltip:SetOption("TextSize", 10)

						for i, thisSpellId in ipairs(sameNameSpells) do
							GameCooltip:AddLine(spellName .. " (" .. thisSpellId .. ")")
							GameCooltip:AddIcon(spellIcon, 1, 1, 14, 14, .1, .9, .1, .9)
						end

						GameCooltip:Show()
					end
				end
			end

		else --the user entered the spellId to track the spell
			GameCooltip:Preset(2)
			GameCooltip:SetOwner(line, "left", "right", 2, 0)
			GameCooltip:SetOption("TextSize", 10)

			local spellName, _, spellIcon = GetSpellInfo(spellId)
			if (spellName) then
				GameCooltip:AddLine(spellName .. " (" .. spellId .. ")")
				GameCooltip:AddIcon(spellIcon, 1, 1, 14, 14, .1, .9, .1, .9)
			end
			GameCooltip:Show()
		end
	end

    local onClickAuraRemoveButton = function(self)
        local spellId = tonumber(self:GetParent().SpellID)
        if (spellId and type(spellId) == "number") then
            --button > line > scrollbox
            local scrollBox = self:GetParent():GetParent()
            scrollBox.data_original[spellId] = nil
            scrollBox.data_original["" .. (spellId or "")] = nil -- cleanup...

            scrollBox:TransformAuraData()
            scrollBox:Refresh()

            if (onAuraRemoveCallback) then --upvalue
                detailsFramework:QuickDispatch(onAuraRemoveCallback, spellId)
            end
        end
    end

    local createLineFunc = function(self, index)
        local line = CreateFrame("button", "$parentLine" .. index, self, "BackdropTemplate")
        local scrollBoxWidth = options.width
        local lineHeight = options.line_height
        local verticalPadding = options.vertical_padding

        line:SetPoint("topleft", self, "topleft", 1, -((index-1) * (lineHeight + verticalPadding)) - 1)
        line:SetSize(scrollBoxWidth - 2, lineHeight)
        line:SetScript("OnEnter", onEnterAuraLine)
        line:SetScript("OnLeave", onLeaveAuraLine)

        line:SetBackdrop({bgFile = [[Interface\Tooltips\UI-Tooltip-Background]], tileSize = 64, tile = true})
        line:SetBackdropColor(unpack(options.backdrop_onleave))

        local iconTexture = line:CreateTexture("$parentIcon", "overlay")
        iconTexture:SetSize(lineHeight - 2, lineHeight - 2)

        local spellNameFontString = line:CreateFontString("$parentName", "overlay", "GameFontNormal")
        detailsFramework:SetFontSize(spellNameFontString, options.font_size)

        local removeButton = CreateFrame("button", "$parentRemoveButton", line, "UIPanelCloseButton")
        removeButton:SetSize(16, 16)
        removeButton:SetScript("OnClick", onClickAuraRemoveButton)
        removeButton:SetPoint("topright", line, "topright", 0, 0)
        removeButton:GetNormalTexture():SetDesaturated(true)

        iconTexture:SetPoint("left", line, "left", 2, 0)
        spellNameFontString:SetPoint("left", iconTexture, "right", 3, 0)

        line.icon = iconTexture
        line.name = spellNameFontString
        line.removebutton = removeButton

        return line
    end

    ---@class df_aurascrollbox : df_scrollbox
    ---@field data_original table
    ---@field refresh_original function
	---@field TitleLabel fontstring
    ---@field TransformAuraData fun(self:df_aurascrollbox)
	---@field GetTitleFontString fun(self:df_aurascrollbox): fontstring

    data = data or {}

    if (not name) then
        name = "DetailsFrameworkAuraScrollBox" .. math.random(1, 9999999)
    end

    local auraScrollBox = detailsFramework:CreateScrollBox(parent, name, refreshAuraLines, data, options.width, options.height, options.line_amount, options.line_height)
    detailsFramework:ReskinSlider(auraScrollBox)
    ---@cast auraScrollBox df_aurascrollbox
    auraScrollBox.data_original = data

	local titleLabel = auraScrollBox:CreateFontString("$parentTitleLabel", "overlay", "GameFontNormal")
	titleLabel:SetPoint("bottomleft", auraScrollBox, "topleft", 0, 2)
	detailsFramework:SetFontColor(titleLabel, "silver")
	detailsFramework:SetFontSize(titleLabel, 10)
	auraScrollBox.TitleLabel = titleLabel

	function auraScrollBox:GetTitleFontString()
		return self.TitleLabel
	end

	for i = 1, options.line_amount do
		auraScrollBox:CreateLine(createLineFunc)
	end

    function auraScrollBox:TransformAuraData()
        local newData = {}
        local added = {}

        for spellId, bAddedBySpellName in pairs(self.data_original) do
            local spellName, _, spellIcon = GetSpellInfo(spellId)
            if (spellName and not added[tonumber(spellId) or 0]) then
                local lowerSpellName = spellName:lower()
                table.insert(newData, {spellId, spellName, spellIcon, lowerSpellName, bAddedBySpellName})
                added[tonumber(spellId) or 0] = true
            end
        end

        table.sort(newData, function(t1, t2) return t1[4] < t2[4] end)
        self.data = newData
    end

    auraScrollBox.SetData = function(self, data)
        self.data_original = data
        self.data = data
        auraScrollBox:TransformAuraData()
    end

    auraScrollBox.GetData = function(self)
        return self.data_original
    end

	auraScrollBox.refresh_original = auraScrollBox.Refresh

	auraScrollBox.Refresh = function()
		auraScrollBox:TransformAuraData()
		auraScrollBox:refresh_original()
	end

    auraScrollBox:SetData(data)

    return auraScrollBox
end


detailsFramework.CanvasScrollBoxMixin = {

}

local canvasScrollBoxDefaultOptions = {
	width = 600,
	height = 400,
	reskin_slider = true,
}

---@class df_canvasscrollbox : scrollframe, df_optionsmixin
---@field child frame

---@param parent frame
---@param child frame?
---@param name string?
---@param options table?
---@return df_canvasscrollbox
function detailsFramework:CreateCanvasScrollBox(parent, child, name, options)
	---@type df_canvasscrollbox
	local canvasScrollBox = CreateFrame("scrollframe", name or ("DetailsFrameworkCanvasScroll" .. math.random(50000, 10000000)), parent, "BackdropTemplate, UIPanelScrollFrameTemplate")

	detailsFramework:Mixin(canvasScrollBox, detailsFramework.CanvasScrollBoxMixin)
	detailsFramework:Mixin(canvasScrollBox, detailsFramework.OptionsFunctions)

    options = options or {}
    canvasScrollBox:BuildOptionsTable(canvasScrollBoxDefaultOptions, options)

	canvasScrollBox:SetSize(canvasScrollBox.options.width, canvasScrollBox.options.height)

	if (not child) then
		child = CreateFrame("frame", "$parentChild", canvasScrollBox)
	end

	canvasScrollBox:SetScrollChild(child)
	canvasScrollBox:EnableMouseWheel(true)

	canvasScrollBox.child = child

	if (canvasScrollBox.options.reskin_slider) then
		detailsFramework:ReskinSlider(canvasScrollBox)
	end

	return canvasScrollBox
end
