-----------------------------------------------------------------------
-- Upvalued Lua API.
-----------------------------------------------------------------------
local _G = getfenv(0)

-- Functions
local error = _G.error
local pairs = _G.pairs

-- Libraries
local table = _G.table

-----------------------------------------------------------------------
-- Library namespace.
-----------------------------------------------------------------------
local LibStub = _G.LibStub
local MAJOR = "LibToast-1.0"

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

local MINOR = 15 -- Should be manually increased
local LibToast, previousMinorVersion = LibStub:NewLibrary(MAJOR, MINOR)

if not LibToast then
    return
end -- No upgrade needed

-----------------------------------------------------------------------
-- Migrations.
-----------------------------------------------------------------------
LibToast.active_toasts = LibToast.active_toasts or {}
LibToast.queued_toasts = LibToast.queued_toasts or {}
LibToast.templates = LibToast.templates or {}
LibToast.unique_templates = LibToast.unique_templates or {}

LibToast.button_heap = LibToast.button_heap or {}
LibToast.toast_heap = LibToast.toast_heap or {}

LibToast.addon_names = LibToast.addon_names or {}
LibToast.registered_sink = LibToast.registered_sink
LibToast.sink_icons = LibToast.sink_icons or {}
LibToast.sink_template = LibToast.sink_template or {} -- Cheating here, since users can only use strings.
LibToast.sink_titles = LibToast.sink_titles or {}

-----------------------------------------------------------------------
-- Variables.
-----------------------------------------------------------------------
local CurrentToast
local IsInternalCall
local CallingObject

-----------------------------------------------------------------------
-- Constants.
-----------------------------------------------------------------------
local ActiveToasts = LibToast.active_toasts
local QueuedToasts = LibToast.queued_toasts
local ToastHeap = LibToast.toast_heap
local ButtonHeap = LibToast.button_heap

local ToastProxy = {}

local METHOD_USAGE_FORMAT = MAJOR .. ":%s() - %s."

local MAX_ACTIVE_TOASTS = 10

local DEFAULT_FADE_HOLD_TIME = 5
local DEFAULT_FADE_IN_TIME = 0.5
local DEFAULT_FADE_OUT_TIME = 1.2
local DEFAULT_TOAST_WIDTH = 250
local DEFAULT_TOAST_HEIGHT = 50
local DEFAULT_GLOW_WIDTH = 252
local DEFAULT_GLOW_HEIGHT = 56
local DEFAULT_ICON_SIZE = 30
local DEFAULT_OS_SPAWN_POINT = _G.IsMacClient() and "TOPRIGHT" or "BOTTOMRIGHT"

local DEFAULT_TOAST_BACKDROP = {
    bgFile = [[Interface\FriendsFrame\UI-Toast-Background]],
    edgeFile = [[Interface\FriendsFrame\UI-Toast-Border]],
    tile = true,
    tileSize = 12,
    edgeSize = 12,
    insets = {
        left = 5,
        right = 5,
        top = 5,
        bottom = 5,
    },
}

local DEFAULT_BACKGROUND_COLORS = {
    r = 0,
    g = 0,
    b = 0,
}

local DEFAULT_TITLE_COLORS = {
    r = 0.510,
    g = 0.773,
    b = 1,
}

local DEFAULT_TEXT_COLORS = {
    r = 0.486,
    g = 0.518,
    b = 0.541
}

local TOAST_BUTTONS = {
    primary_button = true,
    secondary_button = true,
    tertiary_button = true,
}
local TOAST_BUTTON_HEIGHT = 18

local POINT_TRANSLATION = {
    CENTER = DEFAULT_OS_SPAWN_POINT,
    BOTTOM = "BOTTOMRIGHT",
    BOTTOMLEFT = "BOTTOMLEFT",
    BOTTOMRIGHT = "BOTTOMRIGHT",
    LEFT = "TOPLEFT",
    RIGHT = "TOPRIGHT",
    TOP = "TOPRIGHT",
    TOPLEFT = "TOPLEFT",
    TOPRIGHT = "TOPRIGHT",
}

local SIBLING_ANCHORS = {
    TOPRIGHT = "BOTTOMRIGHT",
    TOPLEFT = "BOTTOMLEFT",
    BOTTOMRIGHT = "TOPRIGHT",
    BOTTOMLEFT = "TOPLEFT",
}

local OFFSET_X = {
    TOPRIGHT = -20,
    TOPLEFT = 20,
    BOTTOMRIGHT = -20,
    BOTTOMLEFT = 20,
}

local OFFSET_Y = {
    TOPRIGHT = -30,
    TOPLEFT = -30,
    BOTTOMRIGHT = 30,
    BOTTOMLEFT = 30,
}

local SIBLING_OFFSET_Y = {
    TOPRIGHT = -10,
    TOPLEFT = -10,
    BOTTOMRIGHT = 10,
    BOTTOMLEFT = 10,
}

local L_TOAST = "Toast"
local L_TOAST_DESC = "Shows messages in a toast window."

local LOCALE = _G.GetLocale()

if LOCALE == "esMX" or LOCALE == "esES" then
    L_TOAST = "Información emergente"
    L_TOAST_DESC = "Muestra mensajes de información en una ventana emergente"
elseif LOCALE == "frFR" then
    L_TOAST_DESC = "Montrer les messages dans une fenêtre \"toast\"."
elseif LOCALE == "deDE" then
    L_TOAST_DESC = "Zeigt Nachrichten in einem Toast-Fenster"
elseif LOCALE == "itIT" then
    -- Nothing yet.
elseif LOCALE == "koKR" then
    -- Nothing yet.
elseif LOCALE == "ptBR" then
    L_TOAST = "Brinde"
    L_TOAST_DESC = "Mostrar mensagems em uma janela externa"
elseif LOCALE == "ruRU" then
    L_TOAST = "Всплывающее"
    L_TOAST_DESC = "Показывать сообщения во всплывающем окне"
elseif LOCALE == "zhCN" then
    L_TOAST = "弹出窗口"
    L_TOAST_DESC = "在弹出窗口显示信息。"
elseif LOCALE == "zhTW" then
    L_TOAST = "彈出視窗"
    L_TOAST_DESC = "在彈出視窗顯示訊息。"
end

-----------------------------------------------------------------------
-- Variables.
-----------------------------------------------------------------------
local QueuedAddOnName

-----------------------------------------------------------------------
-- Settings functions.
-----------------------------------------------------------------------
local function ToastSpawnPoint()
    return _G.Toaster and _G.Toaster:SpawnPoint() or DEFAULT_OS_SPAWN_POINT
end

local function ToastOffsetX()
    return (_G.Toaster and _G.Toaster.SpawnOffsetX) and _G.Toaster:SpawnOffsetX() or nil
end

local function ToastOffsetY()
    return (_G.Toaster and _G.Toaster.SpawnOffsetY) and _G.Toaster:SpawnOffsetY() or nil
end

local function ToastTitleColors(urgencyLevel)
    if _G.Toaster then
        return _G.Toaster:TitleColors(urgencyLevel)
    else
        return DEFAULT_TITLE_COLORS.r, DEFAULT_TITLE_COLORS.g, DEFAULT_TITLE_COLORS.b
    end
end

local function ToastTextColors(urgencyLevel)
    if _G.Toaster then
        return _G.Toaster:TextColors(urgencyLevel)
    else
        return DEFAULT_TEXT_COLORS.r, DEFAULT_TEXT_COLORS.g, DEFAULT_TEXT_COLORS.b
    end
end

local function ToastBackgroundColors(urgencyLevel)
    if _G.Toaster then
        return _G.Toaster:BackgroundColors(urgencyLevel)
    else
        return DEFAULT_BACKGROUND_COLORS.r, DEFAULT_BACKGROUND_COLORS.g, DEFAULT_BACKGROUND_COLORS.b
    end
end

local function ToastDuration(addonName)
    return _G.Toaster and _G.Toaster:Duration(addonName) or DEFAULT_FADE_HOLD_TIME
end

local function ToastOpacity(addonName)
    return _G.Toaster and _G.Toaster:Opacity(addonName) or 0.75
end

local function ToastHasFloatingIcon(addonName)
    return _G.Toaster and _G.Toaster:FloatingIcon(addonName)
end

local function ToastsAreSuppressed(addonName)
    return _G.Toaster and (_G.Toaster:HideToasts() or _G.Toaster:HideToastsFromSource(addonName))
end

local function ToastsAreMuted(addonName)
    return _G.Toaster and (_G.Toaster:MuteToasts() or _G.Toaster:MuteToastsFromSource(addonName))
end

-----------------------------------------------------------------------
-- Helper functions.
-----------------------------------------------------------------------
local function AnimationHideParent(animation)
    animation:GetParent():Hide()
end

local function GetEffectiveSpawnPoint(frame)
    local x, y = frame:GetCenter()
    if not x or not y then
        return DEFAULT_OS_SPAWN_POINT
    end

    local horizontalName = (x > _G.UIParent:GetWidth() * 2 / 3) and "RIGHT" or (x < _G.UIParent:GetWidth() / 3) and "LEFT" or ""
    local verticalName = (y > _G.UIParent:GetHeight() / 2) and "TOP" or "BOTTOM"
    return verticalName .. horizontalName
end

local function GetCallingObject()
    return CallingObject
end

local function StringValue(input)
    local inputType = _G.type(input)

    if inputType == "function" then
        local output = input()
        if _G.type(output) ~= "string" or output == "" then
            return
        end

        return output
    elseif inputType == "string" then
        return input
    end
end

if not LibToast.templates[LibToast.sink_template] then
    LibToast.templates[LibToast.sink_template] = function(toast, text, _, _, _, _, _, _, _, _, iconTexture)
        local callingObject = GetCallingObject()
        toast:SetTitle(StringValue(LibToast.sink_titles[callingObject]))
        toast:SetText(text)
        toast:SetIconTexture(iconTexture or StringValue(LibToast.sink_icons[callingObject]))
    end
end

local function _positionToastIcon(toast)
    toast.icon:ClearAllPoints()

    if ToastHasFloatingIcon(toast.addonName) then
        local lowercasedPointName = POINT_TRANSLATION[GetEffectiveSpawnPoint(toast)]:lower()

        if lowercasedPointName:find("right") then
            toast.icon:SetPoint("TOPRIGHT", toast, "TOPLEFT", -5, -10)
        elseif lowercasedPointName:find("left") then
            toast.icon:SetPoint("TOPLEFT", toast, "TOPRIGHT", 5, -10)
        end
    else
        toast.icon:SetPoint("TOPLEFT", toast, "TOPLEFT", 10, -10)
    end
end

local function _reclaimButton(button)
    button:Hide()
    button:ClearAllPoints()
    button:SetParent(nil)
    button:SetText(nil)
    table.insert(ButtonHeap, button)
end

local function _reclaimToast(toast)
    for buttonName in pairs(TOAST_BUTTONS) do
        local button = toast[buttonName]

        if button then
            toast[buttonName] = nil
            _reclaimButton(button)
        end
    end
    toast.is_persistent = nil
    toast.templateName = nil
    toast.payload = nil
    toast.sound_file = nil
    toast:Hide()

    table.insert(ToastHeap, toast)

    local removalIndex
    for index = 1, #ActiveToasts do
        if ActiveToasts[index] == toast then
            removalIndex = index
            break
        end
    end

    if removalIndex then
        table.remove(ActiveToasts, removalIndex):ClearAllPoints()
    end
    local spawnPoint = ToastSpawnPoint()
    local offsetX = ToastOffsetX() or OFFSET_X[spawnPoint]
    local offsetY = ToastOffsetY() or OFFSET_Y[spawnPoint]

    for index = 1, #ActiveToasts do
        local indexedToast = ActiveToasts[index]
        indexedToast:ClearAllPoints()

        _positionToastIcon(indexedToast)

        if index == 1 then
            indexedToast:SetPoint(spawnPoint, _G.UIParent, spawnPoint, offsetX, offsetY)
        else
            spawnPoint = POINT_TRANSLATION[GetEffectiveSpawnPoint(ActiveToasts[1])]
            indexedToast:SetPoint(spawnPoint, ActiveToasts[index - 1], SIBLING_ANCHORS[spawnPoint], 0, SIBLING_OFFSET_Y[spawnPoint])
        end
    end

    local toastData = table.remove(QueuedToasts, 1)
    if toastData and toastData.addonName and _G.type(toastData.template) == "string" and toastData.template ~= "" then
        QueuedAddOnName = toastData.addonName
        LibToast:Spawn(toastData.template, _G.unpack(toastData.payload))
    end
end

local function AnimationDismissToast(animation)
    _reclaimToast(animation.toast)
end

local function Focus_OnEnter(frame, motion)
    local toast = frame.toast
    toast.dismiss_button:Show()

    if not toast.is_persistent then
        toast.waitAndAnimateOut:Stop()
        toast.waitAndAnimateOut.animateOut:SetStartDelay(1)
    end
end

local function Focus_OnLeave(frame, motion)
    local toast = frame.toast

    if not toast.dismiss_button:IsMouseOver() then
        toast.dismiss_button:Hide()
    end

    if not toast.is_persistent then
        toast.waitAndAnimateOut:Play()
    end
end

local function _dismissToast(frame, button, down)
    _reclaimToast(frame:GetParent())
end

local function _acquireToast(addonName)
    local toast = table.remove(ToastHeap)

    if not toast then
        toast = _G.CreateFrame("Button", nil, _G.UIParent)
        toast:SetFrameStrata("DIALOG")
        toast:Hide()

        local icon = toast:CreateTexture(nil, "BORDER")
        icon:SetSize(DEFAULT_ICON_SIZE, DEFAULT_ICON_SIZE)
        toast.icon = icon

        local title = toast:CreateFontString(nil, "BORDER", "FriendsFont_Normal")
        title:SetJustifyH("LEFT")
        title:SetJustifyV("MIDDLE")
        title:SetWordWrap(true)
        title:SetPoint("TOPLEFT", toast, "TOPLEFT", 44, -10)
        title:SetPoint("RIGHT", toast, "RIGHT", -20, 10)
        toast.title = title

        local focus = _G.CreateFrame("Frame", nil, toast)
        focus:SetAllPoints(toast)
        focus:SetScript("OnEnter", Focus_OnEnter)
        focus:SetScript("OnLeave", Focus_OnLeave)
        focus:SetScript("OnShow", Focus_OnLeave)
        focus.toast = toast

        local dismissButton = _G.CreateFrame("Button", nil, toast)
        dismissButton:SetSize(18, 18)
        dismissButton:SetPoint("TOPRIGHT", toast, "TOPRIGHT", -4, -4)
        dismissButton:SetFrameStrata("DIALOG")
        dismissButton:SetFrameLevel(toast:GetFrameLevel() + 2)
        dismissButton:SetNormalTexture([[Interface\FriendsFrame\UI-Toast-CloseButton-Up]])
        dismissButton:SetPushedTexture([[Interface\FriendsFrame\UI-Toast-CloseButton-Down]])
        dismissButton:SetHighlightTexture([[Interface\FriendsFrame\UI-Toast-CloseButton-Highlight]])
        dismissButton:Hide()
        dismissButton:SetScript("OnClick", _dismissToast)

        toast.dismiss_button = dismissButton

        local text = toast:CreateFontString(nil, "BORDER", "FriendsFont_Normal")
        text:SetJustifyH("LEFT")
        text:SetJustifyV("MIDDLE")
        text:SetWordWrap(true)
        text:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -4)
        toast.text = text

        local toastAnimateIn = toast:CreateAnimationGroup()
        toast.animateIn = toastAnimateIn

        local toastAnimateInFirst = toastAnimateIn:CreateAnimation("Alpha")
        toastAnimateInFirst:SetOrder(1)
        toastAnimateInFirst:SetFromAlpha(1)
        toastAnimateInFirst:SetToAlpha(0)
        toastAnimateInFirst:SetDuration(0)

        local toastAnimateInSecond = toastAnimateIn:CreateAnimation("Alpha")
        toastAnimateInSecond:SetOrder(2)
        toastAnimateInSecond:SetFromAlpha(0)
        toastAnimateInSecond:SetToAlpha(1)
        toastAnimateInSecond:SetDuration(0.2)

        local toastWaitAndAnimateOut = toast:CreateAnimationGroup()
        toast.waitAndAnimateOut = toastWaitAndAnimateOut

        local toastAnimateOut = toastWaitAndAnimateOut:CreateAnimation("Alpha")
        toastAnimateOut:SetStartDelay(DEFAULT_FADE_HOLD_TIME)
        toastAnimateOut:SetFromAlpha(1)
        toastAnimateOut:SetToAlpha(0)
        toastAnimateOut:SetDuration(DEFAULT_FADE_OUT_TIME)
        toastAnimateOut:SetScript("OnFinished", AnimationDismissToast)

        toastAnimateOut.toast = toast
        toastWaitAndAnimateOut.animateOut = toastAnimateOut

        local glowFrame = _G.CreateFrame("Frame", nil, toast)
        glowFrame:SetAllPoints(toast)
        toast.glowFrame = glowFrame

        local glowTexture = glowFrame:CreateTexture(nil, "OVERLAY")
        glowTexture:SetSize(DEFAULT_GLOW_WIDTH, DEFAULT_GLOW_HEIGHT)
        glowTexture:SetPoint("TOPLEFT", -1, 3)
        glowTexture:SetPoint("BOTTOMRIGHT", 1, -3)
        glowTexture:SetTexture([[Interface\FriendsFrame\UI-Toast-Flair]])
        glowTexture:SetBlendMode("ADD")
        glowTexture:Hide()

        glowFrame.glow = glowTexture

        local glowAnimateIn = glowTexture:CreateAnimationGroup()
        glowAnimateIn:SetScript("OnFinished", AnimationHideParent)
        glowTexture.animateIn = glowAnimateIn

        local glowAnimateInFirst = glowAnimateIn:CreateAnimation("Alpha")
        glowAnimateInFirst:SetOrder(1)
        glowAnimateInFirst:SetFromAlpha(0)
        glowAnimateInFirst:SetToAlpha(1)
        glowAnimateInFirst:SetDuration(0.2)

        local glowAnimateInSecond = glowAnimateIn:CreateAnimation("Alpha")
        glowAnimateInSecond:SetOrder(2)
        glowAnimateInSecond:SetFromAlpha(1)
        glowAnimateInSecond:SetToAlpha(0)
        glowAnimateInSecond:SetDuration(0.5)
    end

    toast:SetSize(DEFAULT_TOAST_WIDTH, DEFAULT_TOAST_HEIGHT)
    toast:SetBackdrop(DEFAULT_TOAST_BACKDROP)
    toast:SetBackdropBorderColor(1, 1, 1)

    if _G.Toaster then
        local iconSize = _G.Toaster:IconSize(addonName)
        toast.icon:SetSize(iconSize, iconSize)
    end

    return toast
end

-----------------------------------------------------------------------
-- Library methods.
-----------------------------------------------------------------------
function LibToast:Register(templateName, constructor, isUnique)
    local isLib = (self == LibToast)

    if _G.type(templateName) ~= "string" or templateName == "" then
        error(METHOD_USAGE_FORMAT:format(isLib and "Register" or "RegisterToast", "templateName must be a non-empty string"), 2)
    end

    if _G.type(constructor) ~= "function" then
        error(METHOD_USAGE_FORMAT:format(isLib and "Register" or "RegisterToast", "constructor must be a function"), 2)
    end

    LibToast.templates[templateName] = constructor
    LibToast.unique_templates[templateName] = isUnique or nil
end

function LibToast:Spawn(templateName, ...)
    local isLib = (self == LibToast)

    if not templateName or (not IsInternalCall and (_G.type(templateName) ~= "string" or templateName == "")) then
        error(METHOD_USAGE_FORMAT:format(isLib and "Spawn" or "SpawnToast", "templateName must be a non-empty string"), 2)
    end

    if not LibToast.templates[templateName] then
        error(METHOD_USAGE_FORMAT:format(isLib and "Spawn" or "SpawnToast", ("\"%s\" does not match a registered template"):format(templateName)), 2)
    end

    local addonName
    if QueuedAddOnName then
        addonName = QueuedAddOnName
        QueuedAddOnName = nil
    elseif isLib then
        addonName = _G.select(3, ([[\]]):split(_G.debugstack(2)))
    else
        addonName = LibToast.addon_names[self] or _G.UNKNOWN
    end

    if ToastsAreSuppressed(addonName) then
        return
    end

    if LibToast.unique_templates[templateName] then
        for index = 1, #ActiveToasts do
            if ActiveToasts[index].templateName == templateName then
                return
            end
        end
    end

    if #ActiveToasts >= MAX_ACTIVE_TOASTS then
        table.insert(QueuedToasts, {
            addonName = addonName,
            template = templateName,
            payload = { ... }
        })
        return
    end

    CurrentToast = _acquireToast(addonName)
    CurrentToast.templateName = templateName
    CurrentToast.addonName = addonName

    -----------------------------------------------------------------------
    -- Reset defaults.
    -----------------------------------------------------------------------
    CurrentToast.title:SetText(nil)
    CurrentToast.text:SetText(nil)
    CurrentToast.icon:SetTexture(nil)
    CurrentToast.icon:SetTexCoord(0, 1, 0, 1)

    -----------------------------------------------------------------------
    -- Run constructor.
    -----------------------------------------------------------------------
    CallingObject = self
    LibToast.templates[templateName](ToastProxy, ...)

    if not CurrentToast.title:GetText() and not CurrentToast.text:GetText() and not CurrentToast.icon:GetTexture() then
        _reclaimToast(CurrentToast)
        return
    end

    -----------------------------------------------------------------------
    -- Finalize layout.
    -----------------------------------------------------------------------
    local urgency = CurrentToast.urgency_level
    CurrentToast.title:SetTextColor(ToastTitleColors(urgency))
    CurrentToast.text:SetTextColor(ToastTextColors(urgency))

    local opacity = ToastOpacity(addonName)
    local r, g, b = ToastBackgroundColors(urgency)
    CurrentToast:SetBackdropColor(r, g, b, opacity)

    r, g, b = CurrentToast:GetBackdropBorderColor()
    CurrentToast:SetBackdropBorderColor(r, g, b, opacity)

    if ToastHasFloatingIcon(addonName) or not CurrentToast.icon:GetTexture() then
        CurrentToast.title:SetPoint("TOPLEFT", CurrentToast, "TOPLEFT", 10, -10)
    else
        CurrentToast.title:SetPoint("TOPLEFT", CurrentToast, "TOPLEFT", CurrentToast.icon:GetWidth() + 15, -10)
    end

    if CurrentToast.title:GetText() then
        CurrentToast.title:SetWidth(CurrentToast:GetWidth() - CurrentToast.icon:GetWidth() - 20)
        CurrentToast.title:Show()
    else
        CurrentToast.title:Hide()
    end

    if CurrentToast.text:GetText() then
        CurrentToast.text:SetWidth(CurrentToast:GetWidth() - CurrentToast.icon:GetWidth() - 20)
        CurrentToast.text:Show()
    else
        CurrentToast.text:Hide()
    end
    local buttonHeight = (CurrentToast.primary_button or CurrentToast.secondary_button or CurrentToast.tertiary_button) and TOAST_BUTTON_HEIGHT or 0
    CurrentToast:SetHeight(CurrentToast.text:GetStringHeight() + CurrentToast.title:GetStringHeight() + buttonHeight + 25)

    -----------------------------------------------------------------------
    -- Anchor and spawn.
    -----------------------------------------------------------------------
    local spawnPoint = ToastSpawnPoint()
    local offsetX = ToastOffsetX() or OFFSET_X[spawnPoint]
    local offsetY = ToastOffsetY() or OFFSET_Y[spawnPoint]

    if #ActiveToasts > 0 then
        spawnPoint = POINT_TRANSLATION[GetEffectiveSpawnPoint(ActiveToasts[1])]
        CurrentToast:SetPoint(spawnPoint, ActiveToasts[#ActiveToasts], SIBLING_ANCHORS[spawnPoint], 0, SIBLING_OFFSET_Y[spawnPoint])
    else
        CurrentToast:SetPoint(spawnPoint, _G.UIParent, spawnPoint, offsetX, offsetY)
    end

    ActiveToasts[#ActiveToasts + 1] = CurrentToast

    _positionToastIcon(CurrentToast)

    if CurrentToast.sound_file and not ToastsAreMuted(addonName) then
        _G.PlaySoundFile(CurrentToast.sound_file)
    end

    CurrentToast:Show()
    CurrentToast.animateIn:Play()
    CurrentToast.glowFrame.glow:Show()
    CurrentToast.glowFrame.glow.animateIn:Play()
    CurrentToast.waitAndAnimateOut:Stop() -- Stop prior fade attempt.

    if not CurrentToast.is_persistent then
        if CurrentToast:IsMouseOver() then
            CurrentToast.waitAndAnimateOut.animateOut:SetStartDelay(1)
        else
            CurrentToast.waitAndAnimateOut.animateOut:SetStartDelay(ToastDuration(addonName))
            CurrentToast.waitAndAnimateOut:Play()
        end
    end
end

function LibToast:DefineSink(displayName, texturePath)
    local isLib = (self == LibToast)
    local texturePathType = _G.type(texturePath)
    local displayNameType = _G.type(displayName)

    if texturePath and (texturePathType ~= "function" and (texturePathType ~= "string" or texturePath == "")) then
        error(METHOD_USAGE_FORMAT:format(isLib and "DefineSink" or "DefineSinkToast", "texturePath must be a non-empty string, a function that returns one, or nil"), 2)
    end
    if displayName and (displayNameType ~= "function" and (displayNameType ~= "string" or displayName == "")) then
        error(METHOD_USAGE_FORMAT:format(isLib and "DefineSink" or "DefineSinkToast", "displayName must be a non-empty string, a function that returns one, or nil"), 2)
    end

    local addonName = _G.select(3, ([[\]]):split(_G.debugstack(2)))
    LibToast.addon_names[self] = addonName or _G.UNKNOWN
    LibToast.sink_icons[self] = texturePath
    LibToast.sink_titles[self] = displayName

    if not LibToast.registered_sink then
        local LibSink = LibStub("LibSink-2.0")
        if not LibSink then
            return
        end

        LibSink:RegisterSink("LibToast-1.0", L_TOAST, L_TOAST_DESC, function(caller, text, ...)
            IsInternalCall = true

            local func
            if caller.SpawnToast then
                func = caller.SpawnToast
            else
                caller = LibToast
                func = LibToast.Spawn
            end

            func(caller, LibToast.sink_template, text, ...)
            IsInternalCall = nil
        end)

        LibToast.registered_sink = true
    end
end

-----------------------------------------------------------------------
-- Proxy methods.
-----------------------------------------------------------------------
local TOAST_URGENCIES = {
    very_low = true,
    moderate = true,
    normal = true,
    high = true,
    emergency = true,
}

function ToastProxy:SetUrgencyLevel(urgencyLevel)
    urgencyLevel = urgencyLevel:gsub(" ", "_"):lower()

    if not TOAST_URGENCIES[urgencyLevel] then
        error(("\"%s\" is not a valid toast urgency level"):format(urgencyLevel), 2)
    end
    CurrentToast.urgency_level = urgencyLevel
end

function ToastProxy:UrgencyLevel()
    return CurrentToast.urgency_level
end

function ToastProxy:SetTitle(title)
    CurrentToast.title:SetText(title)
end

function ToastProxy:SetFormattedTitle(title, ...)
    CurrentToast.title:SetFormattedText(title, ...)
end

function ToastProxy:SetText(text)
    CurrentToast.text:SetText(text)
end

function ToastProxy:SetFormattedText(text, ...)
    CurrentToast.text:SetFormattedText(text, ...)
end

function ToastProxy:SetIconAtlas(...)
    CurrentToast.icon:SetAtlas(...)
end

function ToastProxy:SetIconTexture(texture)
    CurrentToast.icon:SetTexture(texture)
end

function ToastProxy:SetIconTexCoord(...)
    CurrentToast.icon:SetTexCoord(...)
end

local _initializedToastButton
do
    local BUTTON_NAME_FORMAT = "LibToast_Button%d"
    local button_count = 0

    local function _buttonCallbackHandler(button, mouseButtonName, isPress)
        button.handler(button.id, mouseButtonName, isPress, button.toast.payload)
        _reclaimToast(button.toast)
    end

    local function _acquireToastButton(toast)
        local button = table.remove(ButtonHeap)

        if not button then
            button_count = button_count + 1

            button = _G.CreateFrame("Button", BUTTON_NAME_FORMAT:format(button_count), toast, "UIMenuButtonStretchTemplate")
            button:SetHeight(TOAST_BUTTON_HEIGHT)
            button:SetFrameStrata("DIALOG")
            button:SetScript("OnClick", _buttonCallbackHandler)

            local fontString = button:GetFontString()
            fontString:SetJustifyH("CENTER")
            fontString:SetJustifyV("CENTER")
        end

        button:SetParent(toast)
        button:SetFrameLevel(toast:GetFrameLevel() + 2)
        return button
    end

    function _initializedToastButton(buttonID, label, handler)
        if not label or not handler then
            error("label and handler are required", 3)
            return
        end

        local button = CurrentToast[buttonID]
        if not button then
            button = _acquireToastButton(CurrentToast)
            CurrentToast[buttonID] = button
        end

        button.id = buttonID:gsub("_button", "")
        button.handler = handler
        button.toast = CurrentToast

        button:Show()
        button:SetText(label)
        button:SetWidth(button:GetFontString():GetStringWidth() + 15)

        return button
    end
end -- do-block

function ToastProxy:SetPrimaryCallback(label, handler)
    local button = _initializedToastButton("primary_button", label, handler)
    button:SetPoint("BOTTOMLEFT", CurrentToast, "BOTTOMLEFT", 3, 4)
    button:SetPoint("BOTTOMRIGHT", CurrentToast, "BOTTOMRIGHT", -3, 4)

    CurrentToast:SetHeight(CurrentToast:GetHeight() + button:GetHeight() + 5)

    if button:GetWidth() > CurrentToast:GetWidth() then
        CurrentToast:SetWidth(button:GetWidth() + 5)
    end
end

function ToastProxy:SetSecondaryCallback(label, handler)
    if not CurrentToast.primary_button then
        error("primary button must be defined first", 2)
    end
    CurrentToast.primary_button:ClearAllPoints()
    CurrentToast.primary_button:SetPoint("BOTTOMLEFT", CurrentToast, "BOTTOMLEFT", 3, 4)

    local button = _initializedToastButton("secondary_button", label, handler)
    button:SetPoint("BOTTOMRIGHT", CurrentToast, "BOTTOMRIGHT", -3, 4)

    if button:GetWidth() + CurrentToast.primary_button:GetWidth() > CurrentToast:GetWidth() then
        CurrentToast:SetWidth(button:GetWidth() + CurrentToast.primary_button:GetWidth() + 5)
    end
end

function ToastProxy:SetTertiaryCallback(label, handler)
    if not CurrentToast.primary_button or not CurrentToast.secondary_button then
        error("primary and secondary buttons must be defined first", 2)
    end
    CurrentToast.secondary_button:ClearAllPoints()
    CurrentToast.secondary_button:SetPoint("LEFT", CurrentToast.primary_button, "RIGHT", 0, 0)

    local button = _initializedToastButton("tertiary_button", label, handler)
    button:SetPoint("LEFT", CurrentToast.secondary_button, "RIGHT", 0, 0)

    if button:GetWidth() + CurrentToast.primary_button:GetWidth() + CurrentToast.secondary_button:GetWidth() > CurrentToast:GetWidth() then
        CurrentToast:SetWidth(button:GetWidth() + CurrentToast.primary_button:GetWidth() + CurrentToast.secondary_button:GetWidth() + 5)
    end
end

function ToastProxy:SetPayload(...)
    CurrentToast.payload = { ... }
end

function ToastProxy:Payload()
    return _G.unpack(CurrentToast.payload)
end

function ToastProxy:MakePersistent()
    CurrentToast.is_persistent = true
end

function ToastProxy:SetSoundFile(filePath)
    CurrentToast.sound_file = filePath
end

-----------------------------------------------------------------------
-- Embed handling.
-----------------------------------------------------------------------
LibToast.embeds = LibToast.embeds or {}

local mixins = {
    "DefineSink",
    "Register",
    "Spawn",
}

function LibToast:Embed(target)
    LibToast.embeds[target] = true

    for index = 1, #mixins do
        local method = mixins[index]
        target[method .. "Toast"] = LibToast[method]
    end
    return target
end

for addon in pairs(LibToast.embeds) do
    LibToast:Embed(addon)
end
