CHAT_SYSTEM = nil

--[[ Essentially there are three main types of controls

* Chat Container
    This container holds all of the text buffers and tabs.
    By default there is a single Chat Container, but players can create more by dragging a tab off of another Chat Container.

* Chat Window
    This is the actual text buffer (and anything special like filters in the combat log) where chat messages are added to.
    A chat container always contains at least one of these.

* Chat Tab
    Each Chat Window has a tab. The tab is the primary place for mouse manipulation.
    Dragging a tab pulls off a Chat Window and consequently create a new Chat Container.
    Unless the tab is the first tab, then it drags the entire Chat Container.
    When the tab is clicked the associated Chat Window is shown.

Lastly, there is the chat system that coordinates dragging across containers, pooling, and so on
]]--

--Named constants
local DONT_FREE = true
local FREE = false

--Constants
local TAB_STARTING_X = 16
local TAB_STARTING_Y = 4

local FADE_ANIMATION_DURATION = 350
local FADE_ANIMATION_DELAY = 3000

local NUM_COMMAND_HISTORY_TO_SAVE = 50

local TAB_ALERT_TEXT_COLOR = ZO_SECOND_CONTRAST_TEXT

local ChannelInfo = ZO_ChatSystem_GetChannelInfo()
local MultiLevelEventToCategoryMappings, SimpleEventToCategoryMappings = ZO_ChatSystem_GetEventCategoryMappings()

function GetChannelName(channelId)
    local channelInfo = ChannelInfo[channelId]
    if channelInfo then
        return channelInfo.dynamicName and GetDynamicChatChannelName(channelInfo.id) or channelInfo.name
    end
end

--[[ ChannelTarget ]]--
local ChannelTarget = ZO_Object:Subclass()

function ChannelTarget:New(...)
    local target = ZO_Object.New(self)
    target:Initialize(...)

    return target
end

function ChannelTarget:Initialize()
    self.currentTargetIndex = 1
    self.targets = {}
end

function ChannelTarget:GetLastTarget()
    return self.lastTarget
end

function ChannelTarget:AddTarget(target)
    if not self.targets[target] then
        self.targets[#self.targets + 1] = target
        self.targets[target] = #self.targets
    end

    -- Always save this off as the last target, we just got a tell from it.
    self.lastTarget = target
end

function ChannelTarget:GetNextTarget()
    local target = self.targets[self.currentTargetIndex]
    self.currentTargetIndex = self.currentTargetIndex + 1
    if self.currentTargetIndex > #self.targets then
        self.currentTargetIndex = 1
    end
    return target
end

function ChannelTarget:GetPreviousTarget()
    local target = self.targets[self.currentTargetIndex]
    self.currentTargetIndex = self.currentTargetIndex - 1
    if self.currentTargetIndex < 1 then
        self.currentTargetIndex = 1
    end
    return target
end

--[[ Text Entry ]]--
local MAX_AUTO_COMPLETION_RESULTS = 10

local TextEntry = ZO_Object:Subclass()

function TextEntry:New(...)
    local entry = ZO_Object.New(self)
    entry:Initialize(...)

    return entry
end

local TEXT_ENTRY_DURATION = 500
local TEXT_ENTRY_MIN_ALPHA = 0.25
local TEXT_ENTRY_MAX_ALPHA = 1.0

function TextEntry:Initialize(system, control, chatEditBufferTop, chatEditBufferBottom)
    self.control = control
    self.system = system
    self.chatEditBufferTop = chatEditBufferTop
    self.chatEditBufferBottom = chatEditBufferBottom
    control.system = system
    control.owner = self

    self.editBg = control:GetNamedChild("Edit")
    self.editControl = control:GetNamedChild("EditBox")
    self.channelLabel = control:GetNamedChild("Label")

    self.commandHistory = ZO_CircularBuffer:New(NUM_COMMAND_HISTORY_TO_SAVE)

    self.commandHistoryCursor = 1

    -- The target autocomplete object handles completions for things like whisper names
    self.targetAutoComplete = ZO_AutoComplete:New(self.editControl, { AUTO_COMPLETE_FLAG_ALL }, { AUTO_COMPLETE_FLAG_GUILD_NAMES }, AUTO_COMPLETION_ONLINE_ONLY, MAX_AUTO_COMPLETION_RESULTS, AUTO_COMPLETION_MANUAL_MODE)
    self.targetAutoComplete:SetUseCallbacks(true)
    self.targetAutoComplete:SetAnchorStyle(AUTO_COMPLETION_ANCHOR_BOTTOM)

    local function OnAutoCompleteEntrySelected(name, selectionMethod)
        self.system.suppressAutoCompleteClear = true
        self.editControl:SetText("")
        self.system:OnAutoCompleteEntrySelected(name)
        self.targetAutoComplete:Hide()
    end
    self.targetAutoComplete:RegisterCallback(ZO_AutoComplete.ON_ENTRY_SELECTED, OnAutoCompleteEntrySelected)

    -- The slash command autocomplete is automatically triggered to display all available slash commands/switches
    local NO_INCLUDE_FLAGS = nil
    local NO_EXCLUDE_FLAGS = nil
    local DEFAULT_ONLINE_ONLY = nil
    local MAX_RESULTS = 8
    self.slashCommandAutoComplete = SlashCommandAutoComplete:New(self.editControl, NO_INCLUDE_FLAGS, NO_EXCLUDE_FLAGS, DEFAULT_ONLINE_ONLY, MAX_RESULTS, AUTO_COMPLETION_AUTOMATIC_MODE, AUTO_COMPLETION_DONT_USE_ARROWS)

    self.control:SetAlpha(TEXT_ENTRY_MIN_ALPHA)
end

function TextEntry:GetEditControl()
    return self.editControl
end

function TextEntry:GetControl()
    return self.control
end

function TextEntry:StartCommandAtIndex(index)
    local command = self.commandHistory:At(index)
    if command then
        self.system:StartTextEntry(command)
        self.commandHistoryCursor = index
    end
end

function TextEntry:AutoCompleteTarget(target)
    self.targetAutoComplete:Show(target)
end

function TextEntry:NextCommand()
    self:StartCommandAtIndex(self.commandHistoryCursor + 1)
end

function TextEntry:PreviousCommand()
    self:StartCommandAtIndex(self.commandHistoryCursor - 1)
end

function TextEntry:AddCommandHistory(text)
    self.commandHistory:Add(text)
    self.commandHistoryCursor = self.commandHistory:Size() + 1
end

local function CreateFadeAnimations(self)
    if not self.fadeAnim then
        self.fadeAnim = ZO_AlphaAnimation:New(self.control)
    end
end

function TextEntry:FadeOut(delay)
    CreateFadeAnimations(self)

    self.fadeAnim:SetMinMaxAlpha(TEXT_ENTRY_MIN_ALPHA, TEXT_ENTRY_MAX_ALPHA)
    self.fadeAnim:FadeOut(0, TEXT_ENTRY_DURATION)
end

function TextEntry:FadeIn()
    CreateFadeAnimations(self)

    self.fadeAnim:SetMinMaxAlpha(TEXT_ENTRY_MIN_ALPHA, TEXT_ENTRY_MAX_ALPHA)
    self.fadeAnim:FadeIn(0, TEXT_ENTRY_DURATION)
end

function TextEntry:Open(text)
    if text and text ~= "" then
        self:SetText(text)
    end

    if not self.open then
        self:FadeIn()
        self.editControl:TakeFocus()
        self.open = true
        SCENE_MANAGER:OnChatInputStart()
    end
end

function TextEntry:Close(keepText)
    if not keepText then
        self.editControl:Clear()
    end

    if self.open then
        self.editControl:LoseFocus()
        self:FadeOut()
        self.open = false
        SCENE_MANAGER:OnChatInputEnd()
    end
end

function TextEntry:IsOpen()
    return self.open
end

function TextEntry:GetText()
    return self.editControl:GetText()
end

function TextEntry:SetText(text)
    self.editControl:SetText(text)
end

function TextEntry:InsertText(text)
    self.editControl:InsertText(text)
end

function TextEntry:SetCursorPosition(pos)
    self.editControl:SetCursorPosition(pos)
end
function TextEntry:GetCursorPosition(offset)
    return self.editControl:GetCursorPosition()
end

function TextEntry:SetChannel(channelData, target)
    local oldChannelText = self.channelLabel:GetText()
    if target then
        if IsDecoratedDisplayName(target) then
            target = ZO_FormatUserFacingDisplayName(target)
        end
        self.channelLabel:SetText(zo_strformat(SI_CHAT_ENTRY_TARGET_FORMAT, GetChannelName(channelData.id), target))
    else
        self.channelLabel:SetText(zo_strformat(SI_CHAT_ENTRY_GENERAL_FORMAT, GetChannelName(channelData.id)))
    end

    return (self.channelLabel:GetText() ~= oldChannelText)
end

function TextEntry:SetColor(r, g, b)
    self.channelLabel:SetColor(r, g, b, 1)
    self.editControl:SetColor(r, g, b, 1)
end

-- As a convenience, this function always starts chat input when trying to link an item.
-- Except on consoles.
function TextEntry:InsertLink(link)
    if not self.editControl:HasFocus() then
        StartChatInput()
    end

    self.editControl:InsertText(link)
    return true
end

function TextEntry:SetFont(font)
    self.editControl:SetFont(font)
    self.channelLabel:SetFont(font)

    ZO_EditContainerSizer.ForceRefreshSize(self.editBg, self.chatEditBufferTop, self.chatEditBufferBottom)
    self.control:SetHeight(self.editBg:GetHeight())
end

function TextEntry:IsAutoCompleteOpen()
    return self.targetAutoComplete:IsOpen()
end

function TextEntry:CloseAutoComplete()
    self.targetAutoComplete:Hide()
end

--[[ Chat Container ]]--
SharedChatContainer = ZO_Object:Subclass()

function SharedChatContainer:New(chatSystem, control, windowPool, tabPool)
    local container = ZO_Object.New(self)
    container.system = chatSystem
    container:Initialize(control, windowPool, tabPool)

    return container
end

function SharedChatContainer:Initialize(control, windowPool, tabPool)
    self.control = control
    self.backdrop = control:GetNamedChild("Bg")
    control.container = self

    self.fadeInReferences = 0
    self.isMinimizingOrMaximizing = false
end

function SharedChatContainer:IsMinimizingOrMaximizing()
    return self.isMinimizingOrMaximizing
end

function SharedChatContainer:SetMinimizingOrMaximizing(isMinimizingOrMaximizing)
    if self.isMinimizingOrMaximizing ~= isMinimizingOrMaximizing then
        self.isMinimizingOrMaximizing = isMinimizingOrMaximizing
        if self.isDragging and self.isMinimizingOrMaximizing then
            self:StopDraggingTab()
        end
    end
end

function SharedChatContainer:LoadWindowSettings(window)
    local tabIndex = window.tab.index

    local _, locked, interactable, _, areTimestampsEnabled = GetChatContainerTabInfo(self.id, tabIndex)
    local fontSize = self.system:GetFontSizeFromSetting()

    self:SetLocked(tabIndex, locked)
    self:SetInteractivity(tabIndex, interactable)
    self:SetTimestampsEnabled(tabIndex, areTimestampsEnabled)
    self:SetFontSize(tabIndex, fontSize)
end

function SharedChatContainer:SetTimestampsEnabled(tabIndex, areTimestampsEnabled)
    local window = self.windows[tabIndex]
    if window and window.combatLog and window.combatLog:AreTimestampsEnabled() ~= areTimestampsEnabled then
        window.combatLog:SetTimestampsEnabled(areTimestampsEnabled)
        self:SaveWindowSettings(tabIndex)
    end
end

function SharedChatContainer:AreTimestampsEnabled(tabIndex)
    local window = self.windows[tabIndex]
    if window and window.combatLog then
        return window.combatLog:AreTimestampsEnabled()
    end
end

function SharedChatContainer:SetLocked(tabIndex, locked)
    local window = self.windows[tabIndex]
    if window and window.locked ~= locked then
        window.locked = locked
        self:SaveWindowSettings(tabIndex)
    end
end

function SharedChatContainer:IsLocked(tabIndex)
    local window = self.windows[tabIndex]
    if window then
        return window.locked
    end
end
function SharedChatContainer:AddFadeInReference()
    self.fadeInReferences = self.fadeInReferences + 1
    self:FadeIn()
end

function SharedChatContainer:RemoveFadeInReference()
    self.fadeInReferences = self.fadeInReferences - 1
    if self.fadeInReferences == 0 then
        self:FadeOut()
    end
end

function SharedChatContainer:SetBackgroundColor(r, g, b, minAlpha, maxAlpha)
    self.backdrop:SetCenterColor(r, g, b, 1)
    self.minAlpha, self.maxAlpha = minAlpha, maxAlpha
    if self.currentBuffer then
        if self.control:GetAlpha() < self.minAlpha then
            self:FadeIn(0)
        else
            self:FadeOut(0)
        end
    end
    self:SaveSettings()
end

function SharedChatContainer:GetBackgroundColor()
    local r, g, b = self.backdrop:GetCenterColor()
    return r, g, b, self.minAlpha, self.maxAlpha
end

function SharedChatContainer:SetMinAlpha(minAlpha)
    local r, g, b, _, maxAlpha = self:GetBackgroundColor()
    self:SetBackgroundColor(r, g, b, minAlpha, zo_max(maxAlpha, minAlpha))
end

function SharedChatContainer:GetMinAlpha()
    return self.minAlpha
end

function SharedChatContainer:ResetMinAlphaToDefault()
    local r, g, b, _, maxAlpha = self:GetBackgroundColor()
    ResetChatContainerColorsToDefault(self.id)
    local _, _, _, bgMinAlpha = GetChatContainerColors(self.id)
    self:SetBackgroundColor(r, g, b, bgMinAlpha, maxAlpha)
end

function SharedChatContainer:UpdateInteractivity(isInteractive)
    if isInteractive then
        self:SyncScrollToBuffer()
    end

    local window = self.currentBuffer and self.currentBuffer:GetParent()
    if window and window.combatLog then
        window.filterSelf:SetHidden(not isInteractive)
        window.filterIncoming:SetHidden(not isInteractive)
        window.filterAll:SetHidden(not isInteractive)
    end
end

function SharedChatContainer:IsInteractive(tabIndex)
    local window = self.windows[tabIndex]
    if window then
        return window.buffer:IsMouseEnabled()
    end
end

function SharedChatContainer:ForceWindowIntoView(tabIndex)
    local window = self.windows[tabIndex]
    if window and self.hiddenTabStartIndex > 2 and self.windows[self.hiddenTabStartIndex] then
        local idealPosition = self.hiddenTabStartIndex
        local tabWidth = window.tab:GetDesiredWidth()
        local foundWidth = 0

        repeat
            idealPosition = idealPosition - 1
            foundWidth = foundWidth + self.windows[idealPosition].tab:GetDesiredWidth()
        until idealPosition <= 2 or foundWidth >= tabWidth

        table.remove(self.windows, tabIndex)
        table.insert(self.windows, idealPosition, window)
        TransferChatContainerTab(self.id, tabIndex, self.id, idealPosition)

        self:UpdateTabIndices(idealPosition)
        self:PerformLayout()
        self.tabGroup:SetClickedButton(window.tab)
    end
end

function SharedChatContainer:SetInteractivity(tabIndex, isInteractive)
    local window = self.windows[tabIndex]
    if window and window.buffer and window.buffer:IsMouseEnabled() ~= isInteractive then
        window.buffer:SetMouseEnabled(isInteractive)
        if window.buffer == self.currentBuffer then
            self:UpdateInteractivity(isInteractive)
        end
        self:SaveWindowSettings(tabIndex)
    end
end

function SharedChatContainer:GetTabName(tabIndex)
    local window = self.windows[tabIndex]
    if window then
        return ZO_TabButton_Text_GetText(window.tab)
    end
end

function SharedChatContainer:SetTabName(tabIndex, name)
    local window = self.windows[tabIndex]
    if window and ZO_TabButton_Text_GetText(window.tab) ~= name then
        ZO_TabButton_Text_SetText(window.tab, name)
        self:SaveWindowSettings(tabIndex)
    end
end

function SharedChatContainer:SetFontSize(tabIndex, fontSize)
    local window = self.windows[tabIndex]
    if window and fontSize ~= window.fontSize then
        window.fontSize = fontSize

        local font = self:GetChatFontFormatString(fontSize)

        window.buffer:SetFont(font)
    end
end

function SharedChatContainer:IsCombatLog(tabIndex)
    local window = self.windows[tabIndex]
    return window and window.combatLog ~= nil
end

function SharedChatContainer:GetChatSystem()
    return self.system
end

function SharedChatContainer:SetBufferColor(categoryId, red, green, blue)
    for _, window in ipairs(self.windows) do
        if window.buffer then
            window.buffer:SetColorById(categoryId, red, green, blue)
        end
    end
end

function SharedChatContainer:ResetToDefaults(tabIndex)
    local window = self.windows[tabIndex]
    if window then
        self:UnregisterCategoriesForWindow(tabIndex)
        ResetChatContainerColorsToDefault(self.id)
        local bgR, bgG, bgB, bgMinAlpha, bgMaxAlpha = GetChatContainerColors(self.id)
        self:SetBackgroundColor(bgR, bgG, bgB, bgMinAlpha, bgMaxAlpha)

        ResetChatContainerTabToDefault(self.id, tabIndex)

        self:LoadWindowSettings(window)

        self:RegisterCategoriesForWindow(tabIndex)

        self:FadeOut(0)
    end

    self:GetChatSystem():SetupFonts()
end

function SharedChatContainer:ShowOptions(tabIndex)
    self:AddFadeInReference()

    CHAT_OPTIONS:Show(self, tabIndex)
end

function SharedChatContainer:LoadSettings(settings)
    self.settings = settings

    local bgR, bgG, bgB, bgMinAlpha, bgMaxAlpha = GetChatContainerColors(self.id)

    self:SetBackgroundColor(bgR, bgG, bgB, bgMinAlpha, bgMaxAlpha)

    for i=1, GetNumChatContainerTabs(self.id) do
        local name, _, _, isCombatLog = GetChatContainerTabInfo(self.id, i)
        if not self.windows[i] then
            if isCombatLog and self.system.combatLogObject then
                self:AddCombatWindow(name)
            else
                self:AddWindow(name)
            end
        end
    end
end

function SharedChatContainer:SetAllowSaveSettings(saveSettings)
    self.allowSettingsSave = saveSettings
end

function SharedChatContainer:SaveWindowSettings(tabIndex)
    if self.allowSettingsSave and self.system:CanSaveSettings() then
        local name = self:GetTabName(tabIndex)
        local locked = self:IsLocked(tabIndex)
        local interactable = self:IsInteractive(tabIndex)
        local areTimestampsEnabled = self:AreTimestampsEnabled(tabIndex)
        SetChatContainerTabInfo(self.id, tabIndex, name, locked, interactable, areTimestampsEnabled)
    end
end

function SharedChatContainer:SaveSettings()
    if self.allowSettingsSave and self.system:CanSaveSettings() then
        self.system:SaveLocalContainerSettings(self, self.control)
        local bgR, bgG, bgB = self.backdrop:GetCenterColor()
        SetChatContainerColors(self.id, bgR, bgG, bgB, self.minAlpha, self.maxAlpha)
    end
end

function SharedChatContainer:InitializeWindowManagement(control, windowPool, tabPool)
    self.windows = {}

    function self.MonitorTabMovementOnUpdate()
        self:MonitorTabMovement()
    end

    self.tabPool = tabPool
    self.windowPool = windowPool

    self.tabGroup = ZO_TabButtonGroup:New()
    self.windowContainer = control:GetNamedChild("WindowContainer")
end

function SharedChatContainer:OnDestroy()
    self.control:SetHidden(true)
    self.settings = nil
end

function SharedChatContainer:OnResizeStart()
    self.OnResizeUpdateFunc = self.OnResizeUpdateFunc or function()
        self:PerformLayout()
    end

    self.control:SetHandler("OnUpdate", self.OnResizeUpdateFunc)
    self.resizing = true
    self:FadeIn()
end

function SharedChatContainer:OnResizeStop()
    self:UpdateScrollVisibility()
    self.control:SetHandler("OnUpdate", nil)
    self.resizing = false
    self:SaveSettings()
    self:FadeOut()
end

function SharedChatContainer:OnMoveStop()
    self:SaveSettings()
end

function SharedChatContainer:OnMouseEnter()
    self:FadeIn()
end

function SharedChatContainer:FadeOut(delay)
    if self.fadeInReferences > 0 or not IsChatSystemAvailableForCurrentPlatform() then
        return
    end

    if not self.fadeAnim then
        self.fadeAnim = ZO_AlphaAnimation:New(self.control)
    end

    self.fadeAnim:SetMinMaxAlpha(self.minAlpha, self.maxAlpha)
    self.fadeAnim:FadeOut(delay or FADE_ANIMATION_DELAY, FADE_ANIMATION_DURATION)
end

function SharedChatContainer:FadeIn(delay, fadeOption)
    if not IsChatSystemAvailableForCurrentPlatform() then
        return
    end

    if not self.fadeAnim then
        self.fadeAnim = ZO_AlphaAnimation:New(self.control)
    end

    self.fadeAnim:SetMinMaxAlpha(self.minAlpha, self.maxAlpha)
    self.fadeAnim:FadeIn(delay or 0, FADE_ANIMATION_DURATION, fadeOption)

    if self.currentBuffer then
        self.currentBuffer:ShowFadedLines()
    end
end

local OVER_FLOW_TAB_SIZE = 40
local NEW_WINDOW_TAB_SIZE = 40
local ADDITIONAL_TABS_TOTAL_SIZE = OVER_FLOW_TAB_SIZE + NEW_WINDOW_TAB_SIZE

--Lays out tabs and optionally leaves additional spacing at an index for tab dropping
function SharedChatContainer:PerformLayout(insertIndex, xOffset)
    local widthSum = TAB_STARTING_X
    local containerWidth = self.control:GetDesiredWidth()
    local numWindows = #self.windows

    self:CalculateConstraints(xOffset)

    self.hiddenTabStartIndex = numWindows + 1

    for i=1, numWindows do
        local tab = self.windows[i].tab
        local tabWidth = tab:GetDesiredWidth()

        tab:ClearAnchors()

        local offset = insertIndex == i and xOffset or 0
        local overflowSize = i == numWindows and 0 or ADDITIONAL_TABS_TOTAL_SIZE
        if widthSum + tabWidth + offset + overflowSize > containerWidth or i > self.hiddenTabStartIndex then
            --this tab can't fit
            if i < self.hiddenTabStartIndex then
                self.hiddenTabStartIndex = i
            end

            tab:SetHidden(true)
        else
            tab:SetHidden(false)
            tab:SetAnchor(BOTTOMLEFT, nil, TOPLEFT, widthSum + offset, TAB_STARTING_Y)
        end

        widthSum = widthSum + tabWidth + offset
    end
end

do
    local function GetTabWidth(window)
        if window and window.tab then
            return window.tab:GetWidth() + TAB_STARTING_X
        end
        return 0
    end
    function SharedChatContainer:CalculateConstraints(secondWidth)
        local width = GetTabWidth(self.windows[1]) + (secondWidth or GetTabWidth(self.windows[2])) + ADDITIONAL_TABS_TOTAL_SIZE
        local minWidth = zo_max(width, self.system.minContainerWidth)
        local maxWidth = zo_max(width + 70, self.system.maxContainerWidth)

        self.control:SetDimensionConstraints(minWidth, self.system.minContainerHeight, maxWidth, self.system.maxContainerHeight)
    end
end

function SharedChatContainer:ApplyInsertIndicator(insertIndex)
    if insertIndex and insertIndex <= self.hiddenTabStartIndex then
        local tab = self.windows[insertIndex - 1].tab
        local insertIndicator = self.system:AcquireInsertIndicator(self)
        insertIndicator:SetAnchor(CENTER, tab, RIGHT, 0, 0)
    else
        self.system:ReleaseInsertIndicator(self)
    end
end

function SharedChatContainer:HandleTabClick(tab)
    local clickedIndex = tab.index

    for windowIndex, window in ipairs(self.windows) do
        window:SetHidden(clickedIndex ~= windowIndex)
    end

    local currentWindow = self.windows[clickedIndex]
    self.currentBuffer = currentWindow and currentWindow.buffer
    if self.currentBuffer then
        self:UpdateInteractivity(self.currentBuffer:IsMouseEnabled())
    end

    if currentWindow.markedForNotification then
        currentWindow.markedForNotification = false
        ZO_TabButton_Text_RestoreDefaultColors(currentWindow.tab)
    end
end

function SharedChatContainer:AddRawTabForWindow(window, name, index, tab)
    if not tab then
        local key
        tab, key = self.tabPool:AcquireObject()
        tab.key = key
    end

    tab.index = index
    tab.container = self

    self.tabGroup:Add(tab)
    tab:SetParent(self.control)

    local function OnTabSelected(tab)
        self:HandleTabClick(tab)
    end

    local function OnTabSizeChanged(tab)
         --Only relayout if the window has actually completed being added
        if self.currentBuffer and self.windows[tab.index] and self.windows[tab.index].tab == tab then
            if not self.monitoredControl then --Don't do any changes while monitoring for a tab drop
                self:PerformLayout()
            end
        end
    end

    ZO_TabButton_Text_Initialize(tab, "SimpleText", name, OnTabSelected, nil, OnTabSizeChanged)

    return tab
end

do
    local function SetAnchor(control, additionalXOffset, valid, point, relativeTo, relativePoint, offsetX, offsetY)
        if valid then
            control:SetAnchor(point, relativeTo, relativePoint, offsetX + additionalXOffset, offsetY)
        end
    end
    function SharedChatContainer:CopyAnchors(from, offsetX)
        self.control:ClearAnchors()
        self.control:SetDimensions(from:GetDimensions())
        SetAnchor(self.control, offsetX, from:GetAnchor(0))
        SetAnchor(self.control, offsetX, from:GetAnchor(1))
    end
end

function SharedChatContainer:TakeWindow(window, previousContainer)
    if #self.windows == 0 then
        --This container was just created due to dragging from another container
        local offsetX = window.tab:GetLeft() - previousContainer.control:GetLeft()
        self:AddRawWindow(window, ZO_TabButton_Text_GetText(window.tab), window.tab)

        self:CopyAnchors(previousContainer.control, offsetX)

        self:StartDraggingTab(1)
        self:FadeIn()
    else
        --This container already existed and this window is being dropped into it
        self:AddRawWindow(window, ZO_TabButton_Text_GetText(window.tab), window.tab, self.insertIndex)
        self.tabGroup:SetClickedButton(window.tab)
        self.insertIndex = nil
    end
    return window.tab.index
end

function SharedChatContainer:FinalizeWindowTransfer(window)
    self:RegisterCategoriesForWindow(window.tab.index)
end

function SharedChatContainer:AddRawWindow(window, name, tab, insertIndex, isCombatLog)
    if insertIndex then
        table.insert(self.windows, insertIndex, window)
        self:UpdateTabIndices(insertIndex)
    else
        self.windows[#self.windows + 1] = window
    end

    tab = self:AddRawTabForWindow(window, name, insertIndex or #self.windows, tab)

    window.buffer.container = self

    window:SetParent(self.windowContainer)
    window:ClearAnchors()
    window:SetAnchorFill()

    window.tab = tab
    tab.window = window

    if #self.windows == 1 then
        self.tabGroup:SetClickedButton(tab)
    end

    self:PerformLayout()

    self.system:OnRawWindowCreated(self, name, isCombatLog)

    if not self.system.isTransferring then
        --Must delay this until the tab is fully transferred to this container and its settings are available
        self:RegisterCategoriesForWindow(tab.index)
    end

    return window
end

function SharedChatContainer:RegisterCategoriesForWindow(tabIndex)
    for i=1, GetNumChatCategories() do
        if IsChatContainerTabCategoryEnabled(self.id, tabIndex, i) then
            self.system:RegisterForCategory(self, i)
        end
    end
end

function SharedChatContainer:UnregisterCategoriesForWindow(tabIndex)
    for i=1, GetNumChatCategories() do
        if IsChatContainerTabCategoryEnabled(self.id, tabIndex, i) then
            self.system:UnregisterFromCategory(self, i)
        end
    end
end

function SharedChatContainer:SetWindowFilterEnabled(tabIndex, category, enabled)
    if self.windows[tabIndex] then
        if IsChatContainerTabCategoryEnabled(self.id, tabIndex, category) ~= enabled then
            SetChatContainerTabCategoryEnabled(self.id, tabIndex, category, enabled)

            if enabled then
                self.system:RegisterForCategory(self, category)
            else
                self.system:UnregisterFromCategory(self, category)
            end
        end
    end
end

function SharedChatContainer:IsScrolledUp()
    local scrollMin, scrollMax = self.scrollbar:GetMinMax()
    local scrollCurrent = self.scrollbar:GetValue()
    if scrollCurrent < scrollMax then
        return true
    else
        return false
    end
end

function SharedChatContainer:AddEventMessageToContainer(formattedEvent, category, narrationMessage, overrideColorDef)
    for i = 1, #self.windows do
        if IsChatContainerTabCategoryEnabled(self.id, i, category) then
            self:AddEventMessageToWindow(self.windows[i], formattedEvent, category, narrationMessage, overrideColorDef)
        end
    end
end

function SharedChatContainer:AddMessageToWindow(window, message, r, g, b, category)
    if window.buffer then
        window.buffer:AddMessage(message, r, g, b, category)
        if self.currentBuffer == window.buffer then
            self:SyncScrollToBuffer()
        end
    end

    -- Handle callout colors and flashes as needed
    if self.system:IsMinimized() then
        if category == CHAT_CATEGORY_WHISPER_INCOMING then
            self.system:StartNewChatNotification()
        end
    else
        if self.currentBuffer == window.buffer then
            if self:IsScrolledUp() then
                ZO_TabButton_Text_SetTextColor(window.tab, TAB_ALERT_TEXT_COLOR)
                ZO_TabButton_Text_AllowColorChanges(window.tab, false)
            end
        else
            if category == CHAT_CATEGORY_WHISPER_INCOMING then
                ZO_TabButton_Text_SetTextColor(window.tab, TAB_ALERT_TEXT_COLOR)
                ZO_TabButton_Text_AllowColorChanges(window.tab, false)
                window.markedForNotification = true
            end
        end
    end
end

function SharedChatContainer:AddEventMessageToWindow(window, message, category, narrationMessage, overrideColorDef)
    local r, g, b
    if overrideColorDef then
        r, g, b = overrideColorDef:UnpackRGB()
    else
        r, g, b = GetChatCategoryColor(category)
    end
    self:AddMessageToWindow(window, message, r, g, b, category)
end

function SharedChatContainer:AddWindow(name)
    local window, key = self.windowPool:AcquireObject()
    window.key = key

    self:AddRawWindow(window, name)
    self:LoadWindowSettings(window)

    return window
end

function SharedChatContainer:AddCombatWindow(name)
    if self.system.combatLogObject then
        local combatLog = self.system.combatLogObject:New()

        local combatWindow = CreateControlFromVirtual(self.windowContainer:GetName(), self.windowContainer, combatLog:GetWindowTemplate(), "Combat")
        combatWindow.buffer = combatWindow:GetNamedChild("Buffer")

        local isCombatLog = true
        self:AddRawWindow(combatWindow, name, nil, nil, isCombatLog)

        combatLog:OnAddedToContainer(self, combatWindow)

        combatWindow.combatLog = combatLog

        self:LoadWindowSettings(combatWindow)

        return combatWindow, combatLog
    end
end

function SharedChatContainer:SetAsPrimary()
    self.primary = true

    self.windowContainer:ClearAnchors()
    self.windowContainer:SetAnchor(TOPRIGHT, self.scrollUpButton, TOPLEFT, 0, 0)
    self.windowContainer:SetAnchor(BOTTOMLEFT, self.system.textEntry:GetControl(), TOPLEFT, 0, -3)
end

function SharedChatContainer:IsPrimary()
    return self.primary
end

function SharedChatContainer:UpdateTabIndices(from)
    for i=from, #self.windows do
        self.windows[i].tab.index = i
    end
end

function SharedChatContainer:RemoveWindow(index, freeOption)
    local window = self.windows[index]

    if window then
        table.remove(self.windows, index)
        self:UpdateTabIndices(index)

        if self.tabGroup:GetClickedButton() == window.tab and #self.windows > 0 then
            self.tabGroup:SetClickedButton(self.windows[1].tab)
        end
        self.tabGroup:Remove(window.tab)
        self:UnregisterCategoriesForWindow(index)

        if freeOption == nil or freeOption == FREE then
            self.tabPool:ReleaseObject(window.tab.key)
            self.windowPool:ReleaseObject(window.key)
            if #self.windows == 0 then
                self.system:DestroyContainer(self)
            else
                self:PerformLayout()
            end

            self.system:OnRawWindowDestroyed(self, index)
        else
            self:PerformLayout()
            return window
        end
    end
end

function SharedChatContainer:TransferWindow(index, targetContainer)
    local window = self:RemoveWindow(index, DONT_FREE)
    if window then
        self.system:TransferWindow(window, self, targetContainer)
        if #self.windows == 0 then
            self.system:DestroyContainer(self)
        end
    end
end

function SharedChatContainer:StartDraggingTab(index)
    local window = self.windows[index]
    if not window or window.locked or self:IsMinimizingOrMaximizing() then
        return
    end

    self.isDragging = true

    if index == 1 or not self.system:MultipleContainersAllowed() then
        --this is the primary tab, just start moving it
        self.control:SetMovable(true)
        self.control:StartMoving()

        if not self.primary then
            self.system:PrepareContainersTabDrop(self, self.windows[index].tab)
        end
    else
        --this tab is being pulled off of the chat container
        self:TransferWindow(index)
    end
end

do
    local function FindInsertPosition(self, controlToInsert, mouseX)
        for i=2, #self.windows do
            local tab = self.windows[i].tab
            if tab:GetLeft() + tab:GetWidth() / 2 > mouseX then
                return i
            end
        end

        return #self.windows + 1
    end

    local function Contains(x, y, left, right, bottom, top)
        return left <= x
            and x < right
            and top <= y
            and y < bottom
    end

    local function GetTabDropArea(control, tab)
        return tab:GetLeft(), control:GetRight(), tab:GetBottom(), tab:GetTop()
    end

    function SharedChatContainer:MonitorTabMovement()
        local monitoredControl = self.monitoredControl

        local x, y = GetUIMousePosition()
        if Contains(x, y, GetTabDropArea(self.control, self.windows[1].tab)) then
            self.insertIndex = FindInsertPosition(self, monitoredControl, x)

            --peform a layout with extra space for the potential tab
            self:PerformLayout(self.insertIndex, monitoredControl:GetWidth())
        elseif self.insertIndex then
            --reset the layout
            self:PerformLayout()
            self.insertIndex = nil
        end
    end
end

function SharedChatContainer:PrepareTabDrop(controlToMonitor)
    self.monitoredControl = controlToMonitor
    self:FadeIn()

    self.MonitorTabMovementOnUpdate = self.MonitorTabMovementOnUpdate or function()
        self:MonitorTabMovement()
    end

    self.control:SetHandler("OnUpdate", self.MonitorTabMovementOnUpdate)
    self:MonitorTabMovement()
end

function SharedChatContainer:StopTabDrop()
    self.monitoredControl = nil
    self.control:SetHandler("OnUpdate", nil)
    self:PerformLayout()
end

function SharedChatContainer:CanTakeTabDrop()
    return self.insertIndex ~= nil
end

function SharedChatContainer:StopDraggingTab()
    self.control:StopMovingOrResizing()
    self.control:SetMovable(false)
    self.isDragging = false

    self.system:StopContainersTabDrop(self)
end

--[[ Scroll bar handling ]]--
function SharedChatContainer:InitializeScrolling(control)
    self.scrollbar = control:GetNamedChild("Scrollbar")
    self.scrollUpButton = self.scrollbar:GetNamedChild("ScrollUp")
    self.scrollDownButton = self.scrollbar:GetNamedChild("ScrollDown")
    self.scrollEndButton = self.scrollbar:GetNamedChild("ScrollEnd")

    self.scrollbar.container = self
end

function SharedChatContainer:SetScroll(value)
    local max = self:GetCurrentMaxScroll()

    if self.scrollbar then
        self.scrollbar:SetValue(value)
    end
    if self.currentBuffer then
        self.currentBuffer:SetScrollPosition(max - value)
    end

    self:UpdateScrollButtons()

    if self.currentBuffer and not self:IsScrolledUp() then
        local window = self.currentBuffer:GetParent()
        ZO_TabButton_Text_RestoreDefaultColors(window.tab)
    end
end

function SharedChatContainer:ScrollByOffset(offset)
    self:SetScroll(self.scrollbar:GetValue() + offset)
end

function SharedChatContainer:ScrollToBottom()
    self:SetScroll(self:GetCurrentMaxScroll())
end

function SharedChatContainer:GetCurrentMaxScroll()
    return self.currentBuffer and self.currentBuffer:GetNumHistoryLines() or 0
end

function SharedChatContainer:UpdateScrollVisibility()
    local visible = self.currentBuffer and self.currentBuffer:GetNumVisibleLines() or 0
    local history = self.currentBuffer and self.currentBuffer:GetNumHistoryLines() or 0
    local hide = history <= visible or self.system.platformSettings.hideScrollBar

    if self.scrollbar then
        self.scrollbar:SetHidden(hide)
        self.scrollUpButton:SetHidden(hide)
        self.scrollDownButton:SetHidden(hide)
        self.scrollEndButton:SetHidden(hide)
    end
end

function SharedChatContainer:SyncScrollToBuffer()
    local max = self:GetCurrentMaxScroll()
    local scrollPosition = self.currentBuffer and self.currentBuffer:GetScrollPosition() or 0

    self.scrollbar:SetMinMax(1, max)
    self.scrollbar:SetValue(max - scrollPosition)

    self:UpdateScrollVisibility()
    self:UpdateScrollButtons()
end

local function GetNewScrollButtonState(scrollButton, disabled)
    if disabled then
        return BSTATE_DISABLED, disabled
    end

    local currentState = scrollButton:GetState()
    if currentState == BSTATE_DISABLED or currentState == BSTATE_DISABLED_PRESSED then
        return BSTATE_NORMAL, disabled
    end

    return currentState, disabled
end

function SharedChatContainer:UpdateScrollButtons()
    local max = self:GetCurrentMaxScroll()
    local value = zo_round(self.scrollbar:GetValue())

    local enabled = max > 1

    if not enabled then
        --force the scroll bar to look like its at the bottom
        self.scrollbar:SetMinMax(0, 1)
        self.scrollbar:SetValue(1)
    end

    self.scrollbar:SetEnabled(enabled)

    local upDisabled = not enabled or value == 1
    self.scrollUpButton:SetState(GetNewScrollButtonState(self.scrollUpButton, upDisabled))

    local downDisabled = not enabled or value == max
    self.scrollDownButton:SetState(GetNewScrollButtonState(self.scrollDownButton, downDisabled))
    self.scrollEndButton:SetState(GetNewScrollButtonState(self.scrollEndButton, downDisabled))
end

function SharedChatContainer:GetChatFontFormatString(fontSize)
    local face = self:GetChatFont():GetFontInfo()
    local shadowStyle = "soft-shadow-thick"
    if fontSize <= 14 then
        shadowStyle = "soft-shadow-thin"
    end

    local fontSizeString = self.system:GetFontSizeString(fontSize)
    return ("%s|%s|%s"):format(face, fontSizeString, shadowStyle)
end

function SharedChatContainer:GetChatFont()
    -- Should be overridden
end

--
--[[ Chat System ]]--
--

SharedChatSystem = ZO_Object:Subclass()

function SharedChatSystem:New(...)
    local chat = ZO_Object.New(self)
    chat:Initialize(...)

    return chat
end

function SharedChatSystem:Initialize(control, platformSettings)
    self.control = control
    self.platformSettings = platformSettings

    self.textEntry = TextEntry:New(self, control:GetNamedChild("TextEntry"), platformSettings.chatEditBufferTop, platformSettings.chatEditBufferBottom)

    local fontSize = self:GetFontSizeFromSetting()
    local textEntryFont = self:GetTextEntryFontString(fontSize)
    self:SetTextEntryFont(textEntryFont)

    self.channelData = ZO_ChatSystem_GetChannelInfo()
    self.switchLookup = ZO_ChatSystem_GetChannelSwitchLookupTable()
    self.targets = {}

    self:InitializeSharedControlManagement(control)
    self:InitializeEventManagement()

    self.commandPrefixes = {}
    self.numUnreadMails = 0
    self:OnNumUnreadMailChanged(GetNumUnreadMail())
    self.isAgentChatActive = false
    self:OnAgentChatUpdated()
    self.isMinimized = false
    self.isMinimizingOrMaximizing = false
    self.allowMultipleContainers = false

    self.minContainerWidth = 300
    self.maxContainerWidth = 550
    self.minContainerHeight = 170
    self.maxContainerHeight = 380
end

function SharedChatSystem:InitializeSharedControlManagement(control, newContainerFn, chatWindowTemplateName, chatWindowTabName)
    local function TabFactoryFunc(tabControl)
        tabControl:GetNamedChild("Text"):SetHidden(self.platformSettings.hideTabs)
    end

    self.containers = {}

    local NO_PARENT = nil
    self.tabPool = ZO_ControlPool:New("ZO_ChatWindowTabTemplate", NO_PARENT, chatWindowTabName)
    self.tabPool:SetCustomFactoryBehavior(TabFactoryFunc)

    local function CreateWindow(objectPool)
        local window = ZO_ObjectPool_CreateControl(chatWindowTemplateName, objectPool, GuiRoot)
        window.buffer = window:GetNamedChild("Buffer")
        window.buffer:SetHorizontalAlignment(self.platformSettings.horizontalAlignment)
        window.buffer:SetLinesInheritAlpha(self.platformSettings.linesInheritAlpha)
        return window
    end

    local function ResetWindowControl(windowControl)
        ZO_ObjectPool_DefaultResetControl(windowControl)
        windowControl.buffer:Clear()
    end

    self.windowPool = ZO_ObjectPool:New(CreateWindow, ResetWindowControl)
    self.insertIndicator = control:GetNamedChild("InsertIndicator")

    local function CreateContainer(objectPool)
        local containerControl = ZO_ObjectPool_CreateNamedControl(self.control:GetName() .. "ZO_ChatContainerTemplate", "ZO_ChatContainerTemplate", objectPool, GuiRoot)
        return newContainerFn(self, containerControl, self.windowPool, self.tabPool)
    end

    self.containerPool = ZO_ObjectPool:New(CreateContainer, function(container) container:OnDestroy() end)
end

function SharedChatSystem:InitializeEventManagement()
    -- override me
end

function SharedChatSystem:InitializeSharedEvents(eventKey)
    self.categories = {}

    local savedVariablesReady = false
    local playerActivated = false
    self.loaded = false

    local function TryLoadingSettings()
        if savedVariablesReady and playerActivated and not self.loaded then
            self:LoadChatFromSettings()
        end
    end

    local function OnAddOnLoaded(event, name)
        if name == "ZO_Ingame" then
            savedVariablesReady = true
            TryLoadingSettings()
        end
    end

    local function OnPlayerActivated()
        playerActivated = true
        TryLoadingSettings()

        if IsChatSystemAvailableForCurrentPlatform() then
            if not self.allowMultipleContainers then
                self:RedockContainersToPrimary()
            end

            self:TryNotificationAndMailBursts()
        end

        self:RefreshVisibility()

        self:OnPlayerActivated()
    end

    EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_ADD_ON_LOADED, OnAddOnLoaded)
    EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_PLAYER_ACTIVATED, OnPlayerActivated)

    if IsChatSystemAvailableForCurrentPlatform() then
        -- Chat events
        local function OnFormattedChatMessage(message, category, targetChannel, fromDisplayName, rawMessageText, narrationMessage, overrideColorDef)
            self:OnFormattedChatMessage(message, category, targetChannel, fromDisplayName, rawMessageText, narrationMessage, overrideColorDef)
        end
        CHAT_ROUTER:RegisterCallback("FormattedChatMessage", OnFormattedChatMessage)

        local function OnTargetAddedToChannel(targetChannel, target)
            self:HandleNewTargetOnChannel(targetChannel, target)
        end
        CHAT_ROUTER:RegisterCallback("TargetAddedToChannel", OnTargetAddedToChannel)

        local function OnAddCommandPrefix(prefixCharacter, callback)
            self:AddCommandPrefix(prefixCharacter, callback)
        end
        CHAT_ROUTER:RegisterCallback("AddCommandPrefix", OnAddCommandPrefix)

        -- Special events (dialogs etc)
        local function OnZoneChannelChanged()
            self:UpdateTextEntryChannel()
        end

        local function OnAgentChatUpdated()
            self:OnAgentChatUpdated()
        end

        local function OnGamepadPreferredModeChanged()
            self:RefreshVisibility()
        end

        local function OnChatCategoryColorChanged(_, categoryId, r, g, b)
            self:OnChatCategoryColorChanged(categoryId, r, g, b)
        end

        local function OnInterfaceSettingChanged(eventCode, settingType, settingId)
            self:OnUiInterfaceSettingChanged(settingId)
        end

        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_ZONE_CHANNEL_CHANGED, OnZoneChannelChanged)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GUILD_DATA_LOADED, function() self:ValidateChatChannel() end)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GUILD_RANK_CHANGED, function() self:ValidateChatChannel() end)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GUILD_RANKS_CHANGED, function() self:ValidateChatChannel() end)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GUILD_MEMBER_RANK_CHANGED, function() self:ValidateChatChannel() end)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_MAIL_NUM_UNREAD_CHANGED, function(_, numUnread) self:OnNumUnreadMailChanged(numUnread) end)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_AGENT_CHAT_REQUESTED, OnAgentChatUpdated)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_AGENT_CHAT_FORCED, OnAgentChatUpdated)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_AGENT_CHAT_TERMINATED, OnAgentChatUpdated)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GAMEPAD_PREFERRED_MODE_CHANGED, OnGamepadPreferredModeChanged)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_CHAT_CATEGORY_COLOR_CHANGED, OnChatCategoryColorChanged)
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_INTERFACE_SETTING_CHANGED, OnInterfaceSettingChanged)
        EVENT_MANAGER:AddFilterForEvent(eventKey, EVENT_INTERFACE_SETTING_CHANGED, REGISTER_FILTER_SETTING_SYSTEM_TYPE, SETTING_TYPE_UI)

        local function OnGamepadUseKeyboardChatChanged()
            self:CloseTextEntry()
            -- If the chat window is hidden and we try to start text entry, the edit control will not be able to be focused
            -- and it will become stuck in an open state. So make sure the visibility is refreshed.
            self:RefreshVisibility()
        end
        EVENT_MANAGER:RegisterForEvent(eventKey, EVENT_GAMEPAD_USE_KEYBOARD_CHAT_CHANGED, OnGamepadUseKeyboardChatChanged)
    end
end

function SharedChatSystem:TryNotificationAndMailBursts()
    -- Overridden if applicable
end

function SharedChatSystem:GetDefaultChatChannel()
    if IsCommunicationRestricted() then
        if IsUnitGrouped("player") then
            return CHAT_CHANNEL_PARTY
        elseif GetNumGuilds() > 0 then
            return CHAT_CHANNEL_GUILD_1
        end
    end
    return CHAT_CHANNEL_SAY
end

function SharedChatSystem:LoadChatFromSettings(newContainerFn, defaults)
    self.suppressSave = true

    self:SetupSavedVars(defaults)

    self.primaryContainer = self:CreateChatContainer(newContainerFn(self, self.control, self.windowPool, self.tabPool))
    self.primaryContainer:SetAsPrimary()

    for i = 2, GetNumChatContainers() do
        self:CreateChatContainer()
    end

    self.suppressSave = false

    self:SetChannel(self:GetDefaultChatChannel())
    self.loaded = true
end

function SharedChatSystem:SetupSavedVars(defaults)
    -- Overridden if applicable
end

function SharedChatSystem:RedockContainersToPrimary()
    for i = 2, #self.containers do
        -- Grab the second container, the first is always the primary
        local container = self.containers[2]
        -- move all tabs to the primary container
        for j = 1, #container.windows do
            container:TransferWindow(1, self.primaryContainer)
        end
    end
end

function SharedChatSystem:CanSaveSettings()
    return not self.suppressSave
end

function SharedChatSystem:SaveLocalContainerSettings(container, containerControl)
    -- Overridden if applicable
end

function SharedChatSystem:AcquireInsertIndicator(container)
    self.insertIndicator.owner = container
    self.insertIndicator:SetHidden(false)
    self.insertIndicator:ClearAnchors()
    return self.insertIndicator
end

function SharedChatSystem:ReleaseInsertIndicator(container)
    if self.insertIndicator.owner == container then
        self.insertIndicator.owner = nil
        self.insertIndicator:SetHidden(true)
    end
end

function SharedChatSystem:RegisterForCategory(container, category)
    if not self.categories[category] then
        self.categories[category] = {}
    end

    self.categories[category][container] = (self.categories[category][container] or 0) + 1
end

function SharedChatSystem:StartNewChatNotification()
    local platformSettings = self.platformSettings
    self.newChatFadeAnim:PingPong(platformSettings.initialFadeAlpha, platformSettings.finalFadeAlpha, platformSettings.fadeTransitionTime, platformSettings.numBlinks)
end

function SharedChatSystem:UnregisterFromCategory(container, category)
    local categories = self.categories[category]
    if categories then
        if categories[container] then
            if categories[container] == 1 then
                categories[container] = nil
            else
                categories[container] = categories[container] - 1
            end

            if not next(categories) then
                self.categories[category] = nil
            end
        end
    end
end

function SharedChatSystem:HandleNewTargetOnChannel(targetChannel, target)
    if not self.targets[targetChannel] then
        self.targets[targetChannel] = ChannelTarget:New()
    end
    self.targets[targetChannel]:AddTarget(zo_strformat(SI_UNIT_NAME, target))
end

function SharedChatSystem:OnFormattedChatMessage(message, category, targetChannel, fromDisplayName, rawMessageText, narrationMessage, overrideColorDef)
    local containers = self.categories[category]
    if containers then
        for container, _ in pairs(containers) do
            container:AddEventMessageToContainer(message, category, narrationMessage, overrideColorDef)
        end
    end
end

function SharedChatSystem:UpdateContainerIndices(start)
    for i=start, #self.containers do
        self.containers[i].id = i
    end
end

function SharedChatSystem:DestroyContainer(container)
    --invalid state, can't destroy a container with windows
    assert(#container.windows == 0)
    --invalid state, primary containers should never be destroyed
    assert(not container:IsPrimary())

    if not self.suppressSave then
        RemoveChatContainer(container.id)
    end
    table.remove(self.containers, container.id)
    self:RemoveSavedContainer(container)
    self:UpdateContainerIndices(container.id)
    self.containerPool:ReleaseObject(container.key)
end

function SharedChatSystem:RemoveSavedContainer(container)
    -- Overridden if applicable
end

function SharedChatSystem:TransferWindow(window, previousContainer, targetContainer)
    local container = targetContainer or self:CreateChatContainer()

    self.isTransferring = true
    local tabIndex = window.tab.index
    local newTabIndex = container:TakeWindow(window, previousContainer)

    if not self.suppressSave then
        TransferChatContainerTab(previousContainer.id, tabIndex, container.id, newTabIndex)
    end

    container:FinalizeWindowTransfer(window)

    self.isTransferring = false
end

function SharedChatSystem:OnRawWindowCreated(container, name, isCombatLog)
    self.hasCombatLog = self.hasCombatLog or isCombatLog
    if not self.suppressSave and not self.isTransferring then
        AddChatContainerTab(container.id, name, isCombatLog)
    end
end

function SharedChatSystem:OnRawWindowDestroyed(container, tabIndex)
    if not self.suppressSave then
        RemoveChatContainerTab(container.id, tabIndex)
    end
end

function SharedChatSystem:AddCombatLog(name)
    if not self.hasCombatLog then
        return self.primaryContainer:AddCombatWindow(name)
    end
    return false
end

function SharedChatSystem:SetCombatLogObject(combatLogObject)
    self.combatLogObject = combatLogObject
end

function SharedChatSystem:PrepareContainersTabDrop(initiator, controlToMonitor)
    for i, container in ipairs(self.containers) do
        if container ~= initiator then
            container:PrepareTabDrop(controlToMonitor)
        end
    end
end

function SharedChatSystem:StopContainersTabDrop(initiator)
    local tabDropContainer
    for i, container in ipairs(self.containers) do
        if container ~= initiator then
            if not tabDropContainer and container:CanTakeTabDrop() then
                tabDropContainer = container
            end
            container:StopTabDrop()
        end
    end

    if tabDropContainer then
        --It's always the first window since that's the only draggable tab
        initiator:TransferWindow(1, tabDropContainer)
    end
end

function SharedChatSystem:SetAllowMultipleContainers(allow)
    self.allowMultipleContainers = allow
end

function SharedChatSystem:MultipleContainersAllowed()
    return self.allowMultipleContainers
end

function SharedChatSystem:CreateChatContainer(container)
    if not container then
        local key
        container, key = self.containerPool:AcquireObject()
        container.key = key
        container.control:SetHidden(false)
    end

    local id = #self.containers + 1
    self.containers[id] = container
    container.id = id

    if not self.suppressSave then
        AddChatContainer()
    end
    container:LoadSettings(self.sv and self.sv.containers[id])

    return container
end

function SharedChatSystem:ResetContainerPositionAndSize(container)
    -- Should be overridden
end

function SharedChatSystem:HandleTryInsertLink(link)
    return self.textEntry:InsertLink(link)
end

function SharedChatSystem:IsTextEntryOpen()
    return self.textEntry:IsOpen()
end

function SharedChatSystem:ValidateChatChannel()
    --Check the requirement again, just incase something has changed since the switch was activated
    if self.channelRequirement and not self.channelRequirement(self.currentChannel) then
        --if it isn't valid, try to revert to the last valid channel
        local lastChannelData = self.channelData[self.lastValidChannel]
        if not lastChannelData.requires or lastChannelData.requires(lastChannelData.id) then
            self:SetChannel(self.lastValidChannel, self.lastValidTarget)
        else
            --if that doesn't work, just revert to default chat channel
            local defaultChatChannel = self:GetDefaultChatChannel()
            self:SetChannel(defaultChatChannel)
            self.lastValidChannel = defaultChatChannel
            self.lastValidTarget = nil
        end

        if self.requirementErrorMessage then
            if type(self.requirementErrorMessage) == "string" then
                CHAT_ROUTER:AddSystemMessage(self.requirementErrorMessage)
            elseif type(self.requirementErrorMessage) == "function" then
                CHAT_ROUTER:AddSystemMessage(self.requirementErrorMessage())
            end
        end

        return false
    else
        --even if the chat channel is still valid, its name may have changed
        local channelData = self.channelData[self.currentChannel]
        if channelData then
            --if it did change, don't send this
            if self.textEntry:SetChannel(channelData, self.currentTarget) then
                return false
            end
        end
    end

    return true --appears to be valid
end

function SharedChatSystem:SubmitTextEntry()
    local text = self.textEntry:GetText()
    self.textEntry:Close()

    if IsChatSystemAvailableForCurrentPlatform() and #text > 0 then
        self.textEntry:AddCommandHistory(text)

        local switch, valid, switchArg, deferredError = self:TextToSwitchData(text, false)
        if switch and valid then
            self:SetChannel(switch.id, switchArg)
            if deferredError then
                -- Validate immediately to run the custom error
                self.requirementErrorMessage = switch.requirementErrorMessage
                self:ValidateChatChannel()
            else
                self.requirementErrorMessage = nil
            end
        else
            local prefix = text:byte(1)
            if self.commandPrefixes[prefix] then
                self.commandPrefixes[prefix](text)
            else
                if self:ValidateChatChannel() then
                    ZO_Menu_SetLastCommandWasFromMenu(false)
                    SendChatMessage(text, self.currentChannel, self.currentTarget)
                end
            end
        end
    else
        self:ValidateChatChannel()
    end
end

function SharedChatSystem:CloseTextEntry(keepText)
    self.textEntry:Close(keepText)

    if self.shouldMinimizeAfterEntry then
        self:Minimize()
    end
end

function SharedChatSystem:OnAutoCompleteEntrySelected(target)
    if self.pendingChannel then
        self:SetChannel(self.pendingChannel, target)
    end
end

function SharedChatSystem:ValidateTargetName(name)
    return (name:match("^%S+$") ~= nil)
end

function SharedChatSystem:ValidateSwitch(switch, text, firstSpaceStart, inferTargetEnd)
    if switch then
        if switch.target then
            -- Either get the arguments from the text or from an channel target
            local switchArg
            local finalSpace
            if switch.target == true then
                if not firstSpaceStart then
                    -- No space means we can't have a target specified
                    return false
                end

                self.pendingChannel = switch.id
                --No channel, just a target requirement
                local doAutoComplete = true
                local secondWordStart = firstSpaceStart + 1
                local isDisplayName = text:byte(secondWordStart) == DISPLAY_NAME_PREFIX_BYTE
                if inferTargetEnd then
                    -- Don't look for a second space if the target begins with a display name flag char
                    if isDisplayName then
                        local secondSpaceStart, secondSpaceEnd = zo_strfind(text, " ", secondWordStart, true)
                        if secondSpaceStart and secondSpaceStart > 1 then
                            finalSpace = secondSpaceStart
                            if self:ValidateTargetName(zo_strsub(text, secondWordStart + 1, secondSpaceStart - 1)) then
                                switchArg = zo_strsub(text, secondWordStart, secondSpaceStart - 1)
                                doAutoComplete = false
                            end
                        end
                    else
                        -- Look for the comma as the final delimiter (character names can have multiple spaces)
                        local commaStart, commaEnd = zo_strfind(text, ",", secondWordStart + 1, true)
                        if commaStart and commaStart > 1 then
                            finalSpace = commaEnd
                            switchArg = zo_strsub(text, secondWordStart, commaStart - 1)
                            doAutoComplete = false
                        end
                    end
                else
                    local potentialSwitchArg = zo_strsub(text, secondWordStart)
                    if potentialSwitchArg ~= "" then
                        if isDisplayName and not self:ValidateTargetName(zo_strsub(potentialSwitchArg, 2)) then
                            potentialSwitchArg = nil
                        end

                        if potentialSwitchArg ~= nil then
                            finalSpace = zo_strlen(text)
                            switchArg = potentialSwitchArg
                            doAutoComplete = false
                        end
                    end
                end

                if doAutoComplete then
                    self.textEntry:AutoCompleteTarget(zo_strsub(text, secondWordStart))
                end
            elseif self.targets[switch.target] then
                --Channel target
                switchArg = self.targets[switch.target]:GetLastTarget()
            end

            if switchArg and switchArg ~= "" then
                --Valid, Channel Argument, No errors, Override the beginning of the switch
                return true, switchArg, false, finalSpace
            end
        elseif switch.requires then
            if switch.requires(switch.id) then
                --Valid, no args, no errors
                return true
            else
                --Only valid if the requirement is getting deferred, no args, deferred if the channel defers the error
                return switch.deferRequirement, nil, switch.deferRequirement
            end
        else
            --No requirements to meet, the switch is valid
            return true
        end
    end

    --No valid switch
    return false
end

function SharedChatSystem:TextToSwitchData(text, inferTargetEnd)
    local lowerText = text:lower()
    local switch = self.switchLookup[lowerText]
    local isValid = false
    local switchArg = nil
    local deferredError = nil
    local spaceStart = nil
    if not switch then
        spaceStart = zo_strfind(lowerText, " ", 1, true)
        if spaceStart and spaceStart > 1 then
            local potentialSwitch = zo_strsub(lowerText, 1, spaceStart - 1)
            switch = self.switchLookup[potentialSwitch:lower()]
        end
    end

    if switch then
        local spaceStartOverride = nil
        isValid, switchArg, deferredError, spaceStartOverride = self:ValidateSwitch(switch, text, spaceStart, inferTargetEnd)
        spaceStart = spaceStartOverride or spaceStart
    end

    return switch, isValid, switchArg, deferredError, spaceStart
end

function SharedChatSystem:OnTextEntryChanged(newText)
    if self.ignoreTextEntryChangedEvent then return end
    self.ignoreTextEntryChangedEvent = true

    local switch, valid, switchArg, deferredError, spaceStart = self:TextToSwitchData(newText, true)

    if spaceStart and spaceStart > 1 then
        if switch and valid then
            if deferredError then
                self.requirementErrorMessage = switch.requirementErrorMessage
            else
                self.requirementErrorMessage = nil
            end

            self:SetChannel(switch.id, switchArg)

            local oldCursorPos = self.textEntry:GetCursorPosition()

            self.textEntry:SetText(zo_strsub(newText, spaceStart + 1))
            self.textEntry:SetCursorPosition(oldCursorPos - spaceStart)
        end
    else
        if self.suppressAutoCompleteClear then
            self.suppressAutoCompleteClear = false
        else
            self.textEntry:CloseAutoComplete()
        end
    end

    self.ignoreTextEntryChangedEvent = false
end

function SharedChatSystem:FindNextTargetForCurrentChannel()
    if self.targets[self.currentChannel] then
        local target = self.targets[self.currentChannel]:GetNextTarget()
        if target and target ~= "" then
            self:SetChannel(self.currentChannel, target)
        end
    end
end

function SharedChatSystem:FindPreviousTargetForCurrentChannel()
    if self.targets[self.currentChannel] then
        local target = self.targets[self.currentChannel]:GetPreviousTarget()
        if target and target ~= "" then
            self:SetChannel(self.currentChannel, target)
        end
    end
end

STUB_SETTING_KEEP_MINIMIZED = false

ZO_CHAT_BLOCKING_SCENE_NAMES =
{
    ["codeRedemptionGamepad"] = true,
    ["gamepad_endeavor_seal_market_pre_scene"] = true,
    ["gamepad_market"] = true,
    ["gamepad_market_pre_scene"] = true,
    ["gamepad_market_preview"] = true,
}

function SharedChatSystem:ShouldTextEntryBeBlocked()
    local currentSceneName = SCENE_MANAGER:GetCurrentSceneName()
    local nextScene = SCENE_MANAGER:GetNextScene()
    local nextSceneName
    if nextScene then
        nextSceneName = nextScene:GetName()
    end        

    if (currentSceneName and ZO_CHAT_BLOCKING_SCENE_NAMES[currentSceneName]) or
        (nextSceneName and ZO_CHAT_BLOCKING_SCENE_NAMES[nextSceneName]) then
        return true
    end

    return false
end

function SharedChatSystem:StartTextEntry(text, channel, target, dontShowHUDWindow)
    -- Don't allow text entry to start if the ingame gui is hidden. This fixes an issue where users could no longer enter text if "]" or "/" were pressed while the ingame gui was hidden.
    -- Also prevent text entry when the game doesn't have focus. This fixes an issue where the chat window gets into a bad state since edit controls can't be focused when the game isn't focused.
    if GetGuiHidden("ingame") or not DoesGameHaveFocus() then
        return
    end

    if IsPlayerActivated() then
        if self:ShouldTextEntryBeBlocked() then
            return
        end

        if not self.currentChannel or channel then
            self:SetChannel(channel or self:GetDefaultChatChannel(), target)
        end

        self.textEntry:Open(text)

        if not dontShowHUDWindow then
            if self.isMinimized then
                self:Maximize()
                self.shouldMinimizeAfterEntry = STUB_SETTING_KEEP_MINIMIZED
            else
                self.primaryContainer:FadeIn()
                self.shouldMinimizeAfterEntry = false
            end
        end
    end
end

function SharedChatSystem:AutoSendTextEntry(text, channel, target, dontShowHUDWindow)
    self:StartTextEntry(text, channel, target, dontShowHUDWindow)
    self:SubmitTextEntry()
end

function SharedChatSystem:ReplyToLastTarget(channelType)
    if self.targets[channelType] then
        local target = self.targets[channelType]:GetLastTarget()
        if target and target ~= "" then
            self:StartTextEntry("", channelType, target)
        end
    end
end

function SharedChatSystem:SetChannelInternal(newChannel, channelTarget)
    local channelData = self.channelData[newChannel]

    if channelData and (newChannel ~= self.currentChannel or channelTarget ~= self.currentTarget) then
        self.lastValidChannel = self.currentChannel
        self.lastValidTarget = self.currentTarget

        self.channelRequirement = channelData.requires
        self.currentChannel = newChannel
        self.currentTarget = channelTarget

        self:UpdateTextEntryChannel()
    end
end

function SharedChatSystem:SetChannel(newChannel, channelTarget)
    if self.currentChannel then
        if self.textEntry:IsOpen() then
            self.textEntry:GetEditControl():TakeFocus()
        else
            self.textEntry:Close()
        end
    end

    newChannel = newChannel or self:GetDefaultChatChannel()
    local channelData = self.channelData[newChannel]
    CHAT_ROUTER:SetCurrentChannelData(channelData, channelTarget)

    --Check for trial limitations
    GetTrialChatIsRestrictedAndWarn(newChannel, channelTarget)
end

function SharedChatSystem:GetCurrentChannelData()
    if not self.currentChannel then
        self:SetChannel(self:GetDefaultChatChannel())
    end
    local channelData = self.channelData[self.currentChannel]
    return channelData, self.currentTarget
end

function SharedChatSystem:SetContainerExtents(minWidth, maxWidth, minHeight, maxHeight)
    self.minContainerWidth = minWidth
    self.maxContainerWidth = maxWidth
    self.minContainerHeight = minHeight
    self.maxContainerHeight = maxHeight
end

function SharedChatSystem:UpdateTextEntryChannel()
    local channelData = self.channelData[self.currentChannel]
    if channelData then
        self.textEntry:SetChannel(channelData, self.currentTarget)
        self.textEntry:SetColor(ZO_ChatSystem_GetCategoryColorFromChannel(self.currentChannel))
    end
end

function SharedChatSystem:AddCommandPrefix(prefixCharacter, callback)
    local characterByte = (prefixCharacter or ""):byte(1)
    if characterByte then
        self.commandPrefixes[characterByte] = callback
    end
end

-- playerName is a decorated display name or a formatted character name
-- rawName is the unformatted/undecorated version of playerName
function SharedChatSystem:ShowPlayerContextMenu(playerName, rawName)
    ClearMenu()

    -- Add to/Remove from Group
    if IsGroupModificationAvailable() then
        local localPlayerIsGrouped = IsUnitGrouped("player")
        local localPlayerIsGroupLeader = IsUnitGroupLeader("player")
        local otherPlayerIsInPlayersGroup = IsPlayerInGroup(rawName)
        if not localPlayerIsGrouped or (localPlayerIsGroupLeader and not otherPlayerIsInPlayersGroup) then
            AddMenuItem(GetString(SI_CHAT_PLAYER_CONTEXT_ADD_GROUP), function()
            local SENT_FROM_CHAT = false
            local DISPLAY_INVITED_MESSAGE = true
            TryGroupInviteByName(playerName, SENT_FROM_CHAT, DISPLAY_INVITED_MESSAGE) end)
        elseif otherPlayerIsInPlayersGroup and localPlayerIsGroupLeader then
            AddMenuItem(GetString(SI_CHAT_PLAYER_CONTEXT_REMOVE_GROUP), function() GroupKickByName(rawName) end)
        end
    end

    -- Whisper
    AddMenuItem(GetString(SI_CHAT_PLAYER_CONTEXT_WHISPER), function() self:StartTextEntry(nil, CHAT_CHANNEL_WHISPER, playerName) end)

    -- Ignore
    local function IgnoreSelectedPlayer()
        ZO_PlatformIgnorePlayer(playerName)
    end

    if not IsIgnored(playerName) then
        AddMenuItem(GetString(SI_CHAT_PLAYER_CONTEXT_ADD_IGNORE), IgnoreSelectedPlayer)
    end

    -- Add Friend
    if not IsFriend(playerName) then
        AddMenuItem(GetString(SI_CHAT_PLAYER_CONTEXT_ADD_FRIEND), function() ZO_Dialogs_ShowDialog("REQUEST_FRIEND", { name = playerName }) end)
    end

    -- Report player
    AddMenuItem(zo_strformat(SI_CHAT_PLAYER_CONTEXT_REPORT, rawName), function()
        ZO_HELP_GENERIC_TICKET_SUBMISSION_MANAGER:OpenReportPlayerTicketScene(playerName)
    end)

    if ZO_Menu_GetNumMenuItems() > 0 then
        ShowMenu()
    end
end

function SharedChatSystem:OnLinkClicked(link, button, text, color, linkType, ...)
    if linkType == CHARACTER_LINK_TYPE then
        local rawName = select(1, ...)
        local characterName = zo_strformat(SI_UNIT_NAME, rawName)
        if characterName ~= GetUnitName("player") then
            if button == MOUSE_BUTTON_INDEX_LEFT then
                IgnoreMouseDownEditFocusLoss()
                self:StartTextEntry(nil, CHAT_CHANNEL_WHISPER, characterName)
            elseif button == MOUSE_BUTTON_INDEX_RIGHT then
                self:ShowPlayerContextMenu(characterName, rawName)
            end
        end
        return true
    elseif linkType == DISPLAY_NAME_LINK_TYPE then
        local displayName = ...
        local decoratedDisplayName = zo_strformat("<<1>>", DecorateDisplayName(displayName))
        if decoratedDisplayName ~= GetDisplayName() then
            if button == MOUSE_BUTTON_INDEX_LEFT then
                IgnoreMouseDownEditFocusLoss()
                self:StartTextEntry(nil, CHAT_CHANNEL_WHISPER, decoratedDisplayName)
            elseif button == MOUSE_BUTTON_INDEX_RIGHT then
                self:ShowPlayerContextMenu(decoratedDisplayName, displayName)
            end
        end
        return true
    elseif linkType == CHANNEL_LINK_TYPE then
        local channelName = ...
        local channelId = GetChatChannelId(channelName)
        if button == MOUSE_BUTTON_INDEX_LEFT then
            IgnoreMouseDownEditFocusLoss()
            self:StartTextEntry(nil, channelId)
        end
        return true
    end
end

function SharedChatSystem:CreateNewChatTab(container)
    local newTab = container:AddWindow(GetString(SI_CHAT_DIALOG_NEW_TAB))
    container:ShowOptions(newTab.tab.index)
end

function SharedChatSystem:SetTextEntryFont(font)
    self.textEntry:SetFont(font)
end

function SharedChatSystem:OnChatCategoryColorChanged(categoryId, red, green, blue)
    -- Update text entry
    local textEntryCategoryId = MultiLevelEventToCategoryMappings[EVENT_CHAT_MESSAGE_CHANNEL][self.currentChannel]
    if textEntryCategoryId == categoryId then
        self.textEntry:SetColor(red, green, blue)
    end

    -- Update chat buffers
    for i=1, #self.containers do
        self.containers[i]:SetBufferColor(categoryId, red, green, blue)
    end
end

function SharedChatSystem:SetFontSize(fontSize)
    for containerIndex=1, #self.containers do
        local container = self.containers[containerIndex]
        for tabIndex = 1, #container.windows do
            container:SetFontSize(tabIndex, fontSize)
        end
    end

    local textEntryFont = self:GetTextEntryFontString(fontSize)
    self:SetTextEntryFont(textEntryFont)
end

function SharedChatSystem:ResetFontSizeToDefault()
    ResetChatFontSizeToDefault()
    local fontSize = self:GetFontSizeFromSetting()
    self:SetFontSize(fontSize)
end

function SharedChatSystem:SetMinAlpha(minAlpha)
    for containerIndex=1, #self.containers do
        local container = self.containers[containerIndex]
        container:SetMinAlpha(minAlpha)
    end
end

function SharedChatSystem:GetMinAlpha()
    return self.primaryContainer:GetMinAlpha()
end

function SharedChatSystem:ResetMinAlphaToDefault()
    for containerIndex=1, #self.containers do
        local container = self.containers[containerIndex]
        container:ResetMinAlphaToDefault()
    end
end

function SharedChatSystem:ShowTextEntryMenu()
    ClearMenu()

    --Prepare switches for sorting
    local switches = {}
    for channel in pairs(self.channelData) do
        local switch = self.switchLookup[channel]
        if switch then
            switches[#switches + 1] = switch
        end
    end

    table.sort(switches)

    --Display sorted switches
    local isCommunicationRestricted = IsCommunicationRestrictedAccount() and GetSetting_Bool(SETTING_TYPE_UI, UI_SETTING_RESTRICTED_COMMUNICATION)
    for i = 1, #switches do
        local switch = switches[i]
        local data = self.switchLookup[switch]
        if data
            and (not data.requires or data.requires(data.id))
            and not IsChannelCategoryCommunicationRestricted(GetChannelCategoryFromChannel(data.id)) then
            local r, g, b = ZO_ChatSystem_GetCategoryColorFromChannel(data.id)
            local itemColor = ZO_ColorDef:New(r, g, b)

            if data.target then
                AddMenuItem(switch, function() self:StartTextEntry(switch .. " ") end, nil, nil, itemColor)
            else
                AddMenuItem(switch, function() self:StartTextEntry(self.textEntry:GetText(), data.id) end, nil, nil, itemColor)
            end
        end
    end

    ShowMenu(self.textEntry:GetControl())
end

function SharedChatSystem:IsAutoCompleteOpen()
    return self.textEntry:IsAutoCompleteOpen()
end

function SharedChatSystem:CloseAutoComplete()
    self.textEntry:CloseAutoComplete()
end

function SharedChatSystem:GetEditControl()
    return self.textEntry:GetEditControl()
end

function SharedChatSystem:HasUnreadMail()
    return self.numUnreadMails ~= 0
end

function SharedChatSystem:OnNumUnreadMailChanged(numUnread)
    if numUnread > self.numUnreadMails and IsPlayerActivated() then
        PlaySound(SOUNDS.NEW_MAIL)
    end
    self.numUnreadMails = numUnread
end

function SharedChatSystem:OnAgentChatUpdated()
    local isActive = IsAgentChatActive()

    if isActive ~= self.isAgentChatActive then
        self.isAgentChatActive = isActive

        self:OnAgentChatActiveChanged(isActive)
    end
end

function SharedChatSystem:GetTextEntryFontString(fontSize)
    local face, _, options = self:GetFont():GetFontInfo()
    local fontSizeString = self:GetFontSizeString(fontSize)
    if options ~= "" then
        return ("%s|%s|%s"):format(face, fontSizeString, options)
    end
    return ("%s|%s"):format(face, fontSizeString)
end

function SharedChatSystem:OnPlayerActivated()
    -- Overridden if applicable
end

function SharedChatSystem:OnAgentChatActiveChanged()
    -- Overridden if applicable
end

function SharedChatSystem:OnNumOnlineFriendsChanged()
    -- Overridden if applicable
end

function SharedChatSystem:Minimize()
    -- Should be overridden
end

function SharedChatSystem:Maximize()
    -- Should be overridden
end

function SharedChatSystem:IsMinimized()
    return self.isMinimized
end

function SharedChatSystem:SetupFonts()
    -- intended to be overriden in subclasses if necessary
end

function SharedChatSystem:GetFont()
    -- Should be overridden
end

function SharedChatSystem:GetFontSizeString()
    -- Should be overridden
end

function SharedChatSystem:GetFontSizeFromSetting()
    -- Should be overridden
end

function SharedChatSystem:ShouldOnlyShowOnHUD()
    -- Should be overridden
    return false
end

function SharedChatSystem:IsHidden()
    -- Should be overridden
    return true
end

function SharedChatSystem:OnUiInterfaceSettingChanged(settingId)
    self:SetChannel(self:GetDefaultChatChannel())
end

function SharedChatSystem:RefreshVisibility()
    self.control:SetHidden(self:IsHidden())
end

function SharedChatSystem:ResetChat()
    ResetChatToDefaults()
    ReloadUI("ingame")
end

function StartChatInput(text, channel, target)
    local isRestrictedCommunicationPermitted = true
    if target ~= nil and IsCommunicationRestricted() then
        isRestrictedCommunicationPermitted = CanCommunicateWith(target)
    end
    if IsChatSystemAvailableForCurrentPlatform() and isRestrictedCommunicationPermitted then
        ZO_GetChatSystem():StartTextEntry(text, channel, target)
    end
end

function AutoSendChatInput(text, channel, target, dontShowHUDWindow)
    if IsChatSystemAvailableForCurrentPlatform() then
        ZO_GetChatSystem():AutoSendTextEntry(text, channel, target, dontShowHUDWindow)
    end
end

function ChatReplyToLastWhisper()
    if IsChatSystemAvailableForCurrentPlatform() then
        ZO_GetChatSystem():ReplyToLastTarget(CHAT_CHANNEL_WHISPER)
    end
end

function ZO_ChatSystem_SubmitChat()
    if IsChatSystemAvailableForCurrentPlatform() then
        ZO_GetChatSystem():SubmitTextEntry()
    end
end

function ZO_ChatSystem_CancelChat()
    if IsChatSystemAvailableForCurrentPlatform() then
        local KEEP_TEXT = true
        ZO_GetChatSystem():CloseTextEntry(KEEP_TEXT)
    end
end

function ZO_ChatSystem_ExitChat()
    if IsChatSystemAvailableForCurrentPlatform() then
        ZO_GetChatSystem():CloseTextEntry()
    end
end

function ZO_ChatSystem_OnMouseWheel(control, delta, ctrl, alt, shift)
    local container = control.container
    if container then
        if shift then
            delta = delta * (container.currentBuffer and container.currentBuffer:GetNumVisibleLines() or 0)
        elseif ctrl then
            delta = delta * (container.currentBuffer and container.currentBuffer:GetNumHistoryLines() or 0)
        end
        container:ScrollByOffset(delta)
    end
end

function ZO_ChatSystem_SetScroll(control, value)
    if control.container then
        control.container:SetScroll(value)
    end
end

function ZO_ChatSystem_ScrollByOffset(control, offset)
    if control.container then
        control.container:ScrollByOffset(offset)
    end
end

function ZO_ChatSystem_ScrollToBottom(control)
    if control.container then
        control.container:ScrollToBottom()
    end
end

function ZO_ChatSystem_OnDragStart(control)
    if control.container then
        control.container:StartDraggingTab(control.index)
    end
end

function ZO_ChatSystem_OnDragStop(control)
    if control.container then
        control.container:StopDraggingTab()
    end
end

function ZO_ChatSystem_OnResizeStart(control)
    if control.container then
        control.container:OnResizeStart()
    end
end

function ZO_ChatSystem_OnResizeStop(control)
    if control.container then
        control.container:OnResizeStop()
    end
end

function ZO_ChatSystem_OnMoveStop(control)
    if control.container then
        control.container:OnMoveStop()
    end
end

function ZO_ChatSystem_OnMouseEnter(control)
    -- ESO-830211: Live players were hitting this, though as far as the code seems it should be impossible
    -- However they're getting into this situation, this will prevent the error
    if control.container then
        control.container:OnMouseEnter()
    end
end

function ZO_ChatTextEntry_PreviousCommand(control)
    if not control.system:IsAutoCompleteOpen() then
        control.owner:PreviousCommand()
    end
end

function ZO_ChatTextEntry_NextCommand(control)
    if not control.system:IsAutoCompleteOpen() then
        control.owner:NextCommand()
    end
end

function ZO_ChatTextEntry_TextChanged(control, newText)
    control.system:OnTextEntryChanged(newText)
end

function ZO_ChatTextEntry_Tab(control)
    if not control.system:IsAutoCompleteOpen() then
        if IsShiftKeyDown() then
            control.system:FindPreviousTargetForCurrentChannel()
        else
            control.system:FindNextTargetForCurrentChannel()
        end
    end
end

function ZO_ChatTextEntry_Escape(control)
    control.system:CloseTextEntry()
end

function ZO_ChatTextEntry_FocusLost(control)
    local KEEP_TEXT = true
    control.system:CloseTextEntry(KEEP_TEXT)
end

function ZO_ChatTextEntry_Execute(control)
    if control.system:IsAutoCompleteOpen() then
        control.system:CloseAutoComplete()
    else
        control.system:SubmitTextEntry()
    end
end

function ZO_ChannelLabel_MouseUp(control)
    control.system:ShowTextEntryMenu()
end
