--- **LibDialog-1.0** provides methods for creating dialogs similar to Blizzard's default StaticPopup dialogs,
-- with additions (such as multiple CheckButtons) and improvements (such as multiple EditBoxes, frame and widget
-- recycling, and not tainting default UI elements).
-- @class file
-- @name LibDialog-1.0.lua
-- @release 1

-----------------------------------------------------------------------
-- Upvalued Lua API.
-----------------------------------------------------------------------
-- Functions
local error = _G.error
local pairs = _G.pairs
local tonumber = _G.tonumber

-- Libraries
local table = _G.table

-----------------------------------------------------------------------
-- Library namespace.
-----------------------------------------------------------------------
local LibStub = _G.LibStub
local MAJOR = "LibDialog-1.0RS"

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

local MINOR = 8 -- Should be manually increased
local lib, oldminor = LibStub:NewLibrary(MAJOR, MINOR)

if not lib then
    return
end -- No upgrade needed

local dialog_prototype = _G.CreateFrame("Frame", nil, _G.UIParent, _G.BackdropTemplateMixin and "BackdropTemplate")
local dialog_meta = {
    __index = dialog_prototype
}

local function _SetupAnchor(dialog)
    local default_dialog
    if _G.StaticPopup_DisplayedFrames then
        default_dialog = _G.StaticPopup_DisplayedFrames[#_G.StaticPopup_DisplayedFrames]
    elseif (_G.StaticPopup_HasDisplayedFrames and _G.StaticPopup_IsLastDisplayedFrame) then
        if StaticPopup_HasDisplayedFrames() then
            for idx = STATICPOPUP_NUMDIALOGS,1,-1 do
                local test_dialog = _G["StaticPopup"..idx]
                if StaticPopup_IsLastDisplayedFrame(test_dialog) then
                    default_dialog = test_dialog
                end
            end
        end
    end
    if default_dialog then
        dialog:SetPoint("TOP", default_dialog, "BOTTOM", 0, 0)
    else
        dialog:SetPoint("TOP", _G.UIParent, "TOP", 0, -135)
    end
end

-----------------------------------------------------------------------
-- Migrations.
-----------------------------------------------------------------------
lib.delegates = lib.delegates or {}
lib.queued_delegates = lib.queues_delegates or {}
lib.delegate_queue = lib.delegate_queue or {}

lib.active_dialogs = lib.active_dialogs or {}
lib.active_buttons = lib.active_buttons or {}
lib.active_checkboxes = lib.active_checkboxes or {}
lib.active_editboxes = lib.active_editboxes or {}
lib.active_icons = lib.active_icons or {}

lib.dialog_heap = lib.dialog_heap or {}
lib.button_heap = lib.button_heap or {}
lib.checkbox_heap = lib.checkbox_heap or {}
lib.editbox_heap = lib.editbox_heap or {}
lib.icon_heap = lib.icon_heap or {}

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

local DEFAULT_DIALOG_WIDTH = 320
local DEFAULT_DIALOG_HEIGHT = 72

local DEFAULT_BUTTON_WIDTH = 128
local DEFAULT_BUTTON_HEIGHT = 21

local DEFAULT_EDITBOX_WIDTH = 130
local DEFAULT_EDITBOX_HEIGHT = 32

local DEFAULT_CHECKBOX_SIZE = 32

local DEFAULT_ICON_SIZE = 36

local DEFAULT_DIALOG_TEXT_WIDTH = 290

local MAX_DIALOGS = 4
local MAX_BUTTONS = 3

local DEFAULT_DIALOG_BACKDROP = {
    bgFile = [[Interface\DialogFrame\UI-DialogBox-Background]],
    edgeFile = [[Interface\DialogFrame\UI-DialogBox-Border]],
    tile = true,
    tileSize = 32,
    edgeSize = 32,
    insets = {
        left = 11,
        right = 12,
        top = 12,
        bottom = 11,
    },
}

local TEXT_HORIZONTAL_JUSTIFICATIONS = {
    CENTER = "CENTER",
    LEFT = "LEFT",
    RIGHT = "RIGHT",
}

local TEXT_VERTICAL_JUSTIFICATIONS = {
    BOTTOM = "BOTTOM",
    MIDDLE = "MIDDLE",
    TOP = "TOP",
}

-----------------------------------------------------------------------
-- Upvalues.
-----------------------------------------------------------------------
local delegates = lib.delegates
local queued_delegates = lib.queued_delegates
local delegate_queue = lib.delegate_queue

local active_dialogs = lib.active_dialogs
local active_buttons = lib.active_buttons
local active_checkboxes = lib.active_checkboxes
local active_editboxes = lib.active_editboxes

local dialog_heap = lib.dialog_heap
local button_heap = lib.button_heap
local checkbox_heap = lib.checkbox_heap
local editbox_heap = lib.editbox_heap

-----------------------------------------------------------------------
-- Helper functions.
-----------------------------------------------------------------------
local function _ProcessQueue()
    if #active_dialogs == MAX_DIALOGS then
        return
    end
    local delegate = table.remove(delegate_queue)

    if not delegate then
        return
    end
    local data = queued_delegates[delegate]
    queued_delegates[delegate] = nil

    if data == "" then
        data = nil
    end
    return lib:Spawn(delegate, data)
end

local function _RefreshDialogAnchors()
    for index = 1, #active_dialogs do
        local current_dialog = active_dialogs[index]
        current_dialog:ClearAllPoints()

        if index == 1 then
            local default_dialog = _SetupAnchor(current_dialog)

            if default_dialog then
                current_dialog:SetPoint("TOP", default_dialog, "BOTTOM", 0, 0)
            else
                current_dialog:SetPoint("TOP", _G.UIParent, "TOP", 0, -135)
            end
        else
            current_dialog:SetPoint("TOP", active_dialogs[index - 1], "BOTTOM", 0, 0)
        end
    end
end

local function _RecycleWidget(widget, actives, heap)
    local remove_index

    for index = 1, #actives do
        if actives[index] == widget then
            remove_index = index
        end
    end

    if not remove_index then
        return
    end
    table.remove(actives, remove_index):ClearAllPoints()
    table.insert(heap, widget)
end

local function _ReleaseCheckBox(checkbox)
    checkbox.container:Hide()
    _RecycleWidget(checkbox, active_checkboxes, checkbox_heap)
    checkbox.container:ClearAllPoints()
    checkbox.container:SetParent(nil)
end

local function _ReleaseEditBox(editbox)
    editbox:SetMaxLetters(0)
    editbox:SetMaxBytes(0)
    editbox:Hide()
    _RecycleWidget(editbox, active_editboxes, editbox_heap)
    editbox:SetParent(nil)
end

local function _ReleaseButton(button)
    button:Hide()
    _RecycleWidget(button, active_buttons, button_heap)
    button:SetParent(nil)
end

local function _ReleaseDialog(dialog)
    dialog.delegate = nil
    dialog.data = nil

    if dialog.editboxes then
        for index = 1, #dialog.editboxes do
            _ReleaseEditBox(dialog.editboxes[index])
        end
        dialog.editboxes = nil
    end

    if dialog.checkboxes then
        for index = 1, #dialog.checkboxes do
            _ReleaseCheckBox(dialog.checkboxes[index])
        end
        dialog.checkboxes = nil
    end

    if dialog.buttons then
        for index = 1, #dialog.buttons do
            _ReleaseButton(dialog.buttons[index])
        end
        dialog.buttons = nil
    end
    _RecycleWidget(dialog, active_dialogs, dialog_heap)
    _RefreshDialogAnchors()
end

local function _Dialog_OnShow(dialog)
    local delegate = dialog.delegate

    if not delegate then
        return
    end

    _G.PlaySound(SOUNDKIT.IG_MAINMENU_OPEN, "Master")

    if delegate.on_show then
        delegate.on_show(dialog, dialog.data)
    end
end

local function _Dialog_OnHide(dialog)
    _G.PlaySound(SOUNDKIT.IG_MAINMENU_CLOSE, "Master")

    -- Required so lib:ActiveDialog() will return false if called from code which is called from the delegate's on_hide
    _RecycleWidget(dialog, active_dialogs, dialog_heap)

    local delegate = dialog.delegate
    if delegate.on_hide then
        delegate.on_hide(dialog, dialog.data)
    end

    _ReleaseDialog(dialog)

    if #delegate_queue > 0 then
        local delegate
        repeat
            delegate = _ProcessQueue()
        until not delegate
    end
end

local function _Dialog_OnUpdate(dialog, elapsed)
    local delegate = dialog.delegate

    if dialog.time_remaining and dialog.time_remaining > 0 then
        local remaining = dialog.time_remaining - elapsed

        if remaining <= 0 then
            dialog.time_remaining = nil

            if delegate and delegate.on_cancel then
                delegate.on_cancel(dialog, dialog.data, "timeout")
            end
            dialog:Hide()
            return
        end
        dialog.time_remaining = remaining
    end

    if delegate and delegate.on_update then
        delegate.on_update(dialog, elapsed)
    end
end

local function _Dialog_OnEvent(self, event, ...)
    if self[event] then
        return self[event](self, event, ...)
    end
end

if not lib.hooked_onhide then
    _G.hooksecurefunc("StaticPopup_OnHide", function()
        _RefreshDialogAnchors()

        if #delegate_queue > 0 then
            local delegate
            repeat
                delegate = _ProcessQueue()
            until not delegate
        end
    end)
    lib.hooked_onhide = true
end

if not lib.hooked_set_up_position then
    _G.hooksecurefunc("StaticPopup_SetUpPosition", function()
        _RefreshDialogAnchors()
    end)
    lib.hooked_set_up_position = true
end

if not lib.hooked_escape_pressed then
    local dialogs_to_release = {}

    _G.hooksecurefunc("StaticPopup_EscapePressed", function()
        table.wipe(dialogs_to_release)

        for index = 1, #active_dialogs do
            local dialog = active_dialogs[index]

            if dialog.delegate.hide_on_escape then
                dialogs_to_release[dialog] = true
            end
        end

        for dialog in pairs(dialogs_to_release) do
            local delegate = dialog.delegate

            if delegate.on_cancel and not delegate.no_cancel_on_escape then
                delegate.on_cancel(dialog)
            end
            dialog:Hide()
        end

        if #delegate_queue > 0 then
            local delegate
            repeat
                delegate = _ProcessQueue()
            until not delegate
        end
    end)
    lib.hooked_escape_pressed = true
end

local function CheckBox_GetValue(checkbox)
    local dialog = checkbox:GetParent():GetParent()
    local get_value = dialog.delegate.checkboxes[checkbox:GetID()].get_value

    if get_value then
        return get_value(checkbox, dialog.data)
    end
end

local function CheckBox_OnClick(checkbox, mouse_button, down)
    local dialog = checkbox:GetParent():GetParent()
    local set_value = dialog.delegate.checkboxes[checkbox:GetID()].set_value

    if set_value then
        set_value(checkbox, CheckBox_GetValue(checkbox), dialog.data, mouse_button, down)
    end
    checkbox:SetChecked(CheckBox_GetValue(checkbox))
end

local function _AcquireCheckBox(parent, index)
    local checkbox = table.remove(checkbox_heap)

    if not checkbox then
        local container = _G.CreateFrame("Frame", ("%s_CheckBoxContainer%d"):format(MAJOR, #active_checkboxes + 1), _G.UIParent)
        container:SetHeight(DEFAULT_CHECKBOX_SIZE)

        checkbox = _G.CreateFrame("CheckButton", ("%s_CheckBox%d"):format(MAJOR, #active_checkboxes + 1), container, "UICheckButtonTemplate")
        checkbox:SetScript("OnClick", CheckBox_OnClick)

        checkbox.container = container
    end
    active_checkboxes[#active_checkboxes + 1] = checkbox

    checkbox:SetPoint("LEFT", 0, 0)
    checkbox.text:SetText(parent.delegate.checkboxes[index].label or "")
    checkbox.container:SetParent(parent)
    checkbox:SetID(index)
    checkbox:SetChecked(CheckBox_GetValue(checkbox))

    checkbox.container:Show()
    return checkbox
end

local function EditBox_OnEnterPressed(editbox)
    if not editbox.autoCompleteParams or not _G.AutoCompleteEditBox_OnEnterPressed(editbox) then
        local dialog = editbox:GetParent()
        local on_enter_pressed = dialog.delegate.editboxes[editbox:GetID()].on_enter_pressed

        if on_enter_pressed then
            on_enter_pressed(editbox, dialog.data)
        end
    end
end

local function EditBox_OnEscapePressed(editbox)
    local dialog = editbox:GetParent()
    local on_escape_pressed = dialog.delegate.editboxes[editbox:GetID()].on_escape_pressed

    if on_escape_pressed then
        on_escape_pressed(editbox, dialog.data)
    end
end

local function EditBox_OnShow(editbox)
    local dialog = editbox:GetParent()
    local on_show = dialog.delegate.editboxes[editbox:GetID()].on_show

    if on_show then
        on_show(editbox, dialog.data)
    end
end

local function EditBox_OnTextChanged(editbox, user_input)
    if not editbox.autoCompleteParams or not _G.AutoCompleteEditBox_OnTextChanged(editbox, user_input) then
        local dialog = editbox:GetParent()
        local on_text_changed = dialog.delegate.editboxes[editbox:GetID()].on_text_changed

        if on_text_changed then
            on_text_changed(editbox, dialog.data)
        end
    end
end

local function _AcquireEditBox(dialog, index)
    local editbox = table.remove(editbox_heap)

    if not editbox then
        local editbox_name = ("%s_EditBox%d"):format(MAJOR, #active_editboxes + 1)

        editbox = _G.CreateFrame("EditBox", editbox_name, _G.UIParent, "AutoCompleteEditBoxTemplate")
        editbox:SetWidth(130)
        editbox:SetHeight(32)
        editbox:SetFontObject("ChatFontNormal")

        local left = editbox:CreateTexture(("%sLeft"):format(editbox_name), "BACKGROUND")
        left:SetTexture([[Interface\ChatFrame\UI-ChatInputBorder-Left2]])
        left:SetWidth(32)
        left:SetHeight(32)
        left:SetPoint("LEFT", -10, 0)

        local right = editbox:CreateTexture(("%sRight"):format(editbox_name), "BACKGROUND")
        right:SetTexture([[Interface\ChatFrame\UI-ChatInputBorder-Right2]])
        right:SetWidth(32)
        right:SetHeight(32)
        right:SetPoint("RIGHT", 10, 0)

        local mid = editbox:CreateTexture(("%sMid"):format(editbox_name), "BACKGROUND")
        mid:SetTexture([[Interface\ChatFrame\UI-ChatInputBorder-Mid2]])
        mid:SetHeight(32)
        mid:SetPoint("TOPLEFT", left, "TOPRIGHT", 0, 0)
        mid:SetPoint("TOPRIGHT", right, "TOPLEFT", 0, 0)

        local label = editbox:CreateFontString(("%sLabel"):format(editbox_name), "ARTWORK", "GameFontNormalSmall")
        label:SetPoint("RIGHT", editbox, "LEFT", -15, 0)

        editbox.left = left
        editbox.right = right
        editbox.mid = mid
        editbox.label = label

        editbox:SetScript("OnEnterPressed", EditBox_OnEnterPressed)
        editbox:SetScript("OnEscapePressed", EditBox_OnEscapePressed)
        editbox:SetScript("OnShow", EditBox_OnShow)
        editbox:SetScript("OnTextChanged", EditBox_OnTextChanged)
    end
    local template = dialog.delegate.editboxes[index]

    active_editboxes[#active_editboxes + 1] = editbox

    editbox.addHighlightedText = true

    editbox:SetParent(dialog)
    editbox:SetID(index)
    editbox:SetWidth(template.width or DEFAULT_EDITBOX_WIDTH)

    editbox:SetAutoFocus(template.auto_focus)

    if not template.auto_focus then
        editbox:ClearFocus()
    end
    editbox:SetMaxLetters(template.max_letters or 0)
    editbox:SetMaxBytes(template.max_bytes or 0)
    editbox:SetText(template.text or "")

    if template.label and template.label ~= "" then
        editbox.label:SetText(template.label)
        editbox.label:SetWidth(editbox.label:GetStringWidth())
    else
        editbox.label:Hide()
    end
    editbox:Show()

    return editbox
end

local function Button_OnClick(button, mouse_button, down)
    local dialog = button:GetParent()
    local hide = true
    local on_click = dialog.delegate.buttons[button:GetID()].on_click

    if on_click then
        hide = not on_click(dialog, dialog.data, "clicked")
    end

    if hide then
        dialog:Hide()
    end
end

local function _AcquireButton(parent, index)
    local button = table.remove(button_heap)

    if not button then
        local button_name = ("%s_Button%d"):format(MAJOR, #active_buttons + 1)
        button = _G.CreateFrame("Button", button_name, _G.UIParent)
        button:SetWidth(DEFAULT_BUTTON_WIDTH)
        button:SetHeight(DEFAULT_BUTTON_HEIGHT)

        button:SetNormalTexture([[Interface\Buttons\UI-DialogBox-Button-Up]])
        button:GetNormalTexture():SetTexCoord(0, 1, 0, 0.71875)

        button:SetPushedTexture([[Interface\Buttons\UI-DialogBox-Button-Down]])
        button:GetPushedTexture():SetTexCoord(0, 1, 0, 0.71875)

        button:SetDisabledTexture([[Interface\Buttons\UI-DialogBox-Button-Disabled]])
        button:GetDisabledTexture():SetTexCoord(0, 1, 0, 0.71875)

        button:SetHighlightTexture([[Interface\Buttons\UI-DialogBox-Button-Highlight]], "ADD")
        button:GetHighlightTexture():SetTexCoord(0, 1, 0, 0.71875)

        button:SetNormalFontObject("GameFontNormal")
        button:SetDisabledFontObject("GameFontDisable")
        button:SetHighlightFontObject("GameFontHighlight")

        button:SetScript("OnClick", Button_OnClick)
    end
    active_buttons[#active_buttons + 1] = button

    button:SetText(parent.delegate.buttons[index].text or "FOOF")
    button:SetParent(parent)
    button:SetID(index)

    button:Show()
    return button
end

local function _BuildDialog(delegate, data)
    if #active_dialogs == MAX_DIALOGS then
        if not queued_delegates[delegate] then
            delegate_queue[#delegate_queue + 1] = delegate
            queued_delegates[delegate] = data or ""
        end
        return
    end
    local dialog = table.remove(dialog_heap)

    if not dialog then
        dialog = _G.setmetatable(_G.CreateFrame("Frame", ("%s_Dialog%d"):format(MAJOR, #active_dialogs + 1), _G.UIParent), dialog_meta)
        dialog.is_new = true

        local close_button = _G.CreateFrame("Button", nil, dialog, "UIPanelCloseButton")
        close_button:SetPoint("TOPRIGHT", -3, -3)
        close_button:Hide()

        dialog.close_button = close_button

        local text = dialog:CreateFontString(nil, nil, "GameFontHighlight")
        text:SetWidth(DEFAULT_DIALOG_TEXT_WIDTH)
        text:SetPoint("TOP", 0, -16)

        dialog.text = text
    end
    dialog:Reset()
    dialog.delegate = delegate
    dialog.data = data

    dialog.text:SetText(delegate.text or "")
    dialog.text:SetJustifyH(delegate.text_justify_h and TEXT_HORIZONTAL_JUSTIFICATIONS[delegate.text_justify_h:upper()] or "CENTER")
    dialog.text:SetJustifyV(delegate.text_justify_v and TEXT_VERTICAL_JUSTIFICATIONS[delegate.text_justify_v:upper()] or "MIDDLE")

    if delegate.no_close_button then
        dialog.close_button:Hide()
    else
        dialog.close_button:Show()
    end

    if _G.type(delegate.icon) == "string" then
        if not dialog.icon then
            dialog.icon = dialog:CreateTexture(("%sIcon"):format(dialog:GetName()), "ARTWORK")
            dialog.icon:SetPoint("LEFT", dialog, "LEFT", 16, 0)
        end

        local icon_size = tonumber(delegate.icon_size) and delegate.icon_size or DEFAULT_ICON_SIZE
        dialog.icon:SetSize(icon_size, icon_size)
        dialog.icon:SetTexture(delegate.icon)
        dialog.icon:Show()
    elseif dialog.icon then
        dialog.icon:SetTexture()
        dialog.icon:Hide()
    end

    if delegate.buttons and #delegate.buttons > 0 then
        dialog.buttons = {}

        for index = 1, MAX_BUTTONS do
            local button = delegate.buttons[index]

            if not button then
                break
            end

            if button.text then
                table.insert(dialog.buttons, _AcquireButton(dialog, index))
            end
        end
        local NUM_BUTTONS = #dialog.buttons

        for index = 1, NUM_BUTTONS do
            local button = dialog.buttons[index]

            if index == 1 then
                if NUM_BUTTONS == 3 then
                    button:SetPoint("BOTTOMRIGHT", dialog, "BOTTOM", -72, 16)
                elseif NUM_BUTTONS == 2 then
                    button:SetPoint("BOTTOMRIGHT", dialog, "BOTTOM", -6, 16)
                elseif NUM_BUTTONS == 1 then
                    button:SetPoint("BOTTOM", dialog, "BOTTOM", 0, 16)
                end
            else
                button:SetPoint("LEFT", dialog.buttons[index - 1], "RIGHT", 13, 0)
            end
            local width = button:GetTextWidth()

            if width > 110 then
                button:SetWidth(width + 20)
            else
                button:SetWidth(120)
            end
            button:Enable()
        end
    end
    local NUM_EDITBOXES = 0

    if delegate.editboxes and #delegate.editboxes > 0 then
        dialog.editboxes = {}

        for index = 1, #delegate.editboxes do
            table.insert(dialog.editboxes, _AcquireEditBox(dialog, index))
        end
        NUM_EDITBOXES = #dialog.editboxes

        for index = 1, NUM_EDITBOXES do
            local editbox = dialog.editboxes[index]

            if index == 1 then
                editbox:SetPoint("TOP", dialog.text, "BOTTOM", 0, -8)
            else
                editbox:SetPoint("TOP", dialog.editboxes[index - 1], "BOTTOM", 0, 0)
            end
        end
    end

    if delegate.checkboxes and #delegate.checkboxes > 0 then
        dialog.checkboxes = {}

        for index = 1, #delegate.checkboxes do
            table.insert(dialog.checkboxes, _AcquireCheckBox(dialog, index))
        end
        local max_string_width = 0

        for index = 1, #dialog.checkboxes do
            local string_width = dialog.checkboxes[index].text:GetStringWidth()

            if string_width > max_string_width then
                max_string_width = string_width
            end
        end

        for index = 1, #dialog.checkboxes do
            local checkbox = dialog.checkboxes[index]
            checkbox.container:SetWidth(DEFAULT_CHECKBOX_SIZE + max_string_width)

            if index == 1 then
                if NUM_EDITBOXES > 0 then
                    checkbox.container:SetPoint("TOPLEFT", dialog.editboxes[NUM_EDITBOXES], "BOTTOMLEFT", -12, 0)
                else
                    checkbox.container:SetPoint("TOP", dialog.text, "BOTTOM", 0, -8)
                end
            else
                checkbox.container:SetPoint("TOPLEFT", dialog.checkboxes[index - 1].container, "BOTTOMLEFT", 0, 0)
            end
        end
    end
    dialog.time_remaining = delegate.duration
    return dialog
end

-----------------------------------------------------------------------
-- Library methods.
-----------------------------------------------------------------------
--- Register a new dialog delegate.
-- @name LibDialog-1.0:Register
-- @class function
-- @paramsig delegate_name, delegate
-- @param delegate_name The name the delegate table will be registered under.
-- @param delegate The delegate table definition.
function lib:Register(delegate_name, delegate)
    if _G.type(delegate_name) ~= "string" or delegate_name == "" then
        error(METHOD_USAGE_FORMAT:format("Register", "delegate_name must be a non-empty string"), 2)
    end

    if _G.type(delegate) ~= "table" then
        error(METHOD_USAGE_FORMAT:format("Register", "delegate must be a table"), 2)
    end
    delegates[delegate_name] = delegate
end

local function _FindDelegate(method_name, reference)
    local reference_type = _G.type(reference)

    if reference == "" or (reference_type ~= "string" and reference_type ~= "table") then
        error(METHOD_USAGE_FORMAT:format(method_name, "reference must be a delegate table or a non-empty string"), 3)
    end
    local delegate

    if reference_type == "string" then
        if not delegates[reference] then
            error(METHOD_USAGE_FORMAT:format(method_name, ("\"%s\" does not match a registered delegate"):format(reference)), 3)
        end
        delegate = delegates[reference]
    else
        delegate = reference
    end
    return delegate
end

--- Spawns a dialog from a delegate reference.
-- @name LibDialog-1.0:Spawn
-- @class function
-- @paramsig reference[, data]
-- @param reference The delegate to be used for the spawned dialog. Can be either a string, in which case the delegate must be registered, or a delegate definition table.
-- @param data Additional data to be passed on to the resultant dialog.
function lib:Spawn(reference, data)
    local delegate = _FindDelegate("Spawn", reference)

    -----------------------------------------------------------------------
    -- Check delegate conditionals before building.
    -----------------------------------------------------------------------
    if _G.UnitIsDeadOrGhost("player") and not delegate.show_while_dead then
        if delegate.on_cancel then
            delegate.on_cancel()
        end
        return
    end

    if _G.InCinematic() and not delegate.show_during_cinematic then
        if delegate.on_cancel then
            delegate.on_cancel()
        end
        return
    end

    if delegate.is_exclusive then
        for index = 1, #active_dialogs do
            local dialog = active_dialogs[index]

            if dialog.delegate.is_exclusive then
                if dialog.delegate.on_cancel then
                    dialog.delegate.on_cancel(dialog, dialog.data, "override")
                end
                dialog:Hide()
            end
        end
    end
    local cancel_list = delegate.cancels_on_spawn

    if cancel_list then
        for index = 1, #cancel_list do
            local delegate_name = cancel_list[index]
            local delegate_to_cancel = delegates[delegate_name]

            if delegate_to_cancel then
                for index = 1, #active_dialogs do
                    local dialog = active_dialogs[index]

                    if dialog.delegate == delegate_to_cancel then
                        dialog:Hide()

                        if dialog.delegate.on_cancel then
                            dialog.delegate.on_cancel(dialog, dialog.data, "override")
                        end
                    end
                end
            else
                error(("\"%s\" does not match a registered delegate - unable to cancel"):format(delegate_name), 2)
            end
        end
    end
    local dialog = self:ActiveDialog(reference, data)

    if dialog then
        local delegate = dialog.delegate

        if not delegate.no_cancel_on_reuse and delegate.on_cancel then
            delegate.on_cancel(dialog, dialog.data, "override")
        end
        dialog:Hide()
    end

    -----------------------------------------------------------------------
    -- Build new dialog and anchor it.
    -----------------------------------------------------------------------
    dialog = _BuildDialog(delegate, data)

    if not dialog then
        return
    end

    if delegate.sound then
        _G.PlaySound(delegate.sound)
    end

    -- Anchor to the bottom of existing dialogs. If none exist, check to see if there are visible default StaticPopupDialogs and anchor to that instead; else, anchor to UIParent.
    if #active_dialogs > 0 then
        dialog:SetPoint("TOP", active_dialogs[#active_dialogs], "BOTTOM", 0, 0)
    else
        local default_dialog =_SetupAnchor(dialog)

        if default_dialog then
            dialog:SetPoint("TOP", default_dialog, "BOTTOM", 0, 0)
        else
            dialog:SetPoint("TOP", _G.UIParent, "TOP", 0, -135)
        end
    end
    active_dialogs[#active_dialogs + 1] = dialog
    dialog:Show()

    if dialog.is_new then
        _Dialog_OnShow(dialog)
        dialog.is_new = nil
    end
    dialog:Resize()
    return dialog
end

--- Determines whether or not a specific dialog is currently active.
-- @name LibDialog-1.0:ActiveDialog
-- @class function
-- @paramsig reference[, data]
-- @param reference The delegate criteria for the dialog being targeted. Can be either a string, in which case the delegate must be registered, or a delegate definition table.
-- @param data Additional data to be used as further criteria to determine if the target dialog is active - this would be the same data used to spawn the dialog.
function lib:ActiveDialog(reference, data)
    local delegate = _FindDelegate("ActiveDialog", reference)

    for index = 1, #active_dialogs do
        local dialog = active_dialogs[index]

        if dialog.delegate == delegate and (not data or dialog.data == data) then
            return dialog
        end
    end
end

--- Dismisses a specific dialog.
-- @name LibDialog-1.0:Dismiss
-- @class function
-- @paramsig reference[, data]
-- @param reference The delegate criteria for the dialog being targeted. Can be either a string, in which case the delegate must be registered, or a delegate definition table.
-- @param data Additional data to be used as further criteria to identify the target dialog - this would be the same data used to spawn the dialog.
function lib:Dismiss(reference, data)
    local delegate = _FindDelegate("Dismiss", reference)

    for index = 1, #active_dialogs do
        local dialog = active_dialogs[index]

        if dialog.delegate == delegate and (not data or dialog.data == data) then
            dialog:Hide()
        end
    end
end

-----------------------------------------------------------------------
-- Dialog methods.
-----------------------------------------------------------------------
function dialog_prototype:Reset()
    self:SetWidth(DEFAULT_DIALOG_WIDTH)
    self:SetHeight(DEFAULT_DIALOG_HEIGHT)
    self:SetBackdrop(DEFAULT_DIALOG_BACKDROP)

    self:SetToplevel(true)
    self:EnableKeyboard(true)
    self:EnableMouse(true)
    self:SetFrameStrata("DIALOG")

    self:SetScript("OnShow", _Dialog_OnShow)
    self:SetScript("OnHide", _Dialog_OnHide)
    self:SetScript("OnUpdate", _Dialog_OnUpdate)
    self:SetScript("OnEvent", _Dialog_OnEvent)

    self:RegisterEvent("DISPLAY_SIZE_CHANGED")
end

function dialog_prototype:Resize()
    local delegate = self.delegate
    if not delegate then
        return
    end

    local width = delegate.width or DEFAULT_DIALOG_WIDTH
    local height = delegate.height or 0

    -- Static size ignores widgets for resizing purposes.
    if delegate.static_size then
        if width > 0 then
            self:SetWidth(width)
            self.text:SetWidth(self:GetWidth() - 30)
        end

        if height > 0 then
            self:SetHeight(height)
        end
        return
    end

    if self.buttons and #self.buttons > 0 then
        height = height + 8 + DEFAULT_BUTTON_HEIGHT

        if #self.buttons == MAX_BUTTONS then
            width = 440
        end
    end

    if self.editboxes and #self.editboxes > 0 then
        local dialog_left = self:GetLeft()
        local dialog_right = self:GetRight()
        local label_offset = 0
        local editbox_offset = 0

        for index = 1, #self.editboxes do
            local editbox = self.editboxes[index]
            local editbox_right = editbox:GetRight()
            height = height + DEFAULT_EDITBOX_HEIGHT

            if editbox_right then
                if editbox_offset < 25 and dialog_right > editbox_right and (dialog_right - editbox_right) < 25 then
                    editbox_offset = 25
                elseif editbox_right > dialog_right and editbox_offset < editbox_right - dialog_right then
                    editbox_offset = 25 + (editbox_right - dialog_right)
                end
            end

            if editbox.label:IsShown() then
                local label_left = editbox.label:GetLeft()

                if label_left and label_left < dialog_left and label_offset < dialog_left - label_left then
                    label_offset = dialog_left - label_left
                end
            end
        end
        width = width + label_offset + editbox_offset
    end

    if self.checkboxes then
        height = height + (DEFAULT_CHECKBOX_SIZE * #self.checkboxes)
    end

    if self.icon and self.icon:IsShown() then
        local icon_width = self.icon:GetWidth() + 32
        width = width + icon_width
        self.text:SetWidth(width - icon_width - (self.close_button:GetWidth() + 16))
    else
        self.text:SetWidth(width - 60)
    end
    height = height + 32 + self.text:GetHeight()

    self:SetWidth(width)
    self:SetHeight(height)
end

-----------------------------------------------------------------------
-- Default dialog events.
-----------------------------------------------------------------------
function dialog_prototype:DISPLAY_SIZE_CHANGED()
    self:Resize()
end
