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

local CreateFrame = CreateFrame
local GetSpellInfo = GetSpellInfo or function(spellID) if not spellID then return nil end local si = C_Spell.GetSpellInfo(spellID) if si then return si.name, nil, si.iconID, si.castTime, si.minRange, si.maxRange, si.spellID, si.originalIconID end end
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
		--self.Frames has a list of frames used by the scrollbox
		for index, frame in ipairs(self.Frames) do
			if (not self.DontHideChildrenOnPreRefresh) then
				frame:Hide()
			end
			--set the frame as not in use
			frame._InUse = nil
		end

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

		--before starting the refresh, check if there's a pre refresh function and call it
		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,

	---@param self df_scrollbox
	---@param offset number
	---@return boolean
	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,

	---Creates multiple lines in the scroll box.
	---@param self df_scrollbox The DF_ScrollBox object.
	---@param callback function The callback function to be called for each line.
	---@param lineAmount number The number of lines to create.
	CreateLines = function(self, callback, lineAmount)
		for i = 1, lineAmount do
			self:CreateLine(callback)
		end
	end,

	---Retrieves a specific line from the scroll box.
	---@param self df_scrollbox The DF_ScrollBox object.
	---@param lineIndex number The index of the line to retrieve.
	---@return frame line The line object at the specified index.
	GetLine = function(self, lineIndex)
		local line = self.Frames[lineIndex]
		--print(self, line, line and line:GetName(), lineIndex, self:GetName())
		if (line) then
			line._InUse = true
		end

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

	---Sets the data for the scroll box.
	---@param data table The data to be set.
	SetData = function(self, data)
		self.data = data
		self.data_original = data
		if (self.OnSetData) then
			detailsFramework:CoreDispatch((self:GetName() or "ScrollBox") .. ":OnSetData()", self.OnSetData, self, self.data)
		end
	end,

	---Retrieves the data associated with the scrollbox.
	---@param self df_scrollbox
	---@return table The data associated with the scrollbox.
	GetData = function(self)
		return self.data
	end,

	---Retrieves the frames contained within the scrollbox.
	---@param self df_scrollbox
	---@return table The frames contained within the scrollbox.
	GetFrames = function(self)
		return self.Frames
	end,

	---Retrieves the lines contained within the scrollbox.
	---This is an alias of GetFrames.
	---@param self df_scrollbox
	---@return table The lines contained within the scrollbox.
	GetLines = function(self)
		return self.Frames
	end,

	---Retrieves the number of frames created within the scrollbox.
	---@param self df_scrollbox
	---@return number The number of frames created within the scrollbox.
	GetNumFramesCreated = function(self)
		return #self.Frames
	end,

	---get the amount of lines the scroll is currently showing
	---@param self df_scrollbox
	---@return number amountOfLines
	GetNumFramesShown = function(self)
		return self.LineAmount
	end,

	---set the max amount of lines the scroll can show
	---@param self df_scrollbox
	---@param newAmount number
	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 horizontal_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,
    horizontal_padding = 1,
    no_backdrop = false,
}

---@class df_gridscrollbox : df_scrollbox
---@field RefreshMe fun(self:df_gridscrollbox)

---create a scrollbox with a grid layout
---@param parent frame
---@param name string
---@param refreshFunc fun(button:frame, data:table)
---@param data table
---@param createColumnFrameFunc fun(line:frame, lineIndex:number, columnIndex:number)
---@param options df_gridscrollbox_options?
---@return df_gridscrollbox
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 horizontalPadding = options.horizontal_padding or grid_scrollbox_options.horizontal_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
			if (columnIndex == 1) then
            	optionFrame:SetPoint("left", line, "left", (columnIndex-1) * (width/columnsPerLine), 0)
			else
				optionFrame:SetPoint("left", line, "left", (columnIndex-1) * (width/columnsPerLine) + (horizontalPadding * (columnIndex-1)), 0)
			end
        end

        return line
    end

    local onSetData = function(self, data)
		self.data_original = 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)

	---@cast scrollBox df_gridscrollbox
	return scrollBox
end

---@class df_gridscrollbox_menu : df_gridscrollbox
---@field data_original table the data passed into :SetData()
---@field searchBox df_searchbox
---@field Select fun(self:df_gridscrollbox_menu, value:any, key:string) --select a line by a value on a key, example: :Select("Power Infusion", "spellName")

---create a scrollbox with a grid layout to be used as a menu
---@param parent frame
---@param name string?
---@param refreshMeFunc fun(gridScrollBox:df_gridscrollbox, searchText:string)
---@param refreshButtonFunc fun(button:button, data:table)
---@param clickFunc fun(button:button, data:table)
---@param onCreateButton fun(button:button, lineIndex:number, columnIndex:number)
---@param gridScrollBoxOptions df_gridscrollbox_options
---@return df_gridscrollbox_menu
function detailsFramework:CreateMenuWithGridScrollBox(parent, name, refreshMeFunc, refreshButtonFunc, clickFunc, onCreateButton, gridScrollBoxOptions)
	local dataSelected = nil
	local gridScrollBox

	local onClickButtonSelectorButton = function(blizzButton, buttonDown, dfButton, data)
		dataSelected = data
		gridScrollBox:Refresh()
		xpcall(clickFunc, geterrorhandler(), dfButton, data)
	end

    --create a search bar to filter the auras
    local searchText = ""
    local onSearchTextChangedCallback = function(self, ...)
        local text = self:GetText()
        searchText = string.lower(text)
        dataSelected = nil
        gridScrollBox:RefreshMe()
    end

	local searchBox = detailsFramework:CreateSearchBox(parent, onSearchTextChangedCallback)

	---when the scroll is refreshing the line, the line will call this function for each selection button on it
    ---@param button df_button
    ---@param data table
    local refreshLine = function(button, data)
        button.data = data

		if (data.tooltip) then
			button.tooltip = data.tooltip
		end

        --set what happen when the user clicks the button
        button:SetClickFunction(onClickButtonSelectorButton, button, data)

		if (button.data == dataSelected) then
			button.widget:SetBorderCornerColor(.9, .9, .9)
		else
			button.widget:SetBorderCornerColor(unpack(gridScrollBoxOptions.roundedFramePreset.border_color))
		end

		xpcall(refreshButtonFunc, geterrorhandler(), button, data)
    end

	--create a line
    local createButton = function(line, lineIndex, columnIndex)
        local width = gridScrollBoxOptions.width / gridScrollBoxOptions.columns_per_line - 5
        local height = gridScrollBoxOptions.line_height
        if (not height) then
            height = 30
        end

        local button = detailsFramework:CreateButton(line, onClickButtonSelectorButton, width, height)
        detailsFramework:AddRoundedCornersToFrame(button.widget, gridScrollBoxOptions.roundedFramePreset)
        button.textsize = 11

		line.button = button

        button:SetHook("OnEnter", function(self)
            local dfButton = self:GetObject()
            GameCooltip:Reset()
			if (dfButton.spellId) then
            	GameCooltip:SetSpellByID(dfButton.spellId)
				GameCooltip:SetOwner(self)
				GameCooltip:Show()
			end
            self:SetBorderCornerColor(.9, .9, .9)
        end)

        button:SetHook("OnLeave", function(self)
            GameCooltip:Hide()
            local dfButton = self:GetObject()
			if (dfButton.data == dataSelected) then
				self:SetBorderCornerColor(.9, .9, .9)
			else
            	self:SetBorderCornerColor(unpack(gridScrollBoxOptions.roundedFramePreset.border_color))
			end
        end)

		xpcall(onCreateButton, geterrorhandler(), button, lineIndex, columnIndex)

        return button
    end

    gridScrollBox = detailsFramework:CreateGridScrollBox(parent, name, refreshLine, {}, createButton, gridScrollBoxOptions)
	---@cast gridScrollBox df_gridscrollbox_menu

    gridScrollBox:SetBackdrop({})
    gridScrollBox:SetBackdropColor(0, 0, 0, 0)
    gridScrollBox:SetBackdropBorderColor(0, 0, 0, 0)
    gridScrollBox.__background:Hide()
    gridScrollBox:Show()

	gridScrollBox.searchBox = searchBox
	searchBox:SetPoint("bottomleft", gridScrollBox, "topleft", 0, 2)
	searchBox:SetWidth(gridScrollBoxOptions.width)

	function gridScrollBox:Select(value, key)
		local bFoundResult = false
		local originalData

		for _, data in ipairs(gridScrollBox.data_original) do
			originalData = data

			if (type(value) == string) then
				value = value:lower()
				local dataValue = data[key]:lower()
				if (dataValue == value) then
					dataSelected = originalData
					bFoundResult = true
					break
				end
			else
				if (data[key] == value) then
					dataSelected = originalData
					bFoundResult = true
					break
				end
			end
		end

		if (bFoundResult) then
			for _, line in ipairs(gridScrollBox:GetFrames()) do
				local button = line.button
				if (button.data == originalData) then
					gridScrollBox:Refresh()
					onClickButtonSelectorButton(nil, nil, button, originalData)
					break
				end
			end
		end
	end

	function gridScrollBox:RefreshMe()
		xpcall(refreshMeFunc, geterrorhandler(), gridScrollBox, searchBox:GetText())
	end

	return gridScrollBox
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?
---@param onSetupAuraClick function?
function detailsFramework:CreateAuraScrollBox(parent, name, data, onAuraRemoveCallback, options, onSetupAuraClick)
    --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

		if (line.setupbutton:IsShown()) then
			
		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)

		local setupAuraButton = CreateFrame("button", "$parentSetupButton", line)
		setupAuraButton:SetSize(16, 16)
		setupAuraButton:SetPoint("right", removeButton, "left", -4, 0)
		setupAuraButton:SetScript("OnClick", onSetupAuraClick)

		line:SetScript("OnMouseUp", function(self, button)
			if (onSetupAuraClick) then
				setupAuraButton:Click()
			end
		end)

		local clickToSetupText = setupAuraButton:CreateFontString("$parentText", "overlay", "GameFontNormal")
		clickToSetupText:SetText("click to setup")
		clickToSetupText:SetPoint("right", setupAuraButton, "left", -2, 0)
		detailsFramework:SetFontSize(clickToSetupText, 9)

		local setupAuraTexture = setupAuraButton:CreateTexture(nil, "overlay")
		setupAuraTexture:SetAllPoints()
		setupAuraTexture:SetTexture([[Interface\ICONS\INV_Misc_Wrench_01.blp]])
		setupAuraTexture:SetTexCoord(0.1, 0.9, 0.1, 0.9)

		setupAuraButton.Texture = setupAuraTexture
		setupAuraButton.Text = clickToSetupText

		if (not onSetupAuraClick) then
			setupAuraButton:Hide()
		end

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

        line.icon = iconTexture
        line.name = spellNameFontString
        line.removebutton = removeButton
		line.setupbutton = setupAuraButton
		line.clicktosetuptext = clickToSetupText

        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 = {
	SetScrollSpeed = function(self, speed)
		assert(type(speed) == "number", "CanvasScrollBox:SetScrollSpeed(speed): speed must be a number.")
		self.scrollStep = speed
	end,

	GetScrollSpeed = function(self)
		return self.scrollStep
	end,

	OnVerticalScroll = function(self, delta)
		local scrollStep = self:GetScrollSpeed()
		if (delta > 0) then
			self:SetVerticalScroll(math.max(self:GetVerticalScroll() - scrollStep, 0))
		else
			self:SetVerticalScroll(math.min(self:GetVerticalScroll() + scrollStep, self:GetVerticalScrollRange()))
		end
	end,
}

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")
	canvasScrollBox.scrollStep = 20
	canvasScrollBox.minValue = 0

	canvasScrollBox:SetScript("OnMouseWheel", detailsFramework.CanvasScrollBoxMixin.OnVerticalScroll)

	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