local MAJOR = "LibQTip-1.0RS"
local MINOR = 49 -- Should be manually increased
local LibStub = _G.LibStub

assert(LibStub, MAJOR .. " requires LibStub")

local lib, oldMinor = LibStub:NewLibrary(MAJOR, MINOR)

if not lib then
    return
end -- No upgrade needed

------------------------------------------------------------------------------
-- Upvalued globals
------------------------------------------------------------------------------
local table = _G.table
local tinsert = table.insert
local tremove = table.remove
local wipe = table.wipe

local error = error
local math = math
local min, max = math.min, math.max
local next = next
local pairs, ipairs = pairs, ipairs
local select = select
local setmetatable = setmetatable
local tonumber, tostring = tonumber, tostring
local type = type

local CreateFrame = _G.CreateFrame
local GameTooltip = _G.GameTooltip
local UIParent = _G.UIParent

local geterrorhandler = _G.geterrorhandler

------------------------------------------------------------------------------
-- Tables and locals
------------------------------------------------------------------------------
lib.frameMetatable = lib.frameMetatable or {__index = CreateFrame("Frame")}

lib.tipPrototype = lib.tipPrototype or setmetatable({}, lib.frameMetatable)
lib.tipMetatable = lib.tipMetatable or {__index = lib.tipPrototype}

lib.providerPrototype = lib.providerPrototype or {}
lib.providerMetatable = lib.providerMetatable or {__index = lib.providerPrototype}

lib.cellPrototype = lib.cellPrototype or setmetatable({}, lib.frameMetatable)
lib.cellMetatable = lib.cellMetatable or {__index = lib.cellPrototype}

lib.activeTooltips = lib.activeTooltips or {}

lib.tooltipHeap = lib.tooltipHeap or {}
lib.frameHeap = lib.frameHeap or {}
lib.timerHeap = lib.timerHeap or {}
lib.tableHeap = lib.tableHeap or {}

lib.onReleaseHandlers = lib.onReleaseHandlers or {}

local tipPrototype = lib.tipPrototype
local tipMetatable = lib.tipMetatable

local providerPrototype = lib.providerPrototype
local providerMetatable = lib.providerMetatable

local cellPrototype = lib.cellPrototype
local cellMetatable = lib.cellMetatable

local activeTooltips = lib.activeTooltips

local highlightFrame = CreateFrame("Frame", nil, UIParent)
highlightFrame:SetFrameStrata("TOOLTIP")
highlightFrame:Hide()

local DEFAULT_HIGHLIGHT_TEXTURE_PATH = [[Interface\QuestFrame\UI-QuestTitleHighlight]]

local highlightTexture = highlightFrame:CreateTexture(nil, "OVERLAY")
highlightTexture:SetTexture(DEFAULT_HIGHLIGHT_TEXTURE_PATH)
highlightTexture:SetBlendMode("ADD")
highlightTexture:SetAllPoints(highlightFrame)

------------------------------------------------------------------------------
-- Private methods for Caches and Tooltip
------------------------------------------------------------------------------
local AcquireTooltip, ReleaseTooltip
local AcquireCell, ReleaseCell
local AcquireTable, ReleaseTable

local InitializeTooltip, SetTooltipSize, ResetTooltipSize, FixCellSizes
local ClearTooltipScripts
local SetFrameScript, ClearFrameScripts

------------------------------------------------------------------------------
-- Cache debugging.
------------------------------------------------------------------------------
-- @debug @
local usedTables, usedFrames, usedTooltips = 0, 0, 0
--@end-debug@

------------------------------------------------------------------------------
-- Internal constants to tweak the layout
------------------------------------------------------------------------------
local TOOLTIP_PADDING = 10
local CELL_MARGIN_H = 6
local CELL_MARGIN_V = 3

------------------------------------------------------------------------------
-- Public library API
------------------------------------------------------------------------------
--- Create or retrieve the tooltip with the given key.
-- If additional arguments are passed, they are passed to :SetColumnLayout for the acquired tooltip.
-- @name LibQTip:Acquire(key[, numColumns, column1Justification, column2justification, ...])
-- @param key string or table - the tooltip key. Any value that can be used as a table key is accepted though you should try to provide unique keys to avoid conflicts.
-- Numbers and booleans should be avoided and strings should be carefully chosen to avoid namespace clashes - no "MyTooltip" - you have been warned!
-- @return tooltip Frame object - the acquired tooltip.
-- @usage Acquire a tooltip with at least 5 columns, justification : left, center, left, left, left
-- <pre>local tip = LibStub('LibQTip-1.0'):Acquire('MyFooBarTooltip', 5, "LEFT", "CENTER")</pre>
function lib:Acquire(key, ...)
    if key == nil then
        error("attempt to use a nil key", 2)
    end

    local tooltip = activeTooltips[key]

    if not tooltip then
        tooltip = AcquireTooltip()
        InitializeTooltip(tooltip, key)
        activeTooltips[key] = tooltip
    end

    if select("#", ...) > 0 then
        -- Here we catch any error to properly report it for the calling code
        local ok, msg = pcall(tooltip.SetColumnLayout, tooltip, ...)

        if not ok then
            error(msg, 2)
        end
    end

    return tooltip
end

function lib:Release(tooltip)
    local key = tooltip and tooltip.key

    if not key or activeTooltips[key] ~= tooltip then
        return
    end

    ReleaseTooltip(tooltip)
    activeTooltips[key] = nil
end

function lib:IsAcquired(key)
    if key == nil then
        error("attempt to use a nil key", 2)
    end

    return not (not activeTooltips[key])
end

function lib:IterateTooltips()
    return pairs(activeTooltips)
end

------------------------------------------------------------------------------
-- Frame cache (for lines and columns)
------------------------------------------------------------------------------
local frameHeap = lib.frameHeap

local function AcquireFrame(parent)
    local frame = tremove(frameHeap) or CreateFrame("Frame", nil, nil, BackdropTemplateMixin and "BackdropTemplate")
    frame:SetParent(parent)
    --[===[@debug@
    usedFrames = usedFrames + 1
    --@end-debug@]===]
    return frame
end

local function ReleaseFrame(frame)
    frame:Hide()
    frame:SetParent(nil)
    frame:ClearAllPoints()
    frame:SetBackdrop(nil)

    ClearFrameScripts(frame)

    tinsert(frameHeap, frame)
    --[===[@debug@
    usedFrames = usedFrames - 1
    --@end-debug@]===]
end

------------------------------------------------------------------------------
-- Timer cache
------------------------------------------------------------------------------
local timerHeap = lib.timerHeap

local function AcquireTimer(parent)
    local frame = tremove(timerHeap) or CreateFrame("Frame")
    frame:SetParent(parent)
    return frame
end

local function ReleaseTimer(frame)
    frame:Hide()
    frame:SetParent(nil)

    ClearFrameScripts(frame)

    tinsert(timerHeap, frame)
end

------------------------------------------------------------------------------
-- Dirty layout handler
------------------------------------------------------------------------------
lib.layoutCleaner = lib.layoutCleaner or CreateFrame("Frame")

local layoutCleaner = lib.layoutCleaner
layoutCleaner.registry = layoutCleaner.registry or {}

function layoutCleaner:RegisterForCleanup(tooltip)
    self.registry[tooltip] = true
    self:Show()
end

function layoutCleaner:CleanupLayouts()
    self:Hide()

    for tooltip in pairs(self.registry) do
        FixCellSizes(tooltip)
    end

    wipe(self.registry)
end

layoutCleaner:SetScript("OnUpdate", layoutCleaner.CleanupLayouts)

------------------------------------------------------------------------------
-- CellProvider and Cell
------------------------------------------------------------------------------
function providerPrototype:AcquireCell()
    local cell = tremove(self.heap)

    if not cell then
        cell = CreateFrame("Frame", nil, UIParent, BackdropTemplateMixin and "BackdropTemplate")
        setmetatable(cell, self.cellMetatable)

        if type(cell.InitializeCell) == "function" then
            cell:InitializeCell()
        end
    end

    self.cells[cell] = true

    return cell
end

function providerPrototype:ReleaseCell(cell)
    if not self.cells[cell] then
        return
    end

    if type(cell.ReleaseCell) == "function" then
        cell:ReleaseCell()
    end

    self.cells[cell] = nil
    tinsert(self.heap, cell)
end

function providerPrototype:GetCellPrototype()
    return self.cellPrototype, self.cellMetatable
end

function providerPrototype:IterateCells()
    return pairs(self.cells)
end

function lib:CreateCellProvider(baseProvider)
    local cellBaseMetatable, cellBasePrototype

    if baseProvider and baseProvider.GetCellPrototype then
        cellBasePrototype, cellBaseMetatable = baseProvider:GetCellPrototype()
    else
        cellBaseMetatable = cellMetatable
    end

    local newCellPrototype = setmetatable({}, cellBaseMetatable)
    local newCellProvider = setmetatable({}, providerMetatable)

    newCellProvider.heap = {}
    newCellProvider.cells = {}
    newCellProvider.cellPrototype = newCellPrototype
    newCellProvider.cellMetatable = {__index = newCellPrototype}

    return newCellProvider, newCellPrototype, cellBasePrototype
end

------------------------------------------------------------------------------
-- Basic label provider
------------------------------------------------------------------------------
if not lib.LabelProvider then
    lib.LabelProvider, lib.LabelPrototype = lib:CreateCellProvider()
end

local labelProvider = lib.LabelProvider
local labelPrototype = lib.LabelPrototype

function labelPrototype:InitializeCell()
    self.fontString = self:CreateFontString()
    self.fontString:SetFontObject(_G.GameTooltipText)
end

function labelPrototype:SetupCell(tooltip, value, justification, font, leftPadding, rightPadding, maxWidth, minWidth, ...)
    local fontString = self.fontString
    local line = tooltip.lines[self._line]

    -- detatch fs from cell for size calculations
    fontString:ClearAllPoints()
    fontString:SetFontObject(font or (line.is_header and tooltip:GetHeaderFont() or tooltip:GetFont()))
    fontString:SetJustifyH(justification)
    fontString:SetText(tostring(value))

    leftPadding = leftPadding or 0
    rightPadding = rightPadding or 0

    local width = fontString:GetStringWidth() + leftPadding + rightPadding

    if maxWidth and minWidth and (maxWidth < minWidth) then
        error("maximum width cannot be lower than minimum width: " .. tostring(maxWidth) .. " < " .. tostring(minWidth), 2)
    end

    if maxWidth and (maxWidth < (leftPadding + rightPadding)) then
        error("maximum width cannot be lower than the sum of paddings: " .. tostring(maxWidth) .. " < " .. tostring(leftPadding) .. " + " .. tostring(rightPadding), 2)
    end

    if minWidth and width < minWidth then
        width = minWidth
    end

    if maxWidth and maxWidth < width then
        width = maxWidth
    end

    fontString:SetWidth(width - (leftPadding + rightPadding))
    -- Use GetHeight() instead of GetStringHeight() so lines which are longer than width will wrap.
    local height = fontString:GetHeight()

    -- reanchor fs to cell
    fontString:SetWidth(0)
    fontString:SetPoint("TOPLEFT", self, "TOPLEFT", leftPadding, 0)
    fontString:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -rightPadding, 0)
    --~ 	fs:SetPoint("TOPRIGHT", self, "TOPRIGHT", -r_pad, 0)

    self._paddingL = leftPadding
    self._paddingR = rightPadding

    return width, height
end

function labelPrototype:getContentHeight()
    local fontString = self.fontString
    fontString:SetWidth(self:GetWidth() - (self._paddingL + self._paddingR))

    local height = self.fontString:GetHeight()
    fontString:SetWidth(0)

    return height
end

function labelPrototype:GetPosition()
    return self._line, self._column
end

------------------------------------------------------------------------------
-- Tooltip cache
------------------------------------------------------------------------------
local tooltipHeap = lib.tooltipHeap

-- Returns a tooltip
function AcquireTooltip()
    local tooltip = tremove(tooltipHeap)

    if not tooltip then
        local template = (TooltipBackdropTemplateMixin and "TooltipBackdropTemplate") or (BackdropTemplateMixin and "BackdropTemplate")
        tooltip = CreateFrame("Frame", nil, UIParent, template)

        local scrollFrame = CreateFrame("ScrollFrame", nil, tooltip)
        scrollFrame:SetPoint("TOP", tooltip, "TOP", 0, -TOOLTIP_PADDING)
        scrollFrame:SetPoint("BOTTOM", tooltip, "BOTTOM", 0, TOOLTIP_PADDING)
        scrollFrame:SetPoint("LEFT", tooltip, "LEFT", TOOLTIP_PADDING, 0)
        scrollFrame:SetPoint("RIGHT", tooltip, "RIGHT", -TOOLTIP_PADDING, 0)
        tooltip.scrollFrame = scrollFrame

        local scrollChild = CreateFrame("Frame", nil, tooltip.scrollFrame)
        scrollFrame:SetScrollChild(scrollChild)
        tooltip.scrollChild = scrollChild

        setmetatable(tooltip, tipMetatable)
    end

    --[===[@debug@
    usedTooltips = usedTooltips + 1
    --@end-debug@]===]
    return tooltip
end

-- Cleans the tooltip and stores it in the cache
function ReleaseTooltip(tooltip)
    if tooltip.releasing then
        return
    end

    tooltip.releasing = true
    tooltip:Hide()

    local releaseHandler = lib.onReleaseHandlers[tooltip]

    if releaseHandler then
        lib.onReleaseHandlers[tooltip] = nil

        local success, errorMessage = pcall(releaseHandler, tooltip)

        if not success then
            geterrorhandler()(errorMessage)
        end
    elseif tooltip.OnRelease then
        local success, errorMessage = pcall(tooltip.OnRelease, tooltip)
        if not success then
            geterrorhandler()(errorMessage)
        end

        tooltip.OnRelease = nil
    end

    tooltip.releasing = nil
    tooltip.key = nil
    tooltip.step = nil

    ClearTooltipScripts(tooltip)

    tooltip:SetAutoHideDelay(nil)
    tooltip:ClearAllPoints()
    tooltip:Clear()

    if tooltip.slider then
        tooltip.slider:SetValue(0)
        tooltip.slider:Hide()
        tooltip.scrollFrame:SetPoint("RIGHT", tooltip, "RIGHT", -TOOLTIP_PADDING, 0)
        tooltip:EnableMouseWheel(false)
    end

    for i, column in ipairs(tooltip.columns) do
        tooltip.columns[i] = ReleaseFrame(column)
    end

    tooltip.columns = ReleaseTable(tooltip.columns)
    tooltip.lines = ReleaseTable(tooltip.lines)
    tooltip.colspans = ReleaseTable(tooltip.colspans)

    layoutCleaner.registry[tooltip] = nil

    if TooltipBackdropTemplateMixin and not tooltip.NineSlice then
        -- don't recycle outdated tooltips into heap
        tooltip = nil
    end

    if tooltip then
        tinsert(tooltipHeap, tooltip)
    end

    highlightTexture:SetTexture(DEFAULT_HIGHLIGHT_TEXTURE_PATH)
    highlightTexture:SetTexCoord(0, 1, 0, 1)

    --[===[@debug@
    usedTooltips = usedTooltips - 1
    --@end-debug@]===]
end

------------------------------------------------------------------------------
-- Cell 'cache' (just a wrapper to the provider's cache)
------------------------------------------------------------------------------
-- Returns a cell for the given tooltip from the given provider
function AcquireCell(tooltip, provider)
    local cell = provider:AcquireCell(tooltip)

    cell:SetParent(tooltip.scrollChild)
    cell:SetFrameLevel(tooltip.scrollChild:GetFrameLevel() + 3)
    cell._provider = provider

    return cell
end

-- Cleans the cell hands it to its provider for storing
function ReleaseCell(cell)
    if cell.fontString and cell.r then
        cell.fontString:SetTextColor(cell.r, cell.g, cell.b, cell.a)
    end

    cell._font = nil
    cell._justification = nil
    cell._colSpan = nil
    cell._line = nil
    cell._column = nil

    cell:Hide()
    cell:ClearAllPoints()
    cell:SetParent(nil)
    cell:SetBackdrop(nil)

    ClearFrameScripts(cell)

    cell._provider:ReleaseCell(cell)
    cell._provider = nil
end

------------------------------------------------------------------------------
-- Table cache
------------------------------------------------------------------------------
local tableHeap = lib.tableHeap

-- Returns a table
function AcquireTable()
    local tbl = tremove(tableHeap) or {}
    --[===[@debug@
    usedTables = usedTables + 1
    --@end-debug@]===]
    return tbl
end

-- Cleans the table and stores it in the cache
function ReleaseTable(tableInstance)
    wipe(tableInstance)
    tinsert(tableHeap, tableInstance)
    --[===[@debug@
    usedTables = usedTables - 1
    --@end-debug@]===]
end

------------------------------------------------------------------------------
-- Tooltip prototype
------------------------------------------------------------------------------
function InitializeTooltip(tooltip, key)
    ----------------------------------------------------------------------
    -- (Re)set frame settings
    ----------------------------------------------------------------------
    if TooltipBackdropTemplateMixin then
        tooltip.layoutType = GameTooltip.layoutType
        NineSlicePanelMixin.OnLoad(tooltip.NineSlice)
        if GameTooltip.layoutType then
            tooltip.NineSlice:SetCenterColor(GameTooltip.NineSlice:GetCenterColor())
            tooltip.NineSlice:SetBorderColor(GameTooltip.NineSlice:GetBorderColor())
        end
    else
        local backdrop = GameTooltip:GetBackdrop()

        tooltip:SetBackdrop(backdrop)

        if backdrop then
            tooltip:SetBackdropColor(GameTooltip:GetBackdropColor())
            tooltip:SetBackdropBorderColor(GameTooltip:GetBackdropBorderColor())
        end
    end

    tooltip:SetScale(GameTooltip:GetScale())
    tooltip:SetAlpha(1)
    tooltip:SetFrameStrata("TOOLTIP")
    tooltip:SetClampedToScreen(false)

    ----------------------------------------------------------------------
    -- Internal data. Since it's possible to Acquire twice without calling
    -- release, check for pre-existence.
    ----------------------------------------------------------------------
    tooltip.key = key
    tooltip.columns = tooltip.columns or AcquireTable()
    tooltip.lines = tooltip.lines or AcquireTable()
    tooltip.colspans = tooltip.colspans or AcquireTable()
    tooltip.regularFont = _G.GameTooltipText
    tooltip.headerFont = _G.GameTooltipHeaderText
    tooltip.labelProvider = labelProvider
    tooltip.cell_margin_h = tooltip.cell_margin_h or CELL_MARGIN_H
    tooltip.cell_margin_v = tooltip.cell_margin_v or CELL_MARGIN_V

    ----------------------------------------------------------------------
    -- Finishing procedures
    ----------------------------------------------------------------------
    tooltip:SetAutoHideDelay(nil)
    tooltip:Hide()
    ResetTooltipSize(tooltip)
end

function tipPrototype:SetDefaultProvider(myProvider)
    if not myProvider then
        return
    end

    self.labelProvider = myProvider
end

function tipPrototype:GetDefaultProvider()
    return self.labelProvider
end

local function checkJustification(justification, level, silent)
    if justification ~= "LEFT" and justification ~= "CENTER" and justification ~= "RIGHT" then
        if silent then
            return false
        end
        error("invalid justification, must one of LEFT, CENTER or RIGHT, not: " .. tostring(justification), level + 1)
    end

    return true
end

function tipPrototype:SetColumnLayout(numColumns, ...)
    if type(numColumns) ~= "number" or numColumns < 1 then
        error("number of columns must be a positive number, not: " .. tostring(numColumns), 2)
    end

    for i = 1, numColumns do
        local justification = select(i, ...) or "LEFT"

        checkJustification(justification, 2)

        if self.columns[i] then
            self.columns[i].justification = justification
        else
            self:AddColumn(justification)
        end
    end
end

function tipPrototype:AddColumn(justification)
    justification = justification or "LEFT"
    checkJustification(justification, 2)

    local colNum = #self.columns + 1
    local column = self.columns[colNum] or AcquireFrame(self.scrollChild)

    column:SetFrameLevel(self.scrollChild:GetFrameLevel() + 1)
    column.justification = justification
    column.width = 0
    column:SetWidth(1)
    column:SetPoint("TOP", self.scrollChild)
    column:SetPoint("BOTTOM", self.scrollChild)

    if colNum > 1 then
        local h_margin = self.cell_margin_h or CELL_MARGIN_H

        column:SetPoint("LEFT", self.columns[colNum - 1], "RIGHT", h_margin, 0)
        SetTooltipSize(self, self.width + h_margin, self.height)
    else
        column:SetPoint("LEFT", self.scrollChild)
    end

    column:Show()
    self.columns[colNum] = column

    return colNum
end

------------------------------------------------------------------------------
-- Convenient methods
------------------------------------------------------------------------------
function tipPrototype:Release()
    lib:Release(self)
end

function tipPrototype:IsAcquiredBy(key)
    return key ~= nil and self.key == key
end

------------------------------------------------------------------------------
-- Script hooks
------------------------------------------------------------------------------
local RawSetScript = lib.frameMetatable.__index.SetScript

function ClearTooltipScripts(tooltip)
    if tooltip.scripts then
        for scriptType in pairs(tooltip.scripts) do
            RawSetScript(tooltip, scriptType, nil)
        end

        tooltip.scripts = ReleaseTable(tooltip.scripts)
    end
end

function tipPrototype:SetScript(scriptType, handler)
    RawSetScript(self, scriptType, handler)

    if handler then
        if not self.scripts then
            self.scripts = AcquireTable()
        end

        self.scripts[scriptType] = true
    elseif self.scripts then
        self.scripts[scriptType] = nil
    end
end

-- That might break some addons ; those addons were breaking other
-- addons' tooltip though.
function tipPrototype:HookScript()
    geterrorhandler()(":HookScript is not allowed on LibQTip tooltips")
end

------------------------------------------------------------------------------
-- Scrollbar data and functions
------------------------------------------------------------------------------
local BACKDROP_SLIDER_8_8 = BACKDROP_SLIDER_8_8 or {
    bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
    edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
    tile = true,
    tileEdge = true,
    tileSize = 8,
    edgeSize = 8,
    insets = { left = 3, right = 3, top = 6, bottom = 6 },
};

local function slider_OnValueChanged(self)
    self.scrollFrame:SetVerticalScroll(self:GetValue())
end

local function tooltip_OnMouseWheel(self, delta)
    local slider = self.slider
    local currentValue = slider:GetValue()
    local minValue, maxValue = slider:GetMinMaxValues()
    local stepValue = self.step or 10

    if delta < 0 and currentValue < maxValue then
        slider:SetValue(min(maxValue, currentValue + stepValue))
    elseif delta > 0 and currentValue > minValue then
        slider:SetValue(max(minValue, currentValue - stepValue))
    end
end

-- Set the step size for the scroll bar
function tipPrototype:SetScrollStep(step)
    self.step = step
end

-- will resize the tooltip to fit the screen and show a scrollbar if needed
function tipPrototype:UpdateScrolling(maxheight)
    self:SetClampedToScreen(false)

    -- all data is in the tooltip; fix colspan width and prevent the layout cleaner from messing up the tooltip later
    FixCellSizes(self)
    layoutCleaner.registry[self] = nil

    local scale = self:GetScale()
    local topside = self:GetTop()
    local bottomside = self:GetBottom()
    local screensize = UIParent:GetHeight() / scale
    local tipsize = (topside - bottomside)

    -- if the tooltip would be too high, limit its height and show the slider
    if bottomside < 0 or topside > screensize or (maxheight and tipsize > maxheight) then
        local shrink = (bottomside < 0 and (5 - bottomside) or 0) + (topside > screensize and (topside - screensize + 5) or 0)

        if maxheight and tipsize - shrink > maxheight then
            shrink = tipsize - maxheight
        end

        self:SetHeight(2 * TOOLTIP_PADDING + self.height - shrink)
        self:SetWidth(2 * TOOLTIP_PADDING + self.width + 20)
        self.scrollFrame:SetPoint("RIGHT", self, "RIGHT", -(TOOLTIP_PADDING + 20), 0)

        if not self.slider then
            local slider = CreateFrame("Slider", nil, self, BackdropTemplateMixin and "BackdropTemplate")
            slider.scrollFrame = self.scrollFrame

            slider:SetOrientation("VERTICAL")
            slider:SetPoint("TOPRIGHT", self, "TOPRIGHT", -TOOLTIP_PADDING, -TOOLTIP_PADDING)
            slider:SetPoint("BOTTOMRIGHT", self, "BOTTOMRIGHT", -TOOLTIP_PADDING, TOOLTIP_PADDING)
            slider:SetBackdrop(BACKDROP_SLIDER_8_8)
            slider:SetThumbTexture([[Interface\Buttons\UI-SliderBar-Button-Vertical]])
            slider:SetMinMaxValues(0, 1)
            slider:SetValueStep(1)
            slider:SetWidth(12)
            slider:SetScript("OnValueChanged", slider_OnValueChanged)
            slider:SetValue(0)

            self.slider = slider
        end

        self.slider:SetMinMaxValues(0, shrink)
        self.slider:Show()

        self:EnableMouseWheel(true)
        self:SetScript("OnMouseWheel", tooltip_OnMouseWheel)
    else
        self:SetHeight(2 * TOOLTIP_PADDING + self.height)
        self:SetWidth(2 * TOOLTIP_PADDING + self.width)

        self.scrollFrame:SetPoint("RIGHT", self, "RIGHT", -TOOLTIP_PADDING, 0)

        if self.slider then
            self.slider:SetValue(0)
            self.slider:Hide()

            self:EnableMouseWheel(false)
            self:SetScript("OnMouseWheel", nil)
        end
    end
end

------------------------------------------------------------------------------
-- Tooltip methods for changing its contents.
------------------------------------------------------------------------------
function tipPrototype:Clear()
    for i, line in ipairs(self.lines) do
        for _, cell in pairs(line.cells) do
            if cell then
                ReleaseCell(cell)
            end
        end

        ReleaseTable(line.cells)

        line.cells = nil
        line.is_header = nil

        ReleaseFrame(line)

        self.lines[i] = nil
    end

    for _, column in ipairs(self.columns) do
        column.width = 0
        column:SetWidth(1)
    end

    wipe(self.colspans)

    self.cell_margin_h = nil
    self.cell_margin_v = nil

    ResetTooltipSize(self)
end

function tipPrototype:SetCellMarginH(size)
    if #self.lines > 0 then
       -- error("Unable to set horizontal margin while the tooltip has lines.", 2)
       return
    end

    if not size or type(size) ~= "number" or size < 0 then
        --error("Margin size must be a positive number or zero.", 2)
        return
    end

    self.cell_margin_h = size
end

function tipPrototype:SetCellMarginV(size)
    if #self.lines > 0 then
        error("Unable to set vertical margin while the tooltip has lines.", 2)
    end

    if not size or type(size) ~= "number" or size < 0 then
        error("Margin size must be a positive number or zero.", 2)
    end

    self.cell_margin_v = size
end

function SetTooltipSize(tooltip, width, height)
    tooltip.height = height
    tooltip.width = width

    tooltip:SetHeight(2 * TOOLTIP_PADDING + height)
    tooltip:SetWidth(2 * TOOLTIP_PADDING + width)

    tooltip.scrollChild:SetHeight(height)
    tooltip.scrollChild:SetWidth(width)
end

-- Add 2 pixels to height so dangling letters (g, y, p, j, etc) are not clipped.
function ResetTooltipSize(tooltip)
    local h_margin = tooltip.cell_margin_h or CELL_MARGIN_H

    SetTooltipSize(tooltip, max(0, (h_margin * (#tooltip.columns - 1)) + (h_margin / 2)), 2)
end

local function EnlargeColumn(tooltip, column, width)
    if width > column.width then
        SetTooltipSize(tooltip, tooltip.width + width - column.width, tooltip.height)

        column.width = width
        column:SetWidth(width)
    end
end

local function ResizeLine(tooltip, line, height)
    SetTooltipSize(tooltip, tooltip.width, tooltip.height + height - line.height)

    line.height = height
    line:SetHeight(height)
end

function FixCellSizes(tooltip)
    local columns = tooltip.columns
    local colspans = tooltip.colspans
    local lines = tooltip.lines
    local h_margin = tooltip.cell_margin_h or CELL_MARGIN_H

    -- resize columns to make room for the colspans
    while next(colspans) do
        local maxNeedCols
        local maxNeedWidthPerCol = 0

        -- calculate the colspan with the highest additional width need per column
        for colRange, width in pairs(colspans) do
            local left, right = colRange:match("^(%d+)%-(%d+)$")

            left, right = tonumber(left), tonumber(right)

            for col = left, right - 1 do
                width = width - columns[col].width - h_margin
            end

            width = width - columns[right].width

            if width <= 0 then
                colspans[colRange] = nil
            else
                width = width / (right - left + 1)

                if width > maxNeedWidthPerCol then
                    maxNeedCols = colRange
                    maxNeedWidthPerCol = width
                end
            end
        end

        -- resize all columns for that colspan
        if maxNeedCols then
            local left, right = maxNeedCols:match("^(%d+)%-(%d+)$")

            for col = left, right do
                EnlargeColumn(tooltip, columns[col], columns[col].width + maxNeedWidthPerCol)
            end

            colspans[maxNeedCols] = nil
        end
    end

    --now that the cell width is set, recalculate the rows' height
    for _, line in ipairs(lines) do
        if #(line.cells) > 0 then
            local lineheight = 0

            for _, cell in pairs(line.cells) do
                if cell then
                    lineheight = max(lineheight, cell:getContentHeight())
                end
            end

            if lineheight > 0 then
                ResizeLine(tooltip, line, lineheight)
            end
        end
    end
end

local function _SetCell(tooltip, lineNum, colNum, value, font, justification, colSpan, provider, ...)
    local line = tooltip.lines[lineNum]
    local cells = line.cells

    -- Unset: be quick
    if value == nil then
        local cell = cells[colNum]

        if cell then
            for i = colNum, colNum + cell._colSpan - 1 do
                cells[i] = nil
            end

            ReleaseCell(cell)
        end

        return lineNum, colNum
    end

    font = font or (line.is_header and tooltip.headerFont or tooltip.regularFont)

    -- Check previous cell
    local cell
    local prevCell = cells[colNum]

    if prevCell then
        -- There is a cell here
        justification = justification or prevCell._justification
        colSpan = colSpan or prevCell._colSpan

        -- Clear the currently marked colspan
        for i = colNum + 1, colNum + prevCell._colSpan - 1 do
            cells[i] = nil
        end

        if provider == nil or prevCell._provider == provider then
            -- Reuse existing cell
            cell = prevCell
            provider = cell._provider
        else
            -- A new cell is required
            cells[colNum] = ReleaseCell(prevCell)
        end
    elseif prevCell == nil then
        -- Creating a new cell, using meaningful defaults.
        provider = provider or tooltip.labelProvider
        justification = justification or tooltip.columns[colNum].justification or "LEFT"
        colSpan = colSpan or 1
    else
        error("overlapping cells at column " .. colNum, 3)
    end

    local tooltipWidth = #tooltip.columns
    local rightColNum

    if colSpan > 0 then
        rightColNum = colNum + colSpan - 1

        if rightColNum > tooltipWidth then
            error("ColSpan too big, cell extends beyond right-most column", 3)
        end
    else
        -- Zero or negative: count back from right-most columns
        rightColNum = max(colNum, tooltipWidth + colSpan)
        -- Update colspan to its effective value
        colSpan = 1 + rightColNum - colNum
    end

    -- Cleanup colspans
    for i = colNum + 1, rightColNum do
        local columnCell = cells[i]

        if columnCell then
            ReleaseCell(columnCell)
        elseif columnCell == false then
            error("overlapping cells at column " .. i, 3)
        end

        cells[i] = false
    end

    -- Create the cell
    if not cell then
        cell = AcquireCell(tooltip, provider)
        cells[colNum] = cell
    end

    -- Anchor the cell
    cell:SetPoint("LEFT", tooltip.columns[colNum])
    cell:SetPoint("RIGHT", tooltip.columns[rightColNum])
    cell:SetPoint("TOP", line)
    cell:SetPoint("BOTTOM", line)

    -- Store the cell settings directly into the cell
    -- That's a bit risky but is really cheap compared to other ways to do it
    cell._font, cell._justification, cell._colSpan, cell._line, cell._column = font, justification, colSpan, lineNum, colNum

    -- Setup the cell content
    local width, height = cell:SetupCell(tooltip, value, justification, font, ...)
    cell:Show()

    if colSpan > 1 then
        -- Postpone width changes until the tooltip is shown
        local colRange = colNum .. "-" .. rightColNum

        tooltip.colspans[colRange] = max(tooltip.colspans[colRange] or 0, width)
        layoutCleaner:RegisterForCleanup(tooltip)
    else
        -- Enlarge the column and tooltip if need be
        EnlargeColumn(tooltip, tooltip.columns[colNum], width)
    end

    -- Enlarge the line and tooltip if need be
    if height > line.height then
        SetTooltipSize(tooltip, tooltip.width, tooltip.height + height - line.height)

        line.height = height
        line:SetHeight(height)
    end

    if rightColNum < tooltipWidth then
        return lineNum, rightColNum + 1
    else
        return lineNum, nil
    end
end

do
    local function CreateLine(tooltip, font, ...)
        if #tooltip.columns == 0 then
            error("column layout should be defined before adding line", 3)
        end

        local lineNum = #tooltip.lines + 1
        local line = tooltip.lines[lineNum] or AcquireFrame(tooltip.scrollChild)

        line:SetFrameLevel(tooltip.scrollChild:GetFrameLevel() + 2)
        line:SetPoint("LEFT", tooltip.scrollChild)
        line:SetPoint("RIGHT", tooltip.scrollChild)

        if lineNum > 1 then
            local v_margin = tooltip.cell_margin_v or CELL_MARGIN_V

            line:SetPoint("TOP", tooltip.lines[lineNum - 1], "BOTTOM", 0, -v_margin)
            SetTooltipSize(tooltip, tooltip.width, tooltip.height + v_margin)
        else
            line:SetPoint("TOP", tooltip.scrollChild)
        end

        tooltip.lines[lineNum] = line

        line.cells = line.cells or AcquireTable()
        line.height = 0
        line:SetHeight(1)
        line:Show()

        local colNum = 1

        for i = 1, #tooltip.columns do
            local value = select(i, ...)

            if value ~= nil then
                lineNum, colNum = _SetCell(tooltip, lineNum, i, value, font, nil, 1, tooltip.labelProvider)
            end
        end

        return lineNum, colNum
    end

    function tipPrototype:AddLine(...)
        return CreateLine(self, self.regularFont, ...)
    end

    function tipPrototype:AddHeader(...)
        local line, col = CreateLine(self, self.headerFont, ...)

        self.lines[line].is_header = true

        return line, col
    end
end -- do-block

local GenericBackdrop = {
    bgFile = "Interface\\Tooltips\\UI-Tooltip-Background"
}

function tipPrototype:AddSeparator(height, r, g, b, a)
    local lineNum, colNum = self:AddLine()
    local line = self.lines[lineNum]
    local color = _G.NORMAL_FONT_COLOR

    height = height or 1

    SetTooltipSize(self, self.width, self.height + height)

    line.height = height
    line:SetHeight(height)
    line:SetBackdrop(GenericBackdrop)
    line:SetBackdropColor(r or color.r, g or color.g, b or color.b, a or 1)

    return lineNum, colNum
end

function tipPrototype:SetCellColor(lineNum, colNum, r, g, b, a)
    local cell = self.lines[lineNum].cells[colNum]

    if cell then
        local sr, sg, sb, sa = self:GetBackdropColor()

        cell:SetBackdrop(GenericBackdrop)
        cell:SetBackdropColor(r or sr, g or sg, b or sb, a or sa)
    end
end

function tipPrototype:SetColumnColor(colNum, r, g, b, a)
    local column = self.columns[colNum]

    if column then
        local sr, sg, sb, sa = self:GetBackdropColor()
        column:SetBackdrop(GenericBackdrop)
        column:SetBackdropColor(r or sr, g or sg, b or sb, a or sa)
    end
end

function tipPrototype:SetLineColor(lineNum, r, g, b, a)
    local line = self.lines[lineNum]

    if line then
        local sr, sg, sb, sa = self:GetBackdropColor()

        line:SetBackdrop(GenericBackdrop)
        line:SetBackdropColor(r or sr, g or sg, b or sb, a or sa)
    end
end

function tipPrototype:SetCellTextColor(lineNum, colNum, r, g, b, a)
    local line = self.lines[lineNum]
    local column = self.columns[colNum]

    if not line or not column then
        return
    end

    local cell = self.lines[lineNum].cells[colNum]

    if cell then
        if not cell.fontString then
            error("cell's label provider did not assign a fontString field", 2)
        end

        if not cell.r then
            cell.r, cell.g, cell.b, cell.a = cell.fontString:GetTextColor()
        end

        cell.fontString:SetTextColor(r or cell.r, g or cell.g, b or cell.b, a or cell.a)
    end
end

function tipPrototype:SetColumnTextColor(colNum, r, g, b, a)
    if not self.columns[colNum] then
        return
    end

    for lineIndex = 1, #self.lines do
        self:SetCellTextColor(lineIndex, colNum, r, g, b, a)
    end
end

function tipPrototype:SetLineTextColor(lineNum, r, g, b, a)
    local line = self.lines[lineNum]

    if not line then
        return
    end

    for cellIndex = 1, #line.cells do
        self:SetCellTextColor(lineNum, line.cells[cellIndex]._column, r, g, b, a)
    end
end

function tipPrototype:SetHighlightTexture(...)
    return highlightTexture:SetTexture(...)
end

function tipPrototype:SetHighlightTexCoord(...)
    highlightTexture:SetTexCoord(...)
end

do
    local function checkFont(font, level, silent)
        local bad = false

        if not font then
            bad = true
        elseif type(font) == "string" then
            local ref = _G[font]

            if not ref or type(ref) ~= "table" or type(ref.IsObjectType) ~= "function" or not ref:IsObjectType("Font") then
                bad = true
            end
        elseif type(font) ~= "table" or type(font.IsObjectType) ~= "function" or not font:IsObjectType("Font") then
            bad = true
        end

        if bad then
            if silent then
                return false
            end

            error("font must be a Font instance or a string matching the name of a global Font instance, not: " .. tostring(font), level + 1)
        end
        return true
    end

    function tipPrototype:SetFont(font)
        local is_string = type(font) == "string"

        checkFont(font, 2)
        self.regularFont = is_string and _G[font] or font
    end

    function tipPrototype:SetHeaderFont(font)
        local is_string = type(font) == "string"

        checkFont(font, 2)
        self.headerFont = is_string and _G[font] or font
    end

    -- TODO: fixed argument positions / remove checks for performance?
    function tipPrototype:SetCell(lineNum, colNum, value, ...)
        -- Mandatory argument checking
        if type(lineNum) ~= "number" then
            error("line number must be a number, not: " .. tostring(lineNum), 2)
        elseif lineNum < 1 or lineNum > #self.lines then
            error("line number out of range: " .. tostring(lineNum), 2)
        elseif type(colNum) ~= "number" then
            error("column number must be a number, not: " .. tostring(colNum), 2)
        elseif colNum < 1 or colNum > #self.columns then
            error("column number out of range: " .. tostring(colNum), 2)
        end

        -- Variable argument checking
        local font, justification, colSpan, provider
        local i, arg = 1, ...

        if arg == nil or checkFont(arg, 2, true) then
            i, font, arg = 2, ...
        end

        if arg == nil or checkJustification(arg, 2, true) then
            i, justification, arg = i + 1, select(i, ...)
        end

        if arg == nil or type(arg) == "number" then
            i, colSpan, arg = i + 1, select(i, ...)
        end

        if arg == nil or type(arg) == "table" and type(arg.AcquireCell) == "function" then
            i, provider = i + 1, arg
        end

        return _SetCell(self, lineNum, colNum, value, font, justification, colSpan, provider, select(i, ...))
    end
end -- do-block

function tipPrototype:GetFont()
    return self.regularFont
end

function tipPrototype:GetHeaderFont()
    return self.headerFont
end

function tipPrototype:GetLineCount()
    return #self.lines
end

function tipPrototype:GetColumnCount()
    return #self.columns
end

------------------------------------------------------------------------------
-- Frame Scripts
------------------------------------------------------------------------------
local scripts = {
    OnEnter = function(frame, ...)
        highlightFrame:SetParent(frame)
        highlightFrame:SetAllPoints(frame)
        highlightFrame:Show()

        if frame._OnEnter_func then
            frame:_OnEnter_func(frame._OnEnter_arg, ...)
        end
    end,
    OnLeave = function(frame, ...)
        highlightFrame:Hide()
        highlightFrame:ClearAllPoints()
        highlightFrame:SetParent(nil)

        if frame._OnLeave_func then
            frame:_OnLeave_func(frame._OnLeave_arg, ...)
        end
    end,
    OnMouseDown = function(frame, ...)
        frame:_OnMouseDown_func(frame._OnMouseDown_arg, ...)
    end,
    OnMouseUp = function(frame, ...)
        frame:_OnMouseUp_func(frame._OnMouseUp_arg, ...)
    end,
    OnReceiveDrag = function(frame, ...)
        frame:_OnReceiveDrag_func(frame._OnReceiveDrag_arg, ...)
    end,
	-- SUPPORT FOR KEYBOARD EVENTS
	OnKeyUp = function(frame, ...)
		frame:SetPropagateKeyboardInput(true)
		frame:_OnKeyUp_func(frame._OnKeyUp_arg, ...)
	end,
	OnKeyDown = function(frame, ...)
		frame:SetPropagateKeyboardInput(true)
		frame:_OnKeyDown_func(frame._OnKeyDown_arg, ...)
	end,
}

function SetFrameScript(frame, script, func, arg)
    if not scripts[script] then
        return
    end

    frame["_" .. script .. "_func"] = func
    frame["_" .. script .. "_arg"] = arg

    if script == "OnMouseDown" or script == "OnMouseUp" or script == "OnReceiveDrag" then
        if func then
            frame:SetScript(script, scripts[script])
        else
            frame:SetScript(script, nil)
        end
    end
	
	-- SUPPORT FOR KEYBOARD EVENTS
	if script == "OnKeyUp" or script == "OnKeyDown" then
		if func then
			frame:SetScript(script, scripts[script])
			frame:EnableKeyboard(true)
		else
			frame:SetScript(script, nil)
			frame:EnableKeyboard(false)
		end
	end

    -- if at least one script is set, set the OnEnter/OnLeave scripts for the highlight
    if frame._OnEnter_func or frame._OnLeave_func or frame._OnMouseDown_func or frame._OnMouseUp_func or frame._OnReceiveDrag_func then
        frame:EnableMouse(true)
        frame:SetScript("OnEnter", scripts.OnEnter)
        frame:SetScript("OnLeave", scripts.OnLeave)
    else
        frame:EnableMouse(false)
        frame:SetScript("OnEnter", nil)
        frame:SetScript("OnLeave", nil)
    end
end

function ClearFrameScripts(frame)
    if frame._OnEnter_func or frame._OnLeave_func or frame._OnMouseDown_func or frame._OnMouseUp_func or frame._OnReceiveDrag_func then
        frame:EnableMouse(false)

        frame:SetScript("OnEnter", nil)
        frame._OnEnter_func = nil
        frame._OnEnter_arg = nil

        frame:SetScript("OnLeave", nil)
        frame._OnLeave_func = nil
        frame._OnLeave_arg = nil

        frame:SetScript("OnReceiveDrag", nil)
        frame._OnReceiveDrag_func = nil
        frame._OnReceiveDrag_arg = nil

        frame:SetScript("OnMouseDown", nil)
        frame._OnMouseDown_func = nil
        frame._OnMouseDown_arg = nil

        frame:SetScript("OnMouseUp", nil)
        frame._OnMouseUp_func = nil
        frame._OnMouseUp_arg = nil
	elseif frame._OnKeyUp_func or frame._OnKeyDown_func then
		frame:EnableKeyboard(false)
		frame:SetScript("OnKeyUp", nil)
		frame._OnKeyUp_func = nil
		frame._OnKeyUp_arg = nil
		
		frame:SetScript("OnKeyDown", nil)
		frame._OnKeyDown_func = nil
		frame._OnKeyDown_arg = nil
	end
end

function tipPrototype:SetLineScript(lineNum, script, func, arg)
    SetFrameScript(self.lines[lineNum], script, func, arg)
end

function tipPrototype:SetColumnScript(colNum, script, func, arg)
    SetFrameScript(self.columns[colNum], script, func, arg)
end

function tipPrototype:SetCellScript(lineNum, colNum, script, func, arg)
    local cell = self.lines[lineNum].cells[colNum]

    if cell then
        SetFrameScript(cell, script, func, arg)
    end
end

------------------------------------------------------------------------------
-- Auto-hiding feature
------------------------------------------------------------------------------

-- Script of the auto-hiding child frame
local function AutoHideTimerFrame_OnUpdate(self, elapsed)
    self.checkElapsed = self.checkElapsed + elapsed

    if self.checkElapsed > 0.1 then
        if self.parent:IsMouseOver() or (self.alternateFrame and self.alternateFrame:IsMouseOver()) then
            self.elapsed = 0
        else
            self.elapsed = self.elapsed + self.checkElapsed

            if self.elapsed >= self.delay then
                lib:Release(self.parent)
            end
        end

        self.checkElapsed = 0
    end
end

-- Usage:
-- :SetAutoHideDelay(0.25) => hides after 0.25sec outside of the tooltip
-- :SetAutoHideDelay(0.25, someFrame) => hides after 0.25sec outside of both the tooltip and someFrame
-- :SetAutoHideDelay() => disable auto-hiding (default)
function tipPrototype:SetAutoHideDelay(delay, alternateFrame, releaseHandler)
    local timerFrame = self.autoHideTimerFrame
    delay = tonumber(delay) or 0

    if releaseHandler then
        if type(releaseHandler) ~= "function" then
            error("releaseHandler must be a function", 2)
        end

        lib.onReleaseHandlers[self] = releaseHandler
    end

    if delay > 0 then
        if not timerFrame then
            timerFrame = AcquireTimer(self)
            timerFrame:SetScript("OnUpdate", AutoHideTimerFrame_OnUpdate)

            self.autoHideTimerFrame = timerFrame
        end

        timerFrame.parent = self
        timerFrame.checkElapsed = 0
        timerFrame.elapsed = 0
        timerFrame.delay = delay
        timerFrame.alternateFrame = alternateFrame
        timerFrame:Show()
    elseif timerFrame then
        self.autoHideTimerFrame = nil

        timerFrame.alternateFrame = nil
        timerFrame:SetScript("OnUpdate", nil)

        ReleaseTimer(timerFrame)
    end
end

------------------------------------------------------------------------------
-- "Smart" Anchoring
------------------------------------------------------------------------------
local function GetTipAnchor(frame)
    local x, y = frame:GetCenter()
	--if not x or not y then return "TOPLEFT", "BOTTOMLEFT" end
	if not x or not y then return "BOTTOMLEFT" end
	local hhalf = (x > UIParent:GetWidth() * 2 / 3) and "RIGHT" or (x < UIParent:GetWidth() / 3) and "LEFT" or "LEFT"
	--local vhalf = (y > UIParent:GetHeight() / 2) and "TOP" or "BOTTOM"
	--return vhalf .. hhalf, frame, (vhalf == "TOP" and "BOTTOM" or "TOP") .. hhalf
	return "BOTTOM"..hhalf, frame, "BOTTOM".. (hhalf == "RIGHT" and "LEFT" or "RIGHT")
end

function tipPrototype:SmartAnchorTo(frame)
    if not frame then
        error("Invalid frame provided.", 2)
    end

    self:ClearAllPoints()
    self:SetClampedToScreen(true)
    self:SetPoint(GetTipAnchor(frame))
end

------------------------------------------------------------------------------
-- Debug slashcmds
------------------------------------------------------------------------------
-- @debug @
local print = print
local function PrintStats()
    local tipCache = tostring(#tooltipHeap)
    local frameCache = tostring(#frameHeap)
    local tableCache = tostring(#tableHeap)
    local header = false

    print("Tooltips used: " .. usedTooltips .. ", Cached: " .. tipCache .. ", Total: " .. tipCache + usedTooltips)
    print("Frames used: " .. usedFrames .. ", Cached: " .. frameCache .. ", Total: " .. frameCache + usedFrames)
    print("Tables used: " .. usedTables .. ", Cached: " .. tableCache .. ", Total: " .. tableCache + usedTables)

    for k in pairs(activeTooltips) do
        if not header then
            print("Active tooltips:")
            header = true
        end
        print("- " .. k)
    end
end

SLASH_LibQTip1 = "/qtip"
_G.SlashCmdList["LibQTip"] = PrintStats
--@end-debug@
