-- TODO: replace with CAMPAIGN_QUEUE_TYPE enum
-- this is used where the API expects a bool isGroup value instead of the enum
CAMPAIGN_QUEUE_INDIVIDUAL = false
CAMPAIGN_QUEUE_GROUP = true

ZO_CAMPAIGN_DATA_TYPE_CAMPAIGN = 1
ZO_CAMPAIGN_DATA_TYPE_QUEUE = 2

ZO_CAMPAIGN_QUEUE_STEP_INITIAL = 1
ZO_CAMPAIGN_QUEUE_STEP_COLLECTIBLE_CHECK = 2
ZO_CAMPAIGN_QUEUE_STEP_QUEUED_AS_GUEST_CHECK = 3
ZO_CAMPAIGN_QUEUE_STEP_ALLIANCE_LOCK_CHECK = 4
ZO_CAMPAIGN_QUEUE_STEP_SELECT_QUEUE_TYPE = 5
ZO_CAMPAIGN_QUEUE_STEP_PERFORM_QUEUE = 6

ZO_CampaignBrowser_Manager = ZO_CallbackObject:Subclass()

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

function ZO_CampaignBrowser_Manager:Initialize()
    self.selectionCampaignList = {}
    self.campaignRulesetsByType = {}
    self:RebuildCampaignData()

    local function OnCampaignSelectionDataChanged()
        self:RebuildCampaignData()
    end
    EVENT_MANAGER:RegisterForEvent("ZO_CampaignBrowserManager", EVENT_CAMPAIGN_SELECTION_DATA_CHANGED, OnCampaignSelectionDataChanged)

    local function OnCampaignQueueChanged(eventCode, campaignId)
        local campaignData = self:GetDataByCampaignId(campaignId)
        if campaignData then
            self:UpdateQueueDataState(campaignData)
        end
    end
    EVENT_MANAGER:RegisterForEvent("ZO_CampaignBrowserManager", EVENT_CAMPAIGN_QUEUE_JOINED, OnCampaignQueueChanged)
    EVENT_MANAGER:RegisterForEvent("ZO_CampaignBrowserManager", EVENT_CAMPAIGN_QUEUE_LEFT, OnCampaignQueueChanged)
    EVENT_MANAGER:RegisterForEvent("ZO_CampaignBrowserManager", EVENT_CAMPAIGN_QUEUE_STATE_CHANGED, OnCampaignQueueChanged)

    local function OnCampaignAllianceLockActivated(_, campaignId, lockedToAlliance)
        local campaignData = self:GetDataByCampaignId(campaignId)
        if campaignData then
            campaignData.lockedToAlliance = lockedToAlliance
        end
    end
    EVENT_MANAGER:RegisterForEvent("ZO_CampaignBrowserManager", EVENT_CAMPAIGN_ALLIANCE_LOCK_ACTIVATED, OnCampaignAllianceLockActivated)
end

do
    local function IsCampaignData(data)
        return data and data.type == ZO_CAMPAIGN_DATA_TYPE_CAMPAIGN
    end

    local function IsQueueData(data)
        return data and data.type == ZO_CAMPAIGN_DATA_TYPE_QUEUE
    end

    function ZO_CampaignBrowser_Manager:CanSetHomeCampaign(data)
        if IsCampaignData(data) then
            return not data.isImperialCityCampaign and GetAssignedCampaignId() ~= data.id and DoesPlayerMeetCampaignRequirements(data.id)
        end
        return false
    end

    function ZO_CampaignBrowser_Manager:DoSetHomeCampaign(data)
        if IsCampaignData(data) then
            if not ZO_CampaignBrowser_DoesPlayerMatchAllianceLock(data) then
                ZO_Dialogs_ShowPlatformDialog("CAMPAIGN_ALLIANCE_LOCKED", { campaignData = data } )
            else
                if IsInGamepadPreferredMode() then
                    local lockTimeLeft = GetCampaignReassignCooldown()
                    if lockTimeLeft > 0 then
                        ZO_Dialogs_ShowGamepadDialog(ZO_GAMEPAD_CAMPAIGN_LOCKED_DIALOG, { id = data.id, campaignData = data } )
                    else
                        ZO_Dialogs_ShowGamepadDialog(ZO_GAMEPAD_CAMPAIGN_SET_HOME_REVIEW_DIALOG, { id = data.id, campaignData = data })
                    end
                else
                    SELECT_HOME_CAMPAIGN_DIALOG:Show(data)
                end
            end
        end
    end

    function ZO_CampaignBrowser_Manager:IsPlayerInSameCampaignType(targetCampaignData)
        local currentCampaignId = GetCurrentCampaignId() 
        if currentCampaignId == targetCampaignData.id then
            return true
        end

        local currentCampaignData = self:GetDataByCampaignId(currentCampaignId) 
        if currentCampaignData == nil then
            return false
        end

        return currentCampaignData.isImperialCityCampaign == targetCampaignData.isImperialCityCampaign
    end

    function ZO_CampaignBrowser_Manager:CanQueueForCampaign(data)
        local canQueueIndividual = false
        local canQueueGroup = false
        if IsCampaignData(data) and not self:IsPlayerInSameCampaignType(data) and not IsQueuedForCampaign(data.id, CAMPAIGN_QUEUE_INDIVIDUAL) and DoesPlayerMeetCampaignRequirements(data.id) then
            if not IsActiveWorldBattleground() and not IsUnitDead("player") then
                canQueueIndividual = true
            end

            if canQueueIndividual and IsUnitGrouped("player") then
                canQueueGroup = true
            end
        end
        return canQueueIndividual, canQueueGroup
    end

    function ZO_CampaignBrowser_Manager:DoQueueForCampaign(data)
        if IsCampaignData(data) then
            local NO_QUEUE_TYPE = nil
            self:ContinueQueueForCampaignFlow(data, ZO_CAMPAIGN_QUEUE_STEP_INITIAL, NO_QUEUE_TYPE)
        end
    end

    function ZO_CampaignBrowser_Manager:ContinueQueueForCampaignFlow(campaignData, lastQueueStepExecuted, queueType)
        -- Queue steps are used to create an asynchronous flow that can be restarted by each individual dialog in the flow.
        -- each dialog knows which step it "comes from", but not which one it "goes to" so we can reorder them without changing each dialog.
        -- To finish a step without needing to stop and restart execution, we can just increment the step.
        local currentQueueStep = lastQueueStepExecuted + 1

        if currentQueueStep == ZO_CAMPAIGN_QUEUE_STEP_COLLECTIBLE_CHECK then
            if campaignData.isImperialCityCampaign then
                local collectibleData = ZO_COLLECTIBLE_DATA_MANAGER:GetCollectibleDataById(GetImperialCityCollectibleId())
                if not collectibleData:IsUnlocked() then
                    local message = GetString(SI_COLLECTIBLE_LOCKED_FAILURE_CAUSED_BY_CAMPAIGN_QUEUE)
                    local marketOperation = MARKET_OPEN_OPERATION_DLC_FAILURE_CAMPAIGN_QUEUE
                    ZO_Dialogs_ShowCollectibleRequirementFailedPlatformDialog(collectibleData, message, marketOperation)
                    return
                end
            end
            currentQueueStep = currentQueueStep + 1
        end

        if currentQueueStep == ZO_CAMPAIGN_QUEUE_STEP_QUEUED_AS_GUEST_CHECK then
            if campaignData.id ~= GetAssignedCampaignId() then
                TUTORIAL_SYSTEM:TriggerTutorialWithDeferredAction(TUTORIAL_TRIGGER_QUEUED_FOR_CAMPAIGN_AS_GUEST, function()
                    self:ContinueQueueForCampaignFlow(campaignData, ZO_CAMPAIGN_QUEUE_STEP_QUEUED_AS_GUEST_CHECK)
                end)
                return
            end
            currentQueueStep = currentQueueStep + 1
        end

        if currentQueueStep == ZO_CAMPAIGN_QUEUE_STEP_ALLIANCE_LOCK_CHECK then
            if not ZO_CampaignBrowser_DoesPlayerMatchAllianceLock(campaignData) then
                ZO_Dialogs_ShowPlatformDialog("CAMPAIGN_ALLIANCE_LOCKED", {campaignData = campaignData})
                return
            elseif ZO_CampaignBrowserDialogs_ShouldShowAllianceLockWarning(campaignData) then
                ZO_Dialogs_ShowPlatformDialog("CAMPAIGN_ABOUT_TO_ALLIANCE_LOCK", {campaignData = campaignData})
                return
            end
            currentQueueStep = currentQueueStep + 1
        end

        if currentQueueStep == ZO_CAMPAIGN_QUEUE_STEP_SELECT_QUEUE_TYPE then
            local canQueueIndividual, canQueueGroup = self:CanQueueForCampaign(campaignData)
            if canQueueIndividual and canQueueGroup then
                if IsInGamepadPreferredMode() then
                    local campaignRulesetTypeString = GetString("SI_CAMPAIGNRULESETTYPE", campaignData.rulesetType)
                    ZO_Dialogs_ShowGamepadDialog(ZO_GAMEPAD_CAMPAIGN_QUEUE_DIALOG, {campaignData = campaignData}, {mainTextParams = {campaignRulesetTypeString, campaignData.name}})
                    return
                else
                    ZO_Dialogs_ShowDialog("CAMPAIGN_QUEUE", {campaignData = campaignData})
                    return
                end
            elseif canQueueIndividual then
                queueType = CAMPAIGN_QUEUE_TYPE_INDIVIDUAL
            elseif canQueueGroup then
                queueType = CAMPAIGN_QUEUE_TYPE_GROUP
            end
            currentQueueStep = currentQueueStep + 1
        end

        if currentQueueStep == ZO_CAMPAIGN_QUEUE_STEP_PERFORM_QUEUE then
            internalassert(queueType ~= nil, "Campaign queue flow completed without picking a queue type")
            local isGroup = queueType == CAMPAIGN_QUEUE_TYPE_GROUP
            QueueForCampaign(campaignData.id, isGroup)
            return
        end
    end

    function ZO_CampaignBrowser_Manager:CanLeaveCampaignQueue(data)
        if IsCampaignData(data) then
            local queueData = data.queue
            return CanLeaveCampaignQueue(queueData.id, queueData.isGroup) == LEAVE_CAMPAIGN_QUEUE_ERROR_NONE
        end
        return false
    end

    function ZO_CampaignBrowser_Manager:DoLeaveCampaignQueue(data)
        if IsCampaignData(data) then
            local queueData = data.queue
            if IsQueuedForCampaign(queueData.id, queueData.isGroup) then
                if queueData.state == CAMPAIGN_QUEUE_REQUEST_STATE_WAITING then
                    LeaveCampaignQueue(queueData.id, queueData.isGroup)
                elseif queueData.state == CAMPAIGN_QUEUE_REQUEST_STATE_CONFIRMING then
                    ConfirmCampaignEntry(queueData.id, queueData.isGroup, false)
                end
            end
        end
    end
end

do
    local function CompareRulesetsByDuration(rulesetIdA, rulesetIdB)
        return (GetCampaignRulesetDurationInSeconds(rulesetIdA) > GetCampaignRulesetDurationInSeconds(rulesetIdB))
    end

    function ZO_CampaignBrowser_Manager:RebuildCampaignData()
        -- Get Campaign Data
        ZO_ClearNumericallyIndexedTable(self.selectionCampaignList)

        for selectionIndex = 1, GetNumSelectionCampaigns() do
            local campaignId = GetSelectionCampaignId(selectionIndex)
            local rulesetId = GetCampaignRulesetId(campaignId)

            local campaignData = {}

            campaignData.name = ZO_CachedStrFormat(SI_CAMPAIGN_NAME, GetCampaignName(campaignId))
            campaignData.type = ZO_CAMPAIGN_DATA_TYPE_CAMPAIGN
            campaignData.id = campaignId
            campaignData.selectionIndex = selectionIndex
            campaignData.rulesetId = rulesetId
            campaignData.rulesetType = GetCampaignRulesetType(rulesetId)

            campaignData.alliancePopulation1 = GetSelectionCampaignPopulationData(selectionIndex, 1)
            campaignData.alliancePopulation2 = GetSelectionCampaignPopulationData(selectionIndex, 2)
            campaignData.alliancePopulation3 = GetSelectionCampaignPopulationData(selectionIndex, 3)

            campaignData.numGroupMembers = GetNumSelectionCampaignGroupMembers(selectionIndex)
            campaignData.numFriends = GetNumSelectionCampaignFriends(selectionIndex)
            campaignData.numGuildMembers = GetNumSelectionCampaignGuildMembers(selectionIndex)

            campaignData.canBeAllianceLocked = CanCampaignBeAllianceLocked(campaignId)
            campaignData.lockedToAlliance = GetSelectionCampaignCurrentAllianceLock(selectionIndex)
            campaignData.allianceLockReason = GetSelectionCampaignAllianceLockReason(selectionIndex)
            campaignData.allianceLockConflictingCharacterName = GetSelectionCampaignAllianceLockConflictingCharacterName(selectionIndex)

            campaignData.isImperialCityCampaign = IsImperialCityCampaign(campaignId)

            campaignData.queue = self:CreateCampaignQueueData(campaignData, CAMPAIGN_QUEUE_INDIVIDUAL)

            self.selectionCampaignList[selectionIndex] = campaignData
        end

        -- Categorize campaign data
        ZO_ClearTable(self.campaignRulesetsByType)
        
        for _, campaignData in ipairs(self.selectionCampaignList) do
            local campaignRulesetId = campaignData.rulesetId
            local campaignRulesetType = campaignData.rulesetType
            
            if not self.campaignRulesetsByType[campaignRulesetType] then
                self.campaignRulesetsByType[campaignRulesetType] = {}
            end
            local rulesetIds = self.campaignRulesetsByType[campaignRulesetType]

            if not ZO_IsElementInNumericallyIndexedTable(rulesetIds, campaignRulesetId) then
                table.insert(rulesetIds, campaignRulesetId)
            end
        end

        for _, rulesetIds in pairs(self.campaignRulesetsByType) do
            table.sort(rulesetIds, CompareRulesetsByDuration)
        end

        self:FireCallbacks("OnCampaignDataUpdated")
    end
end

function ZO_CampaignBrowser_Manager:CreateCampaignQueueData(campaignData, isGroup)
    return
    {
        type = ZO_CAMPAIGN_DATA_TYPE_QUEUE,
        id = campaignData.id,
        isGroup = isGroup,
        isQueued = IsQueuedForCampaign(campaignData.id, isGroup),
        state = GetCampaignQueueState(campaignData.id, isGroup),
        name = campaignData.name,
    }
end

function ZO_CampaignBrowser_Manager:UpdateQueueDataState(campaignData)
    local queueData = campaignData.queue

    queueData.isQueued = IsQueuedForCampaign(queueData.id, queueData.isGroup)
    queueData.state = GetCampaignQueueState(queueData.id, queueData.isGroup)

    self:FireCallbacks("OnCampaignQueueStateUpdated", campaignData)
end

function ZO_CampaignBrowser_Manager:GetCampaignDataList()
    return self.selectionCampaignList
end

function ZO_CampaignBrowser_Manager:GetActiveCampaignRulesetsByType()
    return self.campaignRulesetsByType
end

function ZO_CampaignBrowser_Manager:GetDataByCampaignId(campaignId)
    if campaignId ~= 0 then -- zero means "no campaign"
        for _, campaignData in ipairs(self.selectionCampaignList) do
            if campaignData.id == campaignId then
                return campaignData
            end
        end
    end

    return nil
end

function ZO_CampaignBrowser_Manager:GetMasterHomeData()
    return self:GetDataByCampaignId(GetAssignedCampaignId())
end

do
    local function IsAnyGroupMemberOffline()
        for i = 1, MAX_GROUP_SIZE_THRESHOLD do
            local unitTag = ZO_Group_GetUnitTagForGroupIndex(i)
            if not IsUnitOnline(unitTag) then
                return true
            end
        end
    
        return false
    end

    local QUEUE_MESSAGES = {
        [CAMPAIGN_QUEUE_REQUEST_STATE_PENDING_JOIN] = GetString(SI_CAMPAIGN_BROWSER_QUEUE_PENDING_JOIN),
        [CAMPAIGN_QUEUE_REQUEST_STATE_PENDING_LEAVE] = GetString(SI_CAMPAIGN_BROWSER_QUEUE_PENDING_LEAVE),
        [CAMPAIGN_QUEUE_REQUEST_STATE_PENDING_ACCEPT] = GetString(SI_CAMPAIGN_BROWSER_QUEUE_PENDING_ACCEPT),
    }
    
    function ZO_CampaignBrowser_Manager:GetQueueMessage(id, isGroup, state)
        local queueMessage = QUEUE_MESSAGES[state]
        
        if queueMessage then
            return true, queueMessage
        else
            local descriptionText
            local iconTexture

            if state == CAMPAIGN_QUEUE_REQUEST_STATE_WAITING then
                iconTexture = "EsoUI/Art/Campaign/campaignBrowser_queued.dds"
            elseif state == CAMPAIGN_QUEUE_REQUEST_STATE_CONFIRMING then
                iconTexture = "EsoUI/Art/Campaign/campaignBrowser_ready.dds"
            end
    
            if state == CAMPAIGN_QUEUE_REQUEST_STATE_WAITING then
                local positionInQueue = GetCampaignQueuePosition(id, isGroup)
                descriptionText = zo_strformat(SI_CAMPAIGN_BROWSER_QUEUED, positionInQueue)
            elseif state == CAMPAIGN_QUEUE_REQUEST_STATE_CONFIRMING then
                local timeString = ZO_FormatTime(GetCampaignQueueRemainingConfirmationSeconds(id, isGroup), TIME_FORMAT_STYLE_COLONS, TIME_FORMAT_PRECISION_TWELVE_HOUR)
                descriptionText = zo_strformat(SI_CAMPAIGN_BROWSER_READY, timeString)
            end

            return false, descriptionText, iconTexture
        end
    end
end

function ZO_CampaignBrowser_Manager:GenerateAllianceLockStatusMessage(campaignData)
    local _, secondsUntilCampaignEnd = GetSelectionCampaignTimes(campaignData.selectionIndex)

    local message = GetString("SI_CAMPAIGNALLIANCELOCKREASON", campaignData.allianceLockReason)
    local allianceString = ZO_SELECTED_TEXT:Colorize(ZO_CampaignBrowser_FormatPlatformAllianceIconAndName(campaignData.lockedToAlliance))
    local characterNameString = campaignData.allianceLockConflictingCharacterName and ZO_SELECTED_TEXT:Colorize(ZO_FormatUserFacingCharacterName(campaignData.allianceLockConflictingCharacterName))
    local campaignEndCooldownString = ZO_SELECTED_TEXT:Colorize(ZO_FormatTime(secondsUntilCampaignEnd, TIME_FORMAT_STYLE_SHOW_LARGEST_TWO_UNITS, TIME_FORMAT_PRECISION_TWELVE_HOUR, TIME_FORMAT_DIRECTION_DESCENDING))

    if campaignData.allianceLockReason == CAMPAIGN_ALLIANCE_LOCK_REASON_ENTERED_CAMPAIGN then
        return zo_strformat(message, allianceString, campaignEndCooldownString)
    elseif campaignData.allianceLockReason == CAMPAIGN_ALLIANCE_LOCK_REASON_CHARACTER_ASSIGNED then
        return zo_strformat(message, allianceString, characterNameString)
    elseif campaignData.allianceLockReason == CAMPAIGN_ALLIANCE_LOCK_REASON_CAMPAIGN_ENTERED_AND_ASSIGNED then
        return zo_strformat(message, allianceString, campaignEndCooldownString, characterNameString)
    end
end

CAMPAIGN_BROWSER_MANAGER = ZO_CampaignBrowser_Manager:New()

-- Global functions
do
    internalassert(CAMPAIGN_RULESET_TYPE_MAX_VALUE == 4, "Update ruleset icons")
    local CAMPAIGN_RULESET_TYPE_KEYBOARD_ICONS =
    {
        [CAMPAIGN_RULESET_TYPE_CYRODIIL] = 
        {
            up = "EsoUI/Art/LFG/LFG_indexIcon_allianceWar_up.dds",
            down = "EsoUI/Art/LFG/LFG_indexIcon_allianceWar_down.dds",
            over = "EsoUI/Art/LFG/LFG_indexIcon_allianceWar_over.dds",
        },
        [CAMPAIGN_RULESET_TYPE_IMPERIAL_CITY] = 
        {
            up = "EsoUI/Art/LFG/LFG_indexIcon_imperialCity_up.dds",
            down = "EsoUI/Art/LFG/LFG_indexIcon_imperialCity_down.dds",
            over = "EsoUI/Art/LFG/LFG_indexIcon_imperialCity_over.dds",
        },
    }
    local CAMPAIGN_RULESET_TYPE_GAMEPAD_ICONS =
    {
        [CAMPAIGN_RULESET_TYPE_CYRODIIL] = "EsoUI/Art/LFG/Gamepad/LFG_activityIcon_cyrodiil.dds",
        [CAMPAIGN_RULESET_TYPE_IMPERIAL_CITY] = "EsoUI/Art/LFG/Gamepad/LFG_activityIcon_imperialCity.dds",
    }

    function ZO_CampaignBrowser_GetKeyboardIconsForRulesetType(rulesetType)
        return CAMPAIGN_RULESET_TYPE_KEYBOARD_ICONS[rulesetType]
    end

    function ZO_CampaignBrowser_GetGamepadIconForRulesetType(rulesetType)
        return CAMPAIGN_RULESET_TYPE_GAMEPAD_ICONS[rulesetType]
    end
end

do
    local POPULATION_ICONS =
    {
        [CAMPAIGN_POP_LOW] = "EsoUI/Art/Campaign/campaignBrowser_lowPop.dds",
        [CAMPAIGN_POP_MEDIUM] = "EsoUI/Art/Campaign/campaignBrowser_medPop.dds",
        [CAMPAIGN_POP_HIGH] = "EsoUI/Art/Campaign/campaignBrowser_hiPop.dds",
        [CAMPAIGN_POP_FULL] = "EsoUI/Art/Campaign/campaignBrowser_fullPop.dds",
    }

    function ZO_CampaignBrowser_GetPopulationIcon(population)
        return POPULATION_ICONS[population]
    end
end

function ZO_CampaignBrowser_DoesPlayerMatchAllianceLock(campaignData)
    return campaignData.lockedToAlliance == ALLIANCE_NONE or campaignData.lockedToAlliance == GetUnitAlliance("player")
end

function ZO_CampaignBrowserDialogs_ShouldShowAllianceLockWarning(campaignData)
    -- Only show warning if the campaign hasn't already been entered
   return campaignData.canBeAllianceLocked and
        campaignData.allianceLockReason ~= CAMPAIGN_ALLIANCE_LOCK_REASON_ENTERED_CAMPAIGN and
        campaignData.allianceLockReason ~= CAMPAIGN_ALLIANCE_LOCK_REASON_CAMPAIGN_ENTERED_AND_ASSIGNED
end

function ZO_CampaignBrowser_FormatPlatformAllianceIconAndName(alliance)
    local ICON_SIZE = "100%"
    return zo_iconTextFormatNoSpace(ZO_GetPlatformAllianceSymbolIcon(alliance), ICON_SIZE, ICON_SIZE, GetAllianceName(alliance))
end
