local DEFAULT_FROM_DISPLAY_NAME = nil
local DEFAULT_RAW_MESSAGE_TEXT = nil
local DEFAULT_TARGET_CHANNEL = nil

local g_pvpKillFeedDeathRecurrenceTracker = nil
do
    -- The PvP Kill Feed uses ZO_RecurrenceTracker to track whether any given
    -- killer/victim message has been shown within the last 10 seconds from a
    -- given source (local vs. kill location). Note that the instance count
    -- tracked by ZO_RecurrenceTracker is irrelevant here for the purpose of
    -- the kill feed.
    local EXPIRATION_MS = 10000 -- 10 seconds
    local EXTENSION_MS = 10000 -- 10 seconds
    g_pvpKillFeedDeathRecurrenceTracker = ZO_RecurrenceTracker:New(EXPIRATION_MS, EXTENSION_MS)
end

local ChannelInfo = ZO_ChatSystem_GetChannelInfo()

local function CreateChannelLink(channelInfo, overrideName)
    if channelInfo.channelLinkable then
        local channelName = overrideName or GetChannelName(channelInfo.id)
        return ZO_LinkHandler_CreateChannelLink(channelName)
    end
end

local function GetCustomerServiceIcon(isCustomerServiceAccount)
    if isCustomerServiceAccount then
        return "|t16:16:EsoUI/Art/ChatWindow/csIcon.dds|t"
    end

    return ""
end

local function ShouldShowSocialErrorInChat(error)
    return not ShouldShowSocialErrorInAlert(error)
end

local function ShouldShowGroupErrorInChat(error)
    return not ShouldShowGroupErrorInAlert(error)
end

-- message formatting events can be keyed off of anything, including strings, but numbers will be assumed to be EVENT_MANAGER events and will automatically be registered.
local BUILTIN_MESSAGE_FORMATTERS = {
    [EVENT_CHAT_MESSAGE_CHANNEL] = function(messageType, fromName, text, isFromCustomerService, fromDisplayName)
        local channelInfo = ChannelInfo[messageType]

        if channelInfo and channelInfo.format then
            local channelLink = CreateChannelLink(channelInfo)

            local userFacingName
            if not IsDecoratedDisplayName(fromName) and fromDisplayName ~= "" then
                --We have a character name and a display name, so follow the setting
                userFacingName = ZO_ShouldPreferUserId() and fromDisplayName or fromName
            else
                --We either have two display names, or we weren't given a guaranteed display name, so just use the default fromName
                userFacingName = fromName
            end

            userFacingName = zo_strformat(SI_CHAT_MESSAGE_PLAYER_FORMATTER, userFacingName)
            local fromLink = channelInfo.playerLinkable and ZO_LinkHandler_CreatePlayerLink(userFacingName) or userFacingName

            if channelInfo.formatMessage then
                text = zo_strformat(SI_CHAT_MESSAGE_FORMATTER, text)
            end

            local channelInfoFormat
            if type(channelInfo.format) == "function" then
                channelInfoFormat = channelInfo.format()
            else
                channelInfoFormat = GetString(channelInfo.format)
            end

            local channelInfoNarrationFormat
            if channelInfo.narrationFormat then
                if type(channelInfo.narrationFormat) == "function" then
                    channelInfoNarrationFormat = channelInfo.narrationFormat()
                else
                    channelInfoNarrationFormat = GetString(channelInfo.narrationFormat)
                end
            end

            -- Channels with links will not have CS messages
            local formattedText
            local formattedNarrationText
            if channelLink then
                formattedText = string.format(channelInfoFormat, channelLink, fromLink, text)
                if channelInfoNarrationFormat then
                    formattedNarrationText = string.format(channelInfoNarrationFormat, channelLink, fromLink, text)
                end
            else
                if channelInfo.supportCSIcon then
                    formattedText = string.format(channelInfoFormat, GetCustomerServiceIcon(isFromCustomerService), fromLink, text)
                    if channelInfoNarrationFormat then
                        formattedNarrationText = string.format(channelInfoNarrationFormat, GetCustomerServiceIcon(isFromCustomerService), fromLink, text)
                    end
                else
                    formattedText = string.format(channelInfoFormat, fromLink, text)
                    if channelInfoNarrationFormat then
                        formattedNarrationText = string.format(channelInfoNarrationFormat, fromLink, text)
                    end
                end
            end

            return formattedText, channelInfo.saveTarget, fromDisplayName, text, formattedNarrationText
        end
    end,

    [EVENT_BROADCAST] = function(message)
        return string.format(GetString(SI_CHAT_MESSAGE_SYSTEM), GetString("SI_CHATCHANNELCATEGORIES", CHAT_CATEGORY_SYSTEM), message)
    end,

    [EVENT_FIXED_BROADCAST] = function(broadcastType, arg1)
        return string.format(GetString(SI_CHAT_MESSAGE_SYSTEM), GetString("SI_CHATCHANNELCATEGORIES", CHAT_CATEGORY_SYSTEM), zo_strformat(GetString("SI_BROADCASTTYPE", broadcastType), arg1))
    end,

    [EVENT_FRIEND_PLAYER_STATUS_CHANGED] = function(displayName, characterName, oldStatus, newStatus)
        local wasOnline = oldStatus ~= PLAYER_STATUS_OFFLINE
        local isOnline = newStatus ~= PLAYER_STATUS_OFFLINE

        if wasOnline ~= isOnline then
            local text
            local displayNameLink = ZO_LinkHandler_CreateDisplayNameLink(displayName)
            local characterNameLink = ZO_LinkHandler_CreateCharacterLink(characterName)
            if isOnline then
                if characterName ~= "" then
                    text = zo_strformat(SI_FRIENDS_LIST_FRIEND_CHARACTER_LOGGED_ON, displayNameLink, characterNameLink)
                else
                    text = zo_strformat(SI_FRIENDS_LIST_FRIEND_LOGGED_ON, displayNameLink)
                end
            else
                if characterName ~= "" then
                    text = zo_strformat(SI_FRIENDS_LIST_FRIEND_CHARACTER_LOGGED_OFF, displayNameLink, characterNameLink)
                else
                    text = zo_strformat(SI_FRIENDS_LIST_FRIEND_LOGGED_OFF, displayNameLink)
                end
            end

            return text, nil, displayName
        end
    end,

    [EVENT_IGNORE_ADDED] = function(displayName)
        local link = ZO_LinkHandler_CreateDisplayNameLink(displayName)
        return zo_strformat(SI_FRIENDS_LIST_IGNORE_ADDED, link), nil, displayName
    end,

    [EVENT_IGNORE_REMOVED] = function(displayName)
        local link = ZO_LinkHandler_CreateDisplayNameLink(displayName)
        return zo_strformat(SI_FRIENDS_LIST_IGNORE_REMOVED, link), nil, displayName
    end,

    [EVENT_GROUP_TYPE_CHANGED] = function(largeGroup)
        if largeGroup then
            return GetString(SI_CHAT_ANNOUNCEMENT_IN_LARGE_GROUP)
        else
            return GetString(SI_CHAT_ANNOUNCEMENT_IN_SMALL_GROUP)
        end
    end,

    [EVENT_GROUP_INVITE_RESPONSE] = function(characterName, response, displayName)
        -- Only one name will be sent here, so use that and do not use special formatting since this appears in chat
        local nameToDisplay
        if characterName ~= "" then
            nameToDisplay = IsConsoleUI() and ZO_FormatUserFacingCharacterName(characterName) or characterName
        else
            nameToDisplay = ZO_FormatUserFacingDisplayName(displayName)
        end

        if not IsGroupErrorIgnoreResponse(response) and ShouldShowGroupErrorInChat(response) then
            local alertMessage = nameToDisplay ~= "" and zo_strformat(GetString("SI_GROUPINVITERESPONSE", response), nameToDisplay) or GetString(SI_PLAYER_BUSY)

            return alertMessage, nil, displayName
        end
    end,

    [EVENT_SOCIAL_ERROR] = function(error)
        if not IsSocialErrorIgnoreResponse(error) and ShouldShowSocialErrorInChat(error) then
            return zo_strformat(GetString("SI_SOCIALACTIONRESULT", error))
        end
    end,

    [EVENT_TRIAL_FEATURE_RESTRICTED] = function(restrictionType)
        if ZO_ChatSystem_GetTrialEventMappings()[restrictionType] then
            return GetString("SI_TRIALACCOUNTRESTRICTIONTYPE", restrictionType)
        end
    end,

    [EVENT_GROUP_MEMBER_LEFT] = function(characterName, reason, isLocalPlayer, isLeader, displayName, actionRequiredVote)
        if reason == GROUP_LEAVE_REASON_KICKED and isLocalPlayer and actionRequiredVote then
            return GetString(SI_GROUP_ELECTION_KICK_PLAYER_PASSED)
        end
    end,

    [EVENT_BATTLEGROUND_INACTIVITY_WARNING] = function()
        return GetString(SI_BATTLEGROUND_INACTIVITY_WARNING)
    end,

    [EVENT_GUILD_KEEP_ATTACK_UPDATE] = function(channel, numGuardsKilled, numAttackers, location)
        if tonumber(GetSetting(SETTING_TYPE_UI, UI_SETTING_SHOW_AVA_NOTIFICATIONS)) ~= AVA_NOTIFICATIONS_SETTING_CHOICE_DONT_SHOW and
            tonumber(GetSetting(SETTING_TYPE_UI, UI_SETTING_SHOW_GUILD_KEEP_NOTICES)) == GUILD_KEEP_NOTICES_SETTING_CHOICE_CHAT then
            local channelInfo = ChannelInfo[channel]

            if channelInfo then
                local text
                if numGuardsKilled > 0 then
                    text = zo_strformat(SI_GUILD_KEEP_ATTACK_UPDATE, numGuardsKilled, location, numAttackers)
                else
                    text = zo_strformat(SI_GUILD_KEEP_ATTACK_END, location)
                end
                local channelInfoFormat = GetString(SI_CHAT_MESSAGE_GUILD_NO_SENDER)
                local channelLink = CreateChannelLink(channelInfo)
                local formattedText = string.format(channelInfoFormat, channelLink, text)

                return formattedText
            end
        end
    end,

    [EVENT_PVP_KILL_FEED_DEATH] = function(killLocation, killerDisplayName, killerCharacterName, killerAlliance, killerRank, victimDisplayName, victimCharacterName, victimAlliance, victimRank, isKillLocation)
        local showKillFeedNotifications = GetSetting_Bool(SETTING_TYPE_UI, UI_SETTING_SHOW_PVP_KILL_FEED_NOTIFICATIONS)
        if not showKillFeedNotifications then
            return nil
        end

        local messageKeySuffix = string.format("%s___%s", killerDisplayName, victimDisplayName)
        local messageKeyLocal = "L" .. messageKeySuffix
        local messageKeyKillLocation = "B" .. messageKeySuffix
        if isKillLocation then
            -- This message was kill location sourced.
            if g_pvpKillFeedDeathRecurrenceTracker:RemoveValue(messageKeyLocal) ~= nil then
                -- The same message was already shown as a result of a local message;
                -- remove the original message from the tracker and suppress this message.
                return nil
            end
            -- Track this kill location sourced message.
            g_pvpKillFeedDeathRecurrenceTracker:AddValue(messageKeyKillLocation)
        else
            -- This message was locally sourced.
            if g_pvpKillFeedDeathRecurrenceTracker:RemoveValue(messageKeyKillLocation) ~= nil then
                -- The same message was already shown as a result of a kill location message;
                -- remove the original message from the tracker and suppress this message.
                return nil
            end
            -- Track this locally sourced message.
            g_pvpKillFeedDeathRecurrenceTracker:AddValue(messageKeyLocal)
        end

        local isBattleground = IsActiveWorldBattleground()
        local killerAllianceColor
        local victimAllianceColor
        if isBattleground then
            killerAllianceColor = GetBattlegroundTeamColor(killerAlliance):GetBright()
            victimAllianceColor = GetBattlegroundTeamColor(victimAlliance):GetBright()
        else
            killerAllianceColor = GetAllianceColor(killerAlliance):GetBright()
            victimAllianceColor = GetAllianceColor(victimAlliance):GetBright()
        end

        local ICON_SIZE = 24
        local killerIcon
        local victimIcon
        if isBattleground then
            killerIcon = ZO_GetBattlegroundIconMarkup(killerAlliance, ICON_SIZE)
            victimIcon = ZO_GetBattlegroundIconMarkup(victimAlliance, ICON_SIZE)
        else
            killerIcon = ZO_GetColoredAvARankIconMarkup(killerRank, killerAlliance, ICON_SIZE)
            victimIcon = ZO_GetColoredAvARankIconMarkup(victimRank, victimAlliance, ICON_SIZE)
        end

        local killerAllianceName
        local victimAllianceName
        if isBattleground then
            killerAllianceName = GetString("SI_BATTLEGROUNDTEAM", killerAlliance)
            victimAllianceName = GetString("SI_BATTLEGROUNDTEAM", victimAlliance)
        else
            killerAllianceName = ZO_CachedStrFormat(SI_ALLIANCE_NAME, GetAllianceName(killerAlliance))
            victimAllianceName = ZO_CachedStrFormat(SI_ALLIANCE_NAME, GetAllianceName(victimAlliance))
        end

        local killerName = ZO_GetPrimaryPlayerName(killerDisplayName, killerCharacterName)
        local victimName = ZO_GetPrimaryPlayerName(victimDisplayName, victimCharacterName)

        local killerGender = GetGenderFromNameDescriptor(killerCharacterName)
        local victimGender = GetGenderFromNameDescriptor(victimCharacterName)

        local killerRankName = GetAvARankName(killerGender, killerRank)
        local victimRankName = GetAvARankName(victimGender, victimRank)

        local hasLocation = killLocation and killLocation ~= ""
        local messageStringId = hasLocation and SI_PVP_KILL_FEED_DEATH_AND_LOCATION or SI_PVP_KILL_FEED_DEATH
        local message = zo_strformat(messageStringId, killerAllianceColor:Colorize(killerName), killerIcon, victimAllianceColor:Colorize(victimName), victimIcon, killLocation)

        local narrationStringId = hasLocation and SI_PVP_KILL_FEED_DEATH_AND_LOCATION_NARRATION or SI_PVP_KILL_FEED_DEATH_NARRATION
        local narrationMessage = zo_strformat(narrationStringId, killerAllianceName, killerRankName, killerName, victimAllianceName, victimRankName, victimName, killLocation)
        return message, DEFAULT_TARGET_CHANNEL, DEFAULT_FROM_DISPLAY_NAME, DEFAULT_RAW_MESSAGE_TEXT, narrationMessage, ZO_WHITE
    end,

    ["AddSystemMessage"] = function(messageText)
        -- System messages will already be formatted by the time they get here
        return messageText
    end,

    ["AddTranscriptMessage"] = function(messageText)
        -- Transcript messages will already be formatted by the time they get here
        return messageText
    end,
}

-----------------
-- Chat Router --
-----------------

--[[
    The chat router's job is to format chat events and route them to the multiple chat subsystems that exist.
    All methods should be safely callable without checking if the chat system is available or not
]]--

local ZO_ChatRouter = ZO_CallbackObject:Subclass()
function ZO_ChatRouter:New(...)
    local object = ZO_CallbackObject.New(self)
    object:Initialize(...)
    return object
end

function ZO_ChatRouter:Initialize()
    if not IsChatSystemAvailableForCurrentPlatform() then
        return
    end

    self.registeredMessageFormatters = {}
    self.hasRegisteredEvent = {}
    for eventCode, messageFormatter in pairs(BUILTIN_MESSAGE_FORMATTERS) do
        self:RegisterMessageFormatter(eventCode, messageFormatter)
    end

    if IsConsoleUI() then
        -- VOICE_CHAT_MANAGER is console only
        local function SetTranscriptForwardingEnabled()
            local enableTranscriptForwarding = GetSetting_Bool(SETTING_TYPE_ACCESSIBILITY, ACCESSIBILITY_SETTING_SEND_TRANSCRIPT_TO_TEXT_CHAT)
            self:SetTranscriptForwardingEnabled(enableTranscriptForwarding)
        end

        EVENT_MANAGER:RegisterForEvent("ChatRouter", EVENT_VOICE_CHAT_ACCESSIBILITY_SETTING_CHANGED, SetTranscriptForwardingEnabled)
        EVENT_MANAGER:RegisterForEvent("ChatRouter", EVENT_FORWARD_TRANSCRIPT_TO_TEXT_CHAT_ACCESSIBILITY_SETTING_CHANGED, SetTranscriptForwardingEnabled)
    end

    local function OnTryInsertLink(...)
        return ZO_GetChatSystem():HandleTryInsertLink(...)
    end
    LINK_HANDLER:RegisterCallback(LINK_HANDLER.INSERT_LINK_EVENT, OnTryInsertLink)

    local function OnLinkClicked(...)
        return ZO_GetChatSystem():OnLinkClicked(...)
    end
    LINK_HANDLER:RegisterCallback(LINK_HANDLER.LINK_CLICKED_EVENT, OnLinkClicked)
    LINK_HANDLER:RegisterCallback(LINK_HANDLER.LINK_MOUSE_UP_EVENT, OnLinkClicked)

    -- FlashTaskbarWindow is a private function: to keep it from tainting the normal event handler we'll register for it seperately
    local function OnChatMessageChannel(_, chatChannel)
        if chatChannel == CHAT_CHANNEL_WHISPER then
            local NUM_FLASHES_BEFORE_SOLID = 7
            FlashTaskbarWindow("WHISPER", NUM_FLASHES_BEFORE_SOLID)
        end
    end
    EVENT_MANAGER:RegisterForEvent("ChatRouterNotification", EVENT_CHAT_MESSAGE_CHANNEL, OnChatMessageChannel)
end

function ZO_ChatRouter:GetRegisteredMessageFormatters()
    return self.registeredMessageFormatters
end

do
    local function OnChatEvent(eventCode, ...)
        CHAT_ROUTER:FormatAndAddChatMessage(eventCode, ...)
    end

    function ZO_ChatRouter:RegisterMessageFormatter(eventKey, messageFormatter)
        if not IsChatSystemAvailableForCurrentPlatform() then
            return
        end

        self.registeredMessageFormatters[eventKey] = messageFormatter

        if type(eventKey) == "number" and not self.hasRegisteredEvent[eventKey] then
            local eventCode = eventKey
            EVENT_MANAGER:RegisterForEvent("ChatRouter", eventCode, OnChatEvent)
            self.hasRegisteredEvent[eventCode] = true
        end
    end
end

do
    local MultiLevelEventToCategoryMappings, SimpleEventToCategoryMappings = ZO_ChatSystem_GetEventCategoryMappings()
    function ZO_ChatRouter:FormatAndAddChatMessage(eventKey, ...)
        if not IsChatSystemAvailableForCurrentPlatform() then
            return
        end

        local eventCategory = nil
        if SimpleEventToCategoryMappings[eventKey] then
            eventCategory = SimpleEventToCategoryMappings[eventKey]
        elseif MultiLevelEventToCategoryMappings[eventKey] then
            local messageType = select(1, ...)
            eventCategory = MultiLevelEventToCategoryMappings[eventKey][messageType]
        end

        local messageFormatter = self.registeredMessageFormatters[eventKey]
        if messageFormatter then
            local formattedEventText, targetChannel, fromDisplayName, rawMessageText, formattedNarrationText, overrideColorDef = messageFormatter(...)
            if formattedEventText then
                if targetChannel then
                    local target = select(2, ...)
                    self:FireCallbacks("TargetAddedToChannel", targetChannel, target)
                end

                self:FireCallbacks("FormattedChatMessage", formattedEventText, eventCategory, targetChannel, fromDisplayName, rawMessageText, formattedNarrationText, overrideColorDef)
            end
        end
    end
end

function ZO_ChatRouter:AddSystemMessage(messageText)
    self:FormatAndAddChatMessage("AddSystemMessage", messageText)
end

function ZO_ChatRouter:AddDebugMessage(messageText)
    self:AddSystemMessage(messageText)
end

function ZO_ChatRouter:AddTranscriptMessage(messageText)
    self:FormatAndAddChatMessage("AddTranscriptMessage", messageText)
end

local function AddTranscriptMessage(...)
    CHAT_ROUTER:AddTranscriptMessage(...) 
end

if IsConsoleUI() then
    -- VOICE_CHAT_MANAGER is console only
    function ZO_ChatRouter:SetTranscriptForwardingEnabled(enabled)
        if enabled then
            VOICE_CHAT_MANAGER:RegisterCallback("VoiceChatTranscript", AddTranscriptMessage)
        else
            VOICE_CHAT_MANAGER:UnregisterCallback("VoiceChatTranscript", AddTranscriptMessage)
        end
    end
end

function ZO_ChatRouter:AddCommandPrefix(prefixCharacter, callback)
    self:FireCallbacks("AddCommandPrefix", prefixCharacter, callback)
end

function ZO_ChatRouter:SetCurrentChannelData(channelData, channelTarget)
    self.currentChannel = channelData
    self.currentTarget = channelTarget
    CALLBACK_MANAGER:FireCallbacks("OnChatChannelUpdated")
end

function ZO_ChatRouter:GetCurrentChannelData()
    return self.currentChannel, self.currentTarget
end

CHAT_ROUTER = ZO_ChatRouter:New()

--- Global functions ---

function ZO_ChatSystem_DoesPlatformUseGamepadChatSystem()
    return IsGamepadUISupported()
end

function ZO_ChatSystem_DoesPlatformUseKeyboardChatSystem()
    return IsKeyboardUISupported()
end

function ZO_ChatSystem_ShouldUseKeyboardChatSystem()
    if not IsKeyboardUISupported() then
        return false
    end

    local useKeyboardChat = GetSetting_Bool(SETTING_TYPE_GAMEPAD, GAMEPAD_SETTING_USE_KEYBOARD_CHAT)
    return IsInGamepadPreferredMode() == false or useKeyboardChat == true
end

function ZO_GetChatSystem()
    if ZO_ChatSystem_ShouldUseKeyboardChatSystem() then
        return SYSTEMS:GetKeyboardObject("ChatSystem")
    else
        return SYSTEMS:GetGamepadObject("ChatSystem")
    end
end