local NUM_RECENT_ACHIEVEMENTS_TO_SHOW = 6

-- NOTE: If any chain is added which gets larger than this, design will need to
--  come up with a layout that works with the additional number, or can dynamically
--  adjust the number.
local MAX_CHAIN_SIZE = 5

local CHECKED_ICON          = "EsoUI/Art/Inventory/Gamepad/gp_inventory_icon_equipped.dds"
local UNCHECKED_ICON = nil
local NO_ACHIEVEMENT_ICON   = "EsoUI/Art/Achievements/Gamepad/Achievement_EmptyIcon.dds"
local SUMMARY_ICON          = "EsoUI/Art/TreeIcons/gamepad/achievement_categoryIcon_Summary.dds"

local ZO_Achievements_Gamepad = ZO_Gamepad_ParametricList_Screen:Subclass()

local function IsAchievementALine(achievementId)
    local chainId = GetFirstAchievementInLine(achievementId)
    if chainId == 0 then
        return false
    elseif chainId ~= achievementId then
        return true
    end
    local nextId = GetNextAchievementInLine(chainId)
    return nextId ~= 0
end

local function DoesAchievementLineContainAchievement(achievementLineId, achievementId)
    local firstAchievementId = GetFirstAchievementInLine(achievementLineId)
    if firstAchievementId ~= 0 then
        achievementLineId = firstAchievementId
    end

    while achievementLineId ~= 0 do
        if achievementLineId == achievementId then
            return true
        end
        achievementLineId = GetNextAchievementInLine(achievementLineId)
    end

    return false
end

local function GetCategoryInfoFromAchievementIdDetailed(achievementId)
    -- If the user is selecting from the recent achievements list, there
    --  will not be an open category id, so attempt to get the category
    --  from the achievement.
    local categoryId = GetCategoryInfoFromAchievementId(achievementId)
    if categoryId then
        return categoryId
    end

    -- Some achievements cannot find their category id properly, so try
    --  walking the achievement chain and look for one that has a category
    --  id.
    local tryAchievementId = GetFirstAchievementInLine(achievementId)
    while tryAchievementId ~= 0 do
        categoryId = GetCategoryInfoFromAchievementId(tryAchievementId)
        if categoryId then
            return categoryId
        end
        tryAchievementId = GetNextAchievementInLine(tryAchievementId)
    end

    -- We were unable to determine the correct category id.
    return nil
end

function ZO_Achievements_Gamepad:Initialize(control)
    ACHIEVEMENTS_GAMEPAD_SCENE = ZO_Scene:New("achievementsGamepad", SCENE_MANAGER)

    ZO_Gamepad_ParametricList_Screen.Initialize(self, control, ZO_GAMEPAD_HEADER_TABBAR_DONT_CREATE, nil, ACHIEVEMENTS_GAMEPAD_SCENE)

    self.characterFrameBorderControlPool = ZO_ControlPool:New("ZO_Gamepad_Achievement_IconCharacterFrameBorder", ZO_Gamepad_Achievements, "IconFrameBorder")

    self.selectedCategoryId = nil
    self.visibleCategoryId = nil
    self.achievementId = nil
    self.filterType = SI_ACHIEVEMENT_FILTER_SHOW_ALL

    self.selectedCategoryIndex = nil
    self.selectedAchievementIndices = {} --one saved per category

    self.footerBarName = ZO_Gamepad_Achievements_FooterBar:GetNamedChild("Name")
    self.footerBarValue = ZO_Gamepad_Achievements_FooterBar:GetNamedChild("Rank")
    self.footerBarBar = ZO_Gamepad_Achievements_FooterBar:GetNamedChild("XPBar")

    self.headerData =
    {
        titleText = GetString(SI_JOURNAL_MENU_ACHIEVEMENTS),
    }
    ZO_GamepadGenericHeader_Refresh(self.header, self.headerData)
end

function ZO_Achievements_Gamepad:SetupList(list)
    function ZO_Gamepad_Achivement_Entry_OnSetup(control, data, selected, reselectingDuringRebuild, enabled, active)
        ZO_SharedGamepadEntry_OnSetup(control, data, selected, reselectingDuringRebuild, enabled, active)

        local persistenceLevel = GetAchievementPersistenceLevel(data.achievementId)
        local isCharacterPersistent = persistenceLevel == ACHIEVEMENT_PERSISTENCE_CHARACTER
        if isCharacterPersistent then
            local characterPersistentColor = selected and ZO_SECOND_SELECTED_TEXT or ZO_SECOND_NORMAL_TEXT

            control.label:SetColor(characterPersistentColor:UnpackRGBA())
            control.statusIndicator:ClearIcons()

            if data.isEarnedAchievement then
                local NO_TINT = nil
                control.statusIndicator:AddIcon("EsoUI/Art/Inventory/Gamepad/gp_inventory_icon_equipped.dds", NO_TINT, GetString(SI_SCREEN_NARRATION_ACHIEVEMENT_EARNED_ICON_NARRATION))
            end

            control.statusIndicator:AddIcon("EsoUI/Art/Miscellaneous/Gamepad/gp_charNameIcon.dds", characterPersistentColor, GetString(SI_GAMEPAD_ACHIEVEMENTS_CHARACTER_PERSISTENT))
            control.statusIndicator:Show()

            local iconFrameBorder
            if control.iconFrameBorderKey then
                iconFrameBorder = self.characterFrameBorderControlPool:AcquireObject(control.iconFrameBorderKey)
            else
                local iconFrameBorderKey
                iconFrameBorder, iconFrameBorderKey = self.characterFrameBorderControlPool:AcquireObject()
                iconFrameBorder:SetParent(control:GetNamedChild("Icon"))
                iconFrameBorder:SetAnchor(TOPLEFT, nil, TOPLEFT, -9, -9)
                iconFrameBorder:SetAnchor(BOTTOMRIGHT, nil, BOTTOMRIGHT, 9, 9)
                control.iconFrameBorderKey = iconFrameBorderKey
            end
            iconFrameBorder:SetColor(characterPersistentColor:UnpackRGBA())
        else
            if control.iconFrameBorderKey then
                self.characterFrameBorderControlPool:ReleaseObject(control.iconFrameBorderKey)
                control.iconFrameBorderKey = nil
            end
        end
    end

    list:AddDataTemplate("ZO_GamepadAchievementsEntryTemplate", ZO_Gamepad_Achivement_Entry_OnSetup, ZO_GamepadMenuEntryTemplateParametricListFunction)
    list:AddDataTemplateWithHeader("ZO_GamepadAchievementsEntryTemplate", ZO_Gamepad_Achivement_Entry_OnSetup, ZO_GamepadMenuEntryTemplateParametricListFunction, nil, "ZO_GamepadMenuEntryHeaderTemplate")
    list:AddDataTemplate("ZO_GamepadMenuEntryWithBarTemplate", ZO_SharedGamepadEntry_OnSetup, ZO_GamepadMenuEntryTemplateParametricListFunction)

    self.itemList = list
end

local PADDING = 3
local function CreateAchievementSlot(parent, previous, index)
    local newControl = CreateControlFromVirtual("$(parent)Entry", parent, "ZO_Gamepad_Achievement_Entry", index)
    if previous then
        newControl:SetAnchor(LEFT, previous, RIGHT, PADDING, 0)
    else
         newControl:SetAnchor(LEFT, nil, LEFT, 25, 0)
    end

    newControl.index = index
    newControl.icon = newControl:GetNamedChild("Icon")
    newControl.frame = newControl:GetNamedChild("Frame")

    return newControl
end

local function CanFocusAchievement(control)
    return control.achievementId ~= nil
end

local function AchievementControlActivate(control)
    control.frame:SetEdgeColor(ZO_SELECTED_TEXT:UnpackRGB())
end

local function AchievementControlDeactivate(control)
    control.frame:SetEdgeColor(ZO_NORMAL_TEXT:UnpackRGB())
end

local function PlayHorizontalSound()
    PlaySound(SOUNDS.HOR_LIST_ITEM_SELECTED)
end

local function SetupAchievementList(parentControl, numEntries, controllers, canFocusFunction)
    local focus = ZO_GamepadFocus:New(parentControl, nil, MOVEMENT_CONTROLLER_DIRECTION_HORIZONTAL)
    focus.onPlaySoundFunction = PlayHorizontalSound
    local controls = {}

    local previous
    for i = 1, numEntries do
        local control = CreateAchievementSlot(parentControl, previous, i)
        table.insert(controls, control)

        local focusEntry =
        {
            control = control,
            canFocus = canFocusFunction,
            activate = AchievementControlActivate,
            deactivate = AchievementControlDeactivate,
            iconScaleAnimation = ANIMATION_MANAGER:CreateTimelineFromVirtual("ZO_Gamepad_Achievement_FocusIconScaleAnimation", control.icon),
            headerNarrationFunction = function()
                return SCREEN_NARRATION_MANAGER:CreateNarratableObject(GetString(SI_ACHIEVEMENTS_RECENT))
            end,
            footerNarrationFunction = function()
                return ACHIEVEMENTS_GAMEPAD:GetFooterNarration()
            end,
        }

        focus:AddEntry(focusEntry)
        previous = control
    end

    return focus, controls
end

function ZO_Achievements_Gamepad:OnDeferredInitialize()
    ZO_Gamepad_ParametricList_Screen.OnDeferredInitialize(self)

    self.movementController = ZO_MovementController:New(MOVEMENT_CONTROLLER_DIRECTION_VERTICAL)

    local rootContainer = self.control:GetNamedChild("Mask"):GetNamedChild("Container")

    self.noEntriesLabel = rootContainer:GetNamedChild("NoEntries")

    self.recentAchievementContainer = rootContainer:GetNamedChild("Recent")
    self.recentAchievementFocus, self.recentAchievementControls = SetupAchievementList(self.recentAchievementContainer:GetNamedChild("Centerer"), NUM_RECENT_ACHIEVEMENTS_TO_SHOW, {ZO_DI_LEFT_STICK, ZO_DI_DPAD})
    self.recentAchievementFocus:SetFocusChangedCallback(function(...) self:AchievementListSelectionChanged(self.recentAchievementFocus, ...) end)
    self:SetupHeaderFocus(self.recentAchievementFocus)

    self.chainContainer = self.control:GetNamedChild("Chain")
    self.chainFocus, self.chainControls = SetupAchievementList(self.chainContainer, MAX_CHAIN_SIZE, {ZO_DI_LEFT_STICK}, CanFocusAchievement)
    self.chainFocus:SetFocusChangedCallback(function(...) self:AchievementListSelectionChanged(self.chainFocus, ...) end)

    self:InitializeOptionsDialog()
    self:InitializeEvents()
end

function ZO_Achievements_Gamepad:InitializeEvents()
    local function OnAchievementUpdated(event, id)
        if SCENE_MANAGER:IsShowing("achievementsGamepad") then
            local entry = self.itemList:GetTargetData()
            if entry.achievementId == id then
                self:ShowAchievementTooltip(id)
            end
        end
    end

    local function Update()
        self:Update()
    end

    self.control:RegisterForEvent(EVENT_ACHIEVEMENTS_UPDATED, Update)
    self.control:RegisterForEvent(EVENT_ACHIEVEMENT_UPDATED, OnAchievementUpdated)
    self.control:RegisterForEvent(EVENT_ACHIEVEMENT_AWARDED, Update)
end

function ZO_Achievements_Gamepad:SetRecentAchievementsHidden(hidden)
    if self.recentAchievementContainer:IsControlHidden() == hidden then return end

    self.recentAchievementContainer:SetHidden(hidden)
    if hidden then
        -- In addition to hiding the container, we change its dimensions so the parameteric list
        --  fills the space.
        self.recentAchievementContainer:SetDimensions(nil, 0)
    else
        -- When the container is reshown, we need it to expand back to the size of its children
        --  so the list does not clip into the container.
        self.recentAchievementContainer:SetResizeToFitDescendents(true)
    end
end

function ZO_Achievements_Gamepad:OnEnterHeader()
    PlaySound(SOUNDS.GAMEPAD_MENU_UP)
    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.keybindStripDescriptor)
    local NARRATE_HEADER = true
    SCREEN_NARRATION_MANAGER:QueueFocus(self.recentAchievementFocus, NARRATE_HEADER)
end

function ZO_Achievements_Gamepad:OnLeaveHeader()
    self:ShowAchievementSummaryTooltip() -- We are selecting the "summary" entry.
    PlaySound(SOUNDS.GAMEPAD_MENU_DOWN)
    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.keybindStripDescriptor)
end

function ZO_Achievements_Gamepad:CanEnterHeader()
    return not self.recentAchievementContainer:IsControlHidden()
end

function ZO_Achievements_Gamepad:AchievementListSelectionChanged(list, entry)
    if entry then
        local recentAchievementId = entry.control.achievementId
        if recentAchievementId then
            self:ShowAchievementTooltip(recentAchievementId)
        else
            self:ShowNoAchievementTooltip()
        end
        if list == self.recentAchievementFocus then
            local NARRATE_HEADER = true
            SCREEN_NARRATION_MANAGER:QueueFocus(list, NARRATE_HEADER)
        else
            SCREEN_NARRATION_MANAGER:QueueParametricListEntry(self:GetCurrentList())
        end
    elseif (self.visibleCategoryId == nil) and (list == self.recentAchievementFocus) then
        self:HideTooltip()
    end
end

function ZO_Achievements_Gamepad:RefreshRecentAchievements()
    local recentAchievements = { GetRecentlyCompletedAchievements(NUM_RECENT_ACHIEVEMENTS_TO_SHOW) }
    for i = 1, NUM_RECENT_ACHIEVEMENTS_TO_SHOW do
        local control = self.recentAchievementControls[i]
        local achievementId = recentAchievements[i]

        control.icon:ClearIcons()

        if achievementId then
            local icon = select(4, GetAchievementInfo(achievementId))
            control.achievementId = achievementId
            control.icon:AddIcon(icon)
        else
            -- Occurs if the user does not have enough recent acheivements.
            control.achievementId = nil
            control.icon:AddIcon(NO_ACHIEVEMENT_ICON)
        end

        control.icon:Show()
    end
end

function ZO_Achievements_Gamepad:ShowAchievement(achievementId)
    local categoryId = GetCategoryInfoFromAchievementIdDetailed(achievementId)
    self:SwitchToCategoryAndAchievement(categoryId, achievementId)
    SCENE_MANAGER:Show("achievementsGamepad")
end

function ZO_Achievements_Gamepad:OnShowing()
    self:SwitchToFilterMode(SI_ACHIEVEMENT_FILTER_SHOW_ALL)

    ZO_Gamepad_ParametricList_Screen.OnShowing(self)

    self.recentAchievementFocus:Deactivate()
end

function ZO_Achievements_Gamepad:OnHide()
    if self.visibleCategoryId or self.selectedCategoryId or self.achievementId then
        self:SwitchToCategoryAndAchievement(nil, nil)
    end

    ZO_Gamepad_ParametricList_Screen.OnHide(self)

    self:HideTooltip()

    self.recentAchievementFocus:Deactivate()

    self.characterFrameBorderControlPool:ReleaseAllObjects()
end

function ZO_Achievements_Gamepad:SwitchToFilterMode(newMode)
    -- Skip the update if there was nothing to do.
    if self.filterType == newMode then return end
    self.filterType = newMode

    -- Update the icons displayed in the options dialog.
    for i = 1, #self.dialogFilterEntries do
        local filterEntry = self.dialogFilterEntries[i]
        filterEntry:ClearIcons()
        if filterEntry.filterType == newMode then
            filterEntry:AddIcon(CHECKED_ICON)
        else
            filterEntry:AddIcon(UNCHECKED_ICON)
        end
    end

    -- Refresh the display list, if needed. We know the filter
    --  only applies when viewing a specific category, and not
    --  on the category list.
    if self.visibleCategoryId then
        self:Update()
    end
end

function ZO_Achievements_Gamepad:InitializeOptionsDialog()
    local function SwitchToFilterMode(entry)
        self:SwitchToFilterMode(entry.filterType)
    end

    local function CreateEntry(filterType)
        local newEntry = ZO_GamepadEntryData:New(zo_strformat(filterType), (self.filterType == filterType) and CHECKED_ICON or UNCHECKED_ICON)
        newEntry.setup = ZO_SharedGamepadEntry_OnSetup
        newEntry.filterType = filterType
        newEntry.callback = SwitchToFilterMode
        newEntry.narrationText = function(entryData, entryControl)
            return ZO_FormatRadioButtonNarrationText(entryData.text, entryData.filterType == self.filterType)
        end
        return newEntry
    end

    local showAllAchievements = CreateEntry(SI_ACHIEVEMENT_FILTER_SHOW_ALL)
    local showEarnedAchievements = CreateEntry(SI_ACHIEVEMENT_FILTER_SHOW_EARNED)
    local showUnearnedAchievements = CreateEntry(SI_ACHIEVEMENT_FILTER_SHOW_UNEARNED)
    local linkAchievement = ZO_GamepadEntryData:New(zo_strformat(SI_ITEM_ACTION_LINK_TO_CHAT))
    linkAchievement.setup = ZO_SharedGamepadEntry_OnSetup
    linkAchievement.callback = function(entryData)
        local achievement = entryData.dialog.data.selectedAchievement
        ZO_LinkHandler_InsertLinkAndSubmit(ZO_LinkHandler_CreateChatLink(GetAchievementLink, achievement.achievementId))
        ZO_Dialogs_ReleaseDialogOnButtonPress("ACHIEVEMENTS_OPTIONS_GAMEPAD")
    end
    linkAchievement.visible = function(dialog)
        if IsChatSystemAvailableForCurrentPlatform() then
            return dialog.data.selectedAchievement ~= nil
        end
        return false
    end

    self.dialogFilterEntries =
    {
        showAllAchievements,
        showEarnedAchievements,
        showUnearnedAchievements
    }

    ZO_Dialogs_RegisterCustomDialog("ACHIEVEMENTS_OPTIONS_GAMEPAD",
    {
        gamepadInfo =
        {
            dialogType = GAMEPAD_DIALOGS.PARAMETRIC,
        },

        title =
        {
            text = SI_GAMEPAD_ACHIEVEMENTS_OPTIONS,
        },

        setup = function(dialog)
            dialog:setupFunc()
        end,

        parametricList =
        {
            {
                template = "ZO_GamepadMenuEntryTemplate",
                header = SI_GAMEPAD_ITEM_SETS_BOOK_OPTIONS_FILTERS,
                entryData = showAllAchievements,
            },
            {
                template = "ZO_GamepadMenuEntryTemplate",
                entryData = showEarnedAchievements,
            },
            {
                template = "ZO_GamepadMenuEntryTemplate",
                entryData = showUnearnedAchievements,
            },
            {
                template = "ZO_GamepadMenuEntryTemplate",
                header = GetString(SI_GAMEPAD_ITEM_SETS_BOOK_OPTIONS_ACTIONS),
                entryData = linkAchievement,
            },
        },
        buttons =
        {
            {
                keybind = "DIALOG_PRIMARY",
                text = SI_GAMEPAD_SELECT_OPTION,
                callback =  function(dialog)
                    local data = dialog.entryList:GetTargetData()
                    if data.callback then
                        data.callback(data)
                    end
                end,
                clickSound = SOUNDS.DIALOG_ACCEPT,
            },
            {
                keybind = "DIALOG_NEGATIVE",
                text = SI_DIALOG_CANCEL,
            },
        }
    })
end

function ZO_Achievements_Gamepad:SwitchToCategoryAndAchievement(categoryId, achievementId)
    self.visibleCategoryId = categoryId
    self.selectedCategoryId = categoryId
    self.achievementId = achievementId
    self:Update()
end

function ZO_Achievements_Gamepad:InitializeKeybindStripDescriptors()
    self.keybindStripDescriptor =
    {
        alignment = KEYBIND_STRIP_ALIGN_LEFT,

        -- Back
        KEYBIND_STRIP:GenerateGamepadBackButtonDescriptor(function()
            if self.visibleCategoryId then
                self.visibleCategoryId = nil
                self.achievementId = nil
                self:Update()
                PlaySound(SOUNDS.GAMEPAD_MENU_BACK)
                --This screen seems to use the same list for both the top level and the subcategories. As a result, we need to manually tell it to re-narrate the header when switching between them
                local NARRATE_HEADER = true
                SCREEN_NARRATION_MANAGER:QueueParametricListEntry(self.itemList, NARRATE_HEADER)
            else
                SCENE_MANAGER:HideCurrentScene()
            end
        end),

        -- Select
        {
            name = GetString(SI_GAMEPAD_SELECT_OPTION),
            keybind = "UI_SHORTCUT_PRIMARY",
            callback = function()
                local targetData = self.itemList:GetTargetData()
                self:SwitchToCategoryAndAchievement(targetData.categoryIndex, targetData.achievementId)
                --This screen seems to use the same list for both the top level and the subcategories. As a result, we need to manually tell it to re-narrate the header when switching between them
                local NARRATE_HEADER = true
                SCREEN_NARRATION_MANAGER:QueueParametricListEntry(self.itemList, NARRATE_HEADER)
            end,
            visible = function()
                if self.recentAchievementFocus.active then
                    return false
                else
                    local targetData = self.itemList:GetTargetData()
                    return targetData and targetData.canEnter
                end
            end,
            sound = SOUNDS.GAMEPAD_MENU_FORWARD,
        },

        -- Options
        {
            name = GetString(SI_GAMEPAD_DYEING_OPTIONS),
            keybind = "UI_SHORTCUT_TERTIARY",
            callback = function()
                local targetData = self.itemList:GetTargetData()
                ZO_Dialogs_ShowGamepadDialog("ACHIEVEMENTS_OPTIONS_GAMEPAD", {selectedAchievement = targetData})
            end,
            visible = function()
                return self.visibleCategoryId ~= nil
            end,
        },

        -- Navigate to Zone Guide
        {
            name = function()
                local targetData = self.itemList:GetTargetData()
                if targetData then
                    if GetSkyshardAchievementZoneId(targetData.achievementId) ~= 0 then
                        return GetString(SI_ZONE_STORY_OPEN_FROM_ACHIEVEMENT_ACTION)
                    else
                        return GetString(SI_ACHIEVEMENT_TO_LORE_LIBRARY_ACTION)
                    end
                end
            end,
            keybind = "UI_SHORTCUT_SECONDARY",
            callback = function()
                local targetData = self.itemList:GetTargetData()
                local zoneId = GetSkyshardAchievementZoneId(targetData.achievementId)
                if zoneId ~= 0 then
                    ZONE_STORIES_MANAGER:ShowZoneStoriesScene(zoneId)
                else
                    local bookCollectionId = GetAchievementLinkedBookCollectionId(targetData.achievementId)
                    LORE_LIBRARY_GAMEPAD:SetCollectionIdToSelect(bookCollectionId)
                    MAIN_MENU_GAMEPAD:SelectMenuEntry(ZO_MENU_MAIN_ENTRIES.JOURNAL)
                    SCENE_MANAGER:CreateStackFromScratch("mainMenuGamepad", "loreLibraryGamepad")
                end
            end,
            visible = function()
                local targetData = self.itemList:GetTargetData()
                return targetData and (GetSkyshardAchievementZoneId(targetData.achievementId) ~= 0 or GetAchievementLinkedBookCollectionId(targetData.achievementId) ~= 0)
            end,
        },
    }
    ZO_Gamepad_AddListTriggerKeybindDescriptors(self.keybindStripDescriptor, self.itemList)
end

function ZO_Achievements_Gamepad:ShowAchievementSummaryTooltip()
    -- Setup the tooltip contents.
    GAMEPAD_TOOLTIPS:ClearLines(GAMEPAD_LEFT_TOOLTIP)
    GAMEPAD_TOOLTIPS:LayoutAchievementSummary(GAMEPAD_LEFT_TOOLTIP)

    -- Hide the chain list.
    self:ClearLineList()
end

function ZO_Achievements_Gamepad:ShowNoAchievementTooltip()
    -- Setup the tooltip contents.
    GAMEPAD_TOOLTIPS:ClearLines(GAMEPAD_LEFT_TOOLTIP)
    GAMEPAD_TOOLTIPS:LayoutNoAchievement(GAMEPAD_LEFT_TOOLTIP)

    -- Hide the chain list.
    self:ClearLineList()
end

function ZO_Achievements_Gamepad:ShowAchievementTooltip(achievementId)
    -- Setup the tooltip contents.
    GAMEPAD_TOOLTIPS:ClearLines(GAMEPAD_LEFT_TOOLTIP)
    GAMEPAD_TOOLTIPS:LayoutAchievement(GAMEPAD_LEFT_TOOLTIP, achievementId)
end

function ZO_Achievements_Gamepad:ClearLineList()
    self:SetupLineList(0)
end

function ZO_Achievements_Gamepad:SetupLineList(achievementId)
    self.updatingLineList = true

    local chainId = GetFirstAchievementInLine(achievementId)
    local chainIndex = 1
    local selectedChainIndex = nil

    while chainId ~= 0 do
        local chainControl = self.chainControls[chainIndex]
        local name, _, _, icon, completed = GetAchievementInfo(chainId)
        local iconDesaturation = completed and 0 or 1

        chainControl.icon:ClearIcons()
        chainControl.icon:AddIcon(icon)
        chainControl.icon:SetDesaturation(iconDesaturation)
        chainControl.achievementId = chainId
        chainControl.name = name
        if chainId == achievementId then
            self.chainFocus:SetFocusByIndex(chainIndex)
        end
        chainControl:SetDimensions(55, 55)
        chainControl.icon:Show()
        chainControl:SetHidden(false)

        chainId = GetNextAchievementInLine(chainId)
        chainIndex = chainIndex + 1
    end

    for i=chainIndex, MAX_CHAIN_SIZE do
        local chainControl = self.chainControls[i]
        chainControl.achievementId = nil
        chainControl:SetDimensions(0, 0)
        chainControl.icon:Hide()
        chainControl:SetHidden(true)
    end

    local selectedAchievementEntryControl = self.itemList:GetTargetControl()

    -- NOTE: chainIndex can be 1 or 2 as sometimes GetFirstAchievementInLine will return 0 and sometimes it might
    --  return achievementId for a non-chained achievement. The difference seems to be a 1-achievement chain vs no
    --  chain, however we want to display both cases the same here.
    local chainContainer = self.chainContainer
    if (not selectedAchievementEntryControl) or (chainIndex <= 2) then
        -- This achievement is not part of a chain, hide the chain list.
        chainContainer:SetHidden(true)
        chainContainer:SetDimensions(nil, 0)
        self.chainFocus:Deactivate()
    else
        -- This achievement is part of a chain, show the chain list.
        chainContainer:SetHidden(false)
        self.chainFocus:Activate()

        chainContainer:ClearAnchors()
        chainContainer:SetHeight(71)
        chainContainer:SetAnchor(TOPLEFT, selectedAchievementEntryControl, BOTTOMLEFT, 80, 0)
        chainContainer:SetAnchor(TOPRIGHT, selectedAchievementEntryControl, BOTTOMRIGHT, 0, -2)
    end

    self.updatingLineList = false
end

function ZO_Achievements_Gamepad:HideTooltip()
    self:ClearLineList()
    GAMEPAD_TOOLTIPS:ClearLines(GAMEPAD_LEFT_TOOLTIP)
end

function ZO_Achievements_Gamepad:OnSelectionChanged(list, selectedData, oldSelectedData)
    if selectedData and selectedData.achievementId then
        self:ShowAchievementTooltip(selectedData.achievementId)
        self:SetupLineList(selectedData.achievementId)
        self.selectedAchievementIndices[selectedData.categoryIndex] = list:GetSelectedIndex()
    elseif selectedData and selectedData.isSummary then
        self:ShowAchievementSummaryTooltip()
        self:ClearLineList()
        self.selectedCategoryIndex = 1
    else
        self.selectedCategoryIndex = list:GetSelectedIndex()
        self:HideTooltip()
    end
end

function ZO_Achievements_Gamepad:PopulateCategories()
    local MIN_POINTS = 0
    local totalPoints = GetTotalAchievementPoints()
    local earnedPoints = GetEarnedAchievementPoints()

    self.footerBarNameText = GetString(SI_GAMEPAD_ACHIEVEMENTS_POINTS_LABEL)
    self.footerBarEarnedPoints = earnedPoints
    self.footerBarTotalPoints = totalPoints

    self.footerBarName:SetText(self.footerBarNameText)
    self.footerBarValue:SetText(earnedPoints)
    self.footerBarBar:SetMinMax(MIN_POINTS, totalPoints)
    self.footerBarBar:SetValue(earnedPoints)

    -- Create summary "category".
    local entryData = ZO_GamepadEntryData:New(zo_strformat(SI_JOURNAL_PROGRESS_SUMMARY), SUMMARY_ICON)
    entryData:SetIconTintOnSelection(true)
    entryData:SetBarValues(MIN_POINTS, totalPoints, earnedPoints)
    entryData.canEnter = false
    entryData.isSummary = true
    self.itemList:AddEntry("ZO_GamepadMenuEntryWithBarTemplate", entryData)

    -- Populate actual categories.
    for categoryIndex = 1, GetNumAchievementCategories() do
        local categoryName, _, _, earnedPoints, totalPoints = GetAchievementCategoryInfo(categoryIndex)
        if totalPoints > 0 then
            local gamepadIcon = GetAchievementCategoryGamepadIcon(categoryIndex)
            local entryData = ZO_GamepadEntryData:New(zo_strformat(categoryName), gamepadIcon)
            entryData:SetIconTintOnSelection(true)
            entryData:SetBarValues(MIN_POINTS, totalPoints, earnedPoints)
            entryData.categoryIndex = categoryIndex
            entryData.canEnter = true

            self.itemList:AddEntry("ZO_GamepadMenuEntryWithBarTemplate", entryData)
        end
    end
end

function ZO_Achievements_Gamepad:AddAchievements(categoryIndex, subCategoryIndex, subCategoryName, achievementIds)
    local currentIndex = 0

    for achievementIndex=1, #achievementIds do
        local achievementId = achievementIds[achievementIndex]

        if ZO_ShouldShowAchievement(self.filterType, achievementId) then
            local nextAchievementId = ZO_GetNextInProgressAchievementInLine(achievementId)
            local firstAchievementId = GetFirstAchievementInLine(achievementId)

            currentIndex = currentIndex + 1

            local achievementName, _, points, icon, completed = GetAchievementInfo(nextAchievementId)

            -- Achievement display names use gender switching
            local entryData = ZO_GamepadEntryData:New(zo_strformat(achievementName), icon)
            entryData:SetFontScaleOnSelection(false)
            entryData:SetIconDesaturation(completed and 0 or 1)
            entryData.isEarnedAchievement = completed
            entryData.categoryIndex = categoryIndex
            entryData.subCategoryIndex = subCategoryIndex
            entryData.achievementId = nextAchievementId
            entryData.firstAchievementId = firstAchievementId
            if points ~= ACHIEVEMENT_POINT_LEGENDARY_DEED then
                entryData:AddSubLabel(points)
                entryData:SetShowUnselectedSublabels(true)
            end
            entryData.canEnter = false

            local template
            if currentIndex == 1 then
                entryData:SetHeader(subCategoryName)
                template = "ZO_GamepadAchievementsEntryTemplateWithHeader"
            else
                template = "ZO_GamepadAchievementsEntryTemplate"
            end

            local postSelectedPadding = IsAchievementALine(nextAchievementId) and 48 or 0
            self.itemList:AddEntry(template, entryData, nil, nil, nil, postSelectedPadding)

            if self.achievementId and DoesAchievementLineContainAchievement(achievementId, self.achievementId) then
                -- currentIndex is only in the current category.  self.itemList:GetNumItems() has all current items.
                self.selectedAchievementIndices[self.selectedCategoryId] = self.itemList:GetNumItems()
                self.achievementId = nil
            end
        end
    end
end

function ZO_Achievements_Gamepad:PopulateAchievements(categoryIndex)
    local categoryName, numSubCategories, numAchievements, earnedPoints, totalPoints = GetAchievementCategoryInfo(categoryIndex)
    self.footerBarNameText = categoryName
    self.footerBarEarnedPoints = earnedPoints
    self.footerBarTotalPoints = totalPoints

    self.footerBarName:SetText(categoryName)
    self.footerBarValue:SetText(earnedPoints)
    self.footerBarBar:SetMinMax(0, totalPoints)
    self.footerBarBar:SetValue(earnedPoints)

    -- Handle "General"
    local achievementIds = ZO_GetAchievementIds(categoryIndex, nil, numAchievements)
    self:AddAchievements(categoryIndex, nil, GetString(SI_JOURNAL_PROGRESS_CATEGORY_GENERAL), achievementIds)

    -- Handle categories
    for subCategoryIndex=1, numSubCategories do
        local subCategoryName, subNumAchievements = GetAchievementSubCategoryInfo(categoryIndex, subCategoryIndex)
        local achievementIds = ZO_GetAchievementIds(categoryIndex, subCategoryIndex, subNumAchievements)
        self:AddAchievements(categoryIndex, subCategoryIndex, subCategoryName, achievementIds)
    end
end

--[[
    Gets the currently selected achievement ID from the screen.

    Returns a 2-tuple of (hasSelection, selectionId), with the following possible values:
        false, nil - There is no valid selection.
        true, true - The summary is selected.
        true, nil - An achievement category is selected.
        true, number - An achievement is selected, with selectionId being the achievementId.
]]
function ZO_Achievements_Gamepad:GetSelectionInformation()
    if self.itemList.active then
        local targetData = self.itemList:GetTargetData()
        if targetData and targetData.achievementId then
            -- The user has an entry in the main item list selected that is an achievement.
            return true, targetData.achievementId
        elseif targetData and targetData.isSummary then
            -- The user has an entry in the main item list selected that is the summary.
            return true, true
        elseif targetData then
            -- The user has an entry in the main item list selected that is an achievement category.
            return false, nil
        end
    end

    local selectedData = self.chainFocus:GetFocusItem()
    if selectedData then
        return true, selectedData.control.achievementId
    end

    selectedData = self.recentAchievementFocus:GetFocusItem()
    if selectedData then
        return true, selectedData.control.achievementId
    end

    return false, nil
end

function ZO_Achievements_Gamepad:PerformUpdate()
    self.itemList:Clear()

    local selectedIndex
    if not self.visibleCategoryId then
        selectedIndex = self.selectedCategoryIndex or 1
        self:PopulateCategories()
        self:RefreshRecentAchievements()
        self:SetRecentAchievementsHidden(false)
        self.headerData.titleText = GetString(SI_JOURNAL_MENU_ACHIEVEMENTS)
    else
        self:PopulateAchievements(self.visibleCategoryId)
        -- Determine the selected index AFTER PopulateAchievements because Populate will mark any specific self.achievementId requested.
        selectedIndex = self.selectedAchievementIndices[self.visibleCategoryId] or 1
        self:SetRecentAchievementsHidden(true)
        self.headerData.titleText = GetAchievementCategoryInfo(self.visibleCategoryId)
    end

    self.itemList:CommitWithoutReselect()

    local hasItems = self.itemList:GetNumItems() ~= 0
    self.noEntriesLabel:SetHidden(hasItems)

    if hasItems then
        -- NOTE: This does not use self.itemList:SetSelectedIndexWithoutAnimation() as that function has additional side-effects
        --  with the selected item and does not properly call the setup functions.
        self.itemList:EnableAnimation(false)
        self.itemList:SetSelectedIndex(selectedIndex)
        self.itemList:EnableAnimation(true)
    end

    local hasSelection, selectedAchievementId = self:GetSelectionInformation()
    if hasSelection and (type(selectedAchievementId) == "number") then
        self:ShowAchievementTooltip(selectedAchievementId)
    elseif hasSelection and selectedAchievementId then
        self:ShowAchievementSummaryTooltip()
    elseif hasSelection then
        self:ShowNoAchievementTooltip()
    else
        self:HideTooltip()
    end

    KEYBIND_STRIP:UpdateKeybindButtonGroup(self.keybindStripDescriptor)
    ZO_GamepadGenericHeader_Refresh(self.header, self.headerData)
end

--Overridden from base
function ZO_Achievements_Gamepad:GetFooterNarration()
    local narrations = {}
    table.insert(narrations, SCREEN_NARRATION_MANAGER:CreateNarratableObject(self.footerBarNameText))
    table.insert(narrations, SCREEN_NARRATION_MANAGER:CreateNarratableObject(self.footerBarEarnedPoints))
    local progressNarration = ZO_GetProgressBarNarrationText(0, self.footerBarTotalPoints, self.footerBarEarnedPoints)
    if progressNarration then
        table.insert(narrations, progressNarration)
    end

    return narrations
end

function ZO_Achievements_Gamepad_OnInitialize(control)
    ACHIEVEMENTS_GAMEPAD = ZO_Achievements_Gamepad:New(control)
    SYSTEMS:RegisterGamepadObject("achievements", ACHIEVEMENTS_GAMEPAD)
end
