local DEFAULT_EXPECTED_ENTRY_HEIGHT = ZO_GAMEPAD_DEFAULT_LIST_ENTRY_SELECTED_HEIGHT
local DEFAULT_EXPECTED_HEADER_HEIGHT = 24

ZO_PARAMETRIC_MOVEMENT_TYPES =
{
    MOVE_NEXT = 1,
    MOVE_PREVIOUS = 2,
    JUMP_NEXT = 3,
    JUMP_PREVIOUS = 4,
    -- LAST allows derived classes to start their movement enumerations after the base movements 
    LAST = 5,
}

ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR =
{
    RESELECT_OLD_INDEX = 1,
    RESET_TO_DEFAULT = 2,
    MATCH_OR_RESELECT_OLD_INDEX = 3,
    MATCH_OR_RESET_TO_DEFAULT = 4,
}

ZO_ParametricScrollList = ZO_InitializingCallbackObject:Subclass()

--[[ Public  API ]]--
PARAMETRIC_SCROLL_LIST_VERTICAL = true
PARAMETRIC_SCROLL_LIST_HORIZONTAL = false
ZO_VERTICAL_PARAMETRIC_LIST_DEFAULT_FADE_GRADIENT_SIZE = 150
ZO_HORIZONTAL_PARAMETRIC_LIST_DEFAULT_FADE_GRADIENT_SIZE = 300

function ZO_ParametricScrollList:Initialize(control, mode, onActivatedChangedFunction, onCommitWithItemsFunction, onClearedFunction)
    self.control = control
    control.scrollList = self
    self.scrollControl = control:GetNamedChild("Scroll")

    local screenCenterControl = control:GetNamedChild("ListScreenCenterIsAlongTop")
    if screenCenterControl then
        self.alignToScreenCenterAnchor = screenCenterControl:GetNamedChild("ListScreenCenter")
    end

    self.mode = mode
    self.onActivatedChangedFunction = onActivatedChangedFunction
    self.onCommitWithItemsFunction = onCommitWithItemsFunction
    self.onClearedFunction = onClearedFunction
    self.onPlaySoundFunction = function()
        PlaySound(SOUNDS.HOR_LIST_ITEM_SELECTED)
    end

    self.previousArrow = self.control:GetNamedChild("PreviousArrow")
    self.nextArrow = self.control:GetNamedChild("NextArrow")

    self.scrollUpControl = self.control:GetNamedChild("ScrollUp")
    self.scrollDownControl = self.control:GetNamedChild("ScrollDown")

    self.minOffset = 0
    self.maxOffset = 40

    self.additonalMinBottomOffset = 0
    self.additonalMaxBottomOffset = 0

    self.universalPrePadding = 0
    self.universalPostPadding = 0

    self.headerDefaultPadding = 0
    self.headerSelectedPadding = 0

    self.defaultSelectedIndex = 1

    self.fixedCenterOffset = 0

    self.isMoving = false
    self.animationEnabled = true
    self.soundEnabled = true
    self.directionalInputEnabled = true
    self.handleDynamicViewProperties = false
    self.validGradientDirty = true
    self.anchorOppositeSide = false
    self.reselectBehavior = ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR.MATCH_OR_RESELECT_OLD_INDEX

    self:SetActive(false)
    self.enabled = true

    self.centerDampingFactor = 0

    self.movementController = ZO_MovementController:New(self.mode == PARAMETRIC_SCROLL_LIST_VERTICAL and MOVEMENT_CONTROLLER_DIRECTION_VERTICAL or MOVEMENT_CONTROLLER_DIRECTION_HORIZONTAL)

    self.dataTypes = {}
    self.commitHistoryDictionary = {}

    self.control:SetHandler("OnUpdate", function() self:OnUpdate() end)

    self.noItemsLabel = self.control:GetNamedChild("NoItemsLabel")

    self:Clear()
end

local function DefaultEqualityFunction(leftData, rightData)
    return leftData == rightData
end

function ZO_ParametricScrollList:HasDataTemplate(templateName) 
    return self.dataTypes[templateName] ~= nil
end

function ZO_ParametricScrollList:AddMouseBehaviorToControl(control)
    if not IsConsoleUI() then
        control:SetMouseEnabled(true)
        control:SetHandler("OnMouseUp", function(_, button, upInside)
            if self:IsActive() and self.enabled then
                if button == MOUSE_BUTTON_INDEX_LEFT and upInside then
                    self:SetSelectedIndex(control.dataIndex)
                end
            end
        end)
    end
end

function ZO_ParametricScrollList:AddDataTemplate(templateName, setupFunction, parametricFunction, equalityFunction, controlPoolPrefix, controlPoolResetFunction)
    if not self.dataTypes[templateName] then
        local controlPool = ZO_ControlPool:New(templateName, self.scrollControl, controlPoolPrefix)
        local dataTypeInfo = {
            pool = controlPool,
            setupFunction = setupFunction,
            parametricFunction = parametricFunction,
            equalityFunction = equalityFunction or DefaultEqualityFunction,
            hasHeader = false,
        }

        if not IsConsoleUI() then
            controlPool:SetCustomFactoryBehavior(function(control)
                self:AddMouseBehaviorToControl(control)
            end)
        end

        if controlPoolResetFunction then
            controlPool:SetCustomResetBehavior(controlPoolResetFunction)
        end

        self.dataTypes[templateName] = dataTypeInfo
    end
end

function ZO_ParametricScrollList:SetDataTemplateSetupFunction(templateName, setupFunction)
    if self.dataTypes[templateName] then
        self.dataTypes[templateName].setupFunction = setupFunction
    end
end

function ZO_ParametricScrollList:SetDataTemplateReleaseFunction(templateName, releaseFunction)
    if self.dataTypes[templateName] then
        self.dataTypes[templateName].pool:SetCustomResetBehavior(releaseFunction)
    end
end

function ZO_ParametricScrollList:SetDataTemplateWithHeaderReleaseFunction(templateName, releaseFunction)
    local function HeaderReleaseFunction(control)
        control.headerControl:SetHidden(true)
        releaseFunction(control)
    end
    self:SetDataTemplateReleaseFunction(templateName.."WithHeader", HeaderReleaseFunction)
end

function ZO_ParametricScrollList_DefaultMenuEntryWithHeaderSetup(control, data, selected, selectedDuringRebuild, enabled, activated)
    if data.header then 
        control:SetText(data.header)
    end
end

function ZO_ParametricScrollList:AddDataTemplateWithHeader(templateName, setupFunction , parametricFunction, equalityFunction, headerTemplateName, optionalHeaderSetupFunction, controlPoolPrefix, controlPoolResetFunction)
    local entryTemplateName = templateName
    templateName = templateName.."WithHeader"
    if not self.dataTypes[templateName] then
        if controlPoolPrefix then 
            controlPoolPrefix = controlPoolPrefix.."WithHeader" 
        end
        local dataTypeInfo = {
            pool = ZO_ControlPool:New(entryTemplateName, self.scrollControl, controlPoolPrefix or templateName),
            setupFunction = setupFunction,
            parametricFunction = parametricFunction,
            equalityFunction = equalityFunction or DefaultEqualityFunction,
            headerSetupFunction = optionalHeaderSetupFunction or ZO_ParametricScrollList_DefaultMenuEntryWithHeaderSetup,
            hasHeader = true,
        }
        dataTypeInfo.pool:SetCustomFactoryBehavior(function(control)
            local headerControl = CreateControlFromVirtual(control:GetName().."Header", self.scrollControl, headerTemplateName)
            --We create the control as a child of the scroll control, but the anchors are specified in relation to this control.
            --We do this to have the header not inherit alpha from changing the control's alpha
            for i = 0, 1 do
                local isValid, point, relTo, relPoint, offsetX, offsetY = headerControl:GetAnchor(i)
                if isValid then
                    headerControl:SetAnchor(point, control, relPoint, offsetX, offsetY)
                end
            end
            control.headerControl = headerControl
            self:AddMouseBehaviorToControl(control)
        end)

        dataTypeInfo.pool:SetCustomResetBehavior(function(control)
                control.headerControl:SetHidden(true)
                if controlPoolResetFunction then
                    controlPoolResetFunction(control)
                end
            end)

        dataTypeInfo.pool:SetCustomAcquireBehavior(function(control)
                control.headerControl:SetHidden(false)
            end)

        self.dataTypes[templateName] = dataTypeInfo
    end
end

function ZO_ParametricScrollList:SetEqualityFunction(templateName, equalityFunction)
    if self.dataTypes[templateName] then
        self.dataTypes[templateName].equalityFunction = equalityFunction or DefaultEqualityFunction
    end
end

function ZO_ParametricScrollList:SetReselectBehavior(reselectBehavior)
    self.reselectBehavior = reselectBehavior
end

function ZO_ParametricScrollList:AddEntryAtIndex(index, templateName, data, prePadding, postPadding, preSelectedOffsetAdditionalPadding, postSelectedOffsetAdditionalPadding, selectedCenterOffset)
    if self.dataTypes[templateName] then
        --Keep these parallel arrays in sync with RemoveEntry below

        table.insert(self.templateList, index, templateName)
        table.insert(self.dataList, index, data)

        -- NOTE: These are set to false if not specified as nil will cause the next entry to have an incorrect index in the field. Direct
        --  access is not recommended, use the GetSelectedAdditionalPaddingForDataIndex() or GetPaddingForDataIndex() functions instead.
        table.insert(self.prePadding, index, prePadding or 0)
        table.insert(self.postPadding, index, postPadding or false)
        table.insert(self.preSelectedOffsetAdditionalPadding, index, preSelectedOffsetAdditionalPadding or false)
        table.insert(self.postSelectedOffsetAdditionalPadding, index, postSelectedOffsetAdditionalPadding or false)
        table.insert(self.selectedCenterOffset, index, selectedCenterOffset or 0)
    end
end

function ZO_ParametricScrollList:AddEntry(templateName, data, prePadding, postPadding, preSelectedOffsetAdditionalPadding, postSelectedOffsetAdditionalPadding, selectedCenterOffset)
    self:AddEntryAtIndex(#self.dataList + 1, templateName, data, prePadding, postPadding, preSelectedOffsetAdditionalPadding, postSelectedOffsetAdditionalPadding, selectedCenterOffset)
end

function ZO_ParametricScrollList:RemoveEntry(templateName, data)
    local dataIndex = self:GetIndexForData(templateName, data)
    if dataIndex then
        table.remove(self.templateList, dataIndex)
        table.remove(self.dataList, dataIndex)
        table.remove(self.prePadding, dataIndex)
        table.remove(self.postPadding, dataIndex)
        table.remove(self.preSelectedOffsetAdditionalPadding, dataIndex)
        table.remove(self.postSelectedOffsetAdditionalPadding, dataIndex)
        table.remove(self.selectedCenterOffset, dataIndex)
    end
end

function ZO_ParametricScrollList:GetNumEntries()
    return #self.dataList
end

function ZO_ParametricScrollList:HasEntries()
    return #self.dataList > 0
end

function ZO_ParametricScrollList:IsEmpty()
    return #self.dataList == 0
end

function ZO_ParametricScrollList:GetEntryData(index)
    return self.dataList[index]
end

function ZO_ParametricScrollList:GetIndexForData(templateName, data)
    for i = 1, #self.dataList do
        local currentTemplateName = self.templateList[i]
        if not templateName or currentTemplateName == templateName then
            local templateInfo = self.dataTypes[currentTemplateName]
            if templateInfo.equalityFunction(self.dataList[i], data) then
                return i
            end
        end
    end
end

function ZO_ParametricScrollList:FindFirstIndexByEval(evalFunction)
    for index, data in ipairs(self.dataList) do
        if evalFunction(data) then
            return index
        end
    end

    return nil
end

function ZO_ParametricScrollList:AddEntryWithHeader(templateName, ...)
    self:AddEntry(templateName.."WithHeader", ...)
end

function ZO_ParametricScrollList:SetOnMovementChangedCallback(onMovementChangedCallback)
    self:RegisterCallback("MovementChanged", onMovementChangedCallback)
end

function ZO_ParametricScrollList:RemoveOnMovementChangedCallback(onMovementChangedCallback)
    self:UnregisterCallback("MovementChanged", onMovementChangedCallback)
end

function ZO_ParametricScrollList:SetOnTargetDataChangedCallback(onTargetDataChangedCallback)
    self:RegisterCallback("TargetDataChanged", onTargetDataChangedCallback)
end

function ZO_ParametricScrollList:RemoveOnTargetDataChangedCallback(onTargetDataChangedCallback)
    self:UnregisterCallback("TargetDataChanged", onTargetDataChangedCallback)
end

function ZO_ParametricScrollList:SetOnSelectedDataChangedCallback(onSelectedDataChangedCallback)
    self:RegisterCallback("SelectedDataChanged", onSelectedDataChangedCallback)
end

function ZO_ParametricScrollList:RemoveOnSelectedDataChangedCallback(onSelectedDataChangedCallback)
    self:UnregisterCallback("SelectedDataChanged", onSelectedDataChangedCallback)
end

function ZO_ParametricScrollList:RemoveAllOnSelectedDataChangedCallbacks()
    self:UnregisterAllCallbacks("SelectedDataChanged")
end

function ZO_ParametricScrollList:SetOnHitBeginningOfListCallback(onHitBeginningOfListCallback)
    self:RegisterCallback("HitBeginningOfList", onHitBeginningOfListCallback)
end

function ZO_ParametricScrollList:SetDrawScrollArrows(drawScrollArrows)
    self.drawScrollArrows = drawScrollArrows
end

function ZO_ParametricScrollList:SetAnchorOppositeSide(anchorOppositeSide)
    self.anchorOppositeSide = anchorOppositeSide
end

function ZO_ParametricScrollList:UpdateScrollArrows()
    if(self.drawScrollArrows) then 
        local selectedIndex = self:CalculateSelectedIndexOffsetWithDrag()

        local numItems = self:GetNumItems()

        local firstSelectedableIndex = self:GetNextSelectableIndex(0) -- this function adds 1 from the passed in current index

        if(self.scrollUpControl ~= nil) then
            local hideScrollUp = selectedIndex == firstSelectedableIndex or self:IsControlIndexFullyVisible(1)
            self.scrollUpControl:SetHidden(hideScrollUp)
        end

        if(self.scrollDownControl ~= nil) then
            local hideScrollDown = selectedIndex == numItems or self:IsControlIndexFullyVisible(numItems)
            self.scrollDownControl:SetHidden(hideScrollDown)
        end
    end
end

function ZO_ParametricScrollList:SetSortFunction(sortFunction)
    self.sortFunction = sortFunction
end

function ZO_ParametricScrollList:SetFixedCenterOffset(fixedCenterOffset)
    if self.fixedCenterOffset ~= fixedCenterOffset then
        self.fixedCenterOffset = fixedCenterOffset
        self.validGradientDirty = true
        self:RefreshVisible()
    end
end

function ZO_ParametricScrollList:SetAlignToScreenCenter(alignToScreenCenter, expectedEntryHeight)
    if(not self.alignToScreenCenterAnchor) then return end

    local expectedEntryHalfHeight = (expectedEntryHeight or DEFAULT_EXPECTED_ENTRY_HEIGHT) / 2.0

    if(self.alignToScreenCenter ~= alignToScreenCenter or self.alignToScreenCenterExpectedEntryHalfHeight ~= expectedEntryHalfHeight) then
        self.alignToScreenCenter = alignToScreenCenter
        self.alignToScreenCenterExpectedEntryHalfHeight = expectedEntryHalfHeight

        self.validGradientDirty = true
        self:RefreshVisible()
    end
end

function ZO_ParametricScrollList:SetActive(active, fireActivatedCallback)
    if self.active ~= active then
        --If the list is still animating when it is deactivated then complete the animation instantly before deactivating.
        --Otherwise, things can happen like selected index changing which the list is deactivated
        if not active and self:IsMoving() then
            self:UpdateAnchors(self.targetSelectedIndex)
            self:SetMoving(false)
        end
        
        self.active = active

        if self.active and self.directionalInputEnabled then
            DIRECTIONAL_INPUT:Activate(self, self.control)
        else
            DIRECTIONAL_INPUT:Deactivate(self)
        end

        if self.onActivatedChangedFunction and fireActivatedCallback ~= false then
            self.onActivatedChangedFunction(self, self.active)
            self:FireCallbacks("ActivatedChanged", self, self.active)
        end
    end
end

function ZO_ParametricScrollList:SetOnActivatedChangedFunction(func)
    self.onActivatedChangedFunction = func
end

function ZO_ParametricScrollList:GetOnActivatedChangedFunction()
    return self.onActivatedChangedFunction
end 

function ZO_ParametricScrollList:IsActive()
    return self.active
end

function ZO_ParametricScrollList:Activate()
    self:SetActive(true)
end

function ZO_ParametricScrollList:ActivateWithoutChangedCallback()
    local FIRE_ACTIVATION_CHANGED = false
    self:SetActive(true, FIRE_ACTIVATION_CHANGED)
end

function ZO_ParametricScrollList:Deactivate()
    self:SetActive(false)
end

function ZO_ParametricScrollList:DeactivateWithoutChangedCallback()
    local FIRE_ACTIVATION_CHANGED = false
    self:SetActive(false, FIRE_ACTIVATION_CHANGED)
end

function ZO_ParametricScrollList:SetEnabled(enabled)
    if self.enabled ~= enabled then
        self.enabled = enabled

        self.dragging = false

        self:RefreshVisible()
    end
end

function ZO_ParametricScrollList:SetSelectedItemOffsets(minOffset, maxOffset)
    self.minOffset = minOffset
    self.maxOffset = maxOffset
end

function ZO_ParametricScrollList:SetAdditionalBottomSelectedItemOffsets(additonalMinBottomOffset, additonalMaxBottomOffset)
    self.additonalMinBottomOffset = additonalMinBottomOffset
    self.additonalMaxBottomOffset = additonalMaxBottomOffset
end

function ZO_ParametricScrollList:SetUniversalPrePadding(universalPrePadding)
    self.universalPrePadding = universalPrePadding
end

function ZO_ParametricScrollList:SetUniversalPostPadding(universalPostPadding)
    self.universalPostPadding = universalPostPadding
end

function ZO_ParametricScrollList:SetNoItemText(text)
    if self.noItemsLabel then
        self.noItemsLabel:SetText(text)
        self.noItemsText = text
        self:RefreshNoItemLabelPosition()
    end
end

function ZO_ParametricScrollList:GetNoItemText()
    return self.noItemsText
end

function ZO_ParametricScrollList:IsMoving()
    return self.isMoving
end

function ZO_ParametricScrollList:RefreshVisible()
    if self.dataList and (#self.dataList > 0) then
        local INITIAL_UPDATE = true
        local RESELECTING_DURING_REBUILD = true
        if self.isMoving then
            self:UpdateAnchors(self.lastContinousTargetOffset, INITIAL_UPDATE)
        else
            self:UpdateAnchors(self:CalculateSelectedIndexOffsetWithDrag(), INITIAL_UPDATE, RESELECTING_DURING_REBUILD)
        end
    end
    self:RefreshNoItemLabelPosition()
end

local function CanSelectData(data)
    if data and type(data) == "table" then
        return data.canSelect ~= false -- true if nil
    end
    return true -- default to true
end

function ZO_ParametricScrollList:CanSelect(newIndex)
    local data = self.dataList[zo_clamp(newIndex, 1, #self.dataList)]
    return CanSelectData(data)
end

function ZO_ParametricScrollList:MovePrevious()
    if #self.dataList > 1 then
        local newIndex = (self.targetSelectedIndex or self.selectedIndex or 2) - 1
        while newIndex >= 1 and not self:CanSelect(newIndex) do
            newIndex = newIndex - 1
        end
        if newIndex >= 1 then
            self:SetSelectedIndex(newIndex)
            return true
        elseif newIndex == 0 then
            self:FireCallbacks("HitBeginningOfList")
        end
    end

    return false
end

function ZO_ParametricScrollList:MoveNext()
    if #self.dataList > 1 then
        local newIndex = self:GetNextSelectableIndex(self:CalculateSelectedIndexOffsetWithDrag())
        if newIndex <= #self.dataList then
            self:SetSelectedIndex(newIndex)
            return true
        end
    end

    return false
end

function ZO_ParametricScrollList:GetNumItems()
    return #self.dataList
end

function ZO_ParametricScrollList:IsControlIndexFullyVisible(index)
    local control = self.dataIndexToControl[index]

    if control then
        if(self.visibleControls[control] ~= true) then
            return false
        end

        local left, top, right, bottom = control:GetScreenRect()
        local leftList, topList, rightList, bottomList = self.control:GetScreenRect()

        return (bottom <= bottomList) and (top >= topList)
    end

    return false
end

function ZO_ParametricScrollList:GetSelectedIndex()
    return self.selectedIndex
end

function ZO_ParametricScrollList:SetSelectedIndexWithoutAnimation(selectedIndex, allowEvenIfDisabled, forceAnimation)
    self:EnableAnimation(false)
    self:SetSelectedIndex(selectedIndex, allowEvenIfDisabled, forceAnimation)
    self:EnableAnimation(true)
end

function ZO_ParametricScrollList:SetSelectedIndex(selectedIndex, allowEvenIfDisabled, forceAnimation, jumpType, blockSelectionChangedCallback, reselectingDuringRebuild)
    self:SetJumping(false)

    if self.enabled or allowEvenIfDisabled then
        local oldTargetSelectedIndex = self.targetSelectedIndex
        self.targetSelectedIndex = zo_clamp(selectedIndex, 1, #self.dataList)
        local reachedTargetIndex = (self.targetSelectedIndex == self:CalculateSelectedIndexOffsetWithDrag())

        if not blockSelectionChangedCallback then
            self:FireCallbacks("TargetDataChanged", self, self:GetDataForDataIndex(self.targetSelectedIndex), self:GetDataForDataIndex(oldTargetSelectedIndex), reachedTargetIndex, self.targetSelectedIndex, reselectingDuringRebuild)
        end

        if self.targetSelectedIndex and self.selectedIndex then
            local moveAmount = zo_abs(self.targetSelectedIndex - self.selectedIndex)
            if jumpType and moveAmount > 0 then
                self:SetJumping(true)
                self.onPlaySoundFunction(jumpType)
            end
        end

        if (not forceAnimation) and (not self.animationEnabled) then
            self:UpdateAnchors(self.targetSelectedIndex)
        end
    end
end

function ZO_ParametricScrollList:SetLastIndexSelected(jumpType)
    self:SetSelectedIndex(self:CalculateLastSelectableIndex(), nil, nil, jumpType)
end

function ZO_ParametricScrollList:SetFirstIndexSelected(jumpType)
    self:SetSelectedIndex(self:CalculateFirstSelectableIndex(), nil, nil, jumpType)
end

function ZO_ParametricScrollList:SetDefaultIndexSelected(animate, allowEvenIfDisabled, forceAnimation, jumpType)
    local index = self.defaultSelectedIndex or self:CalculateFirstSelectableIndex()
    if animate then
        self:SetSelectedIndex(index, allowEvenIfDisabled, forceAnimation, jumpType)
    else
        self:SetSelectedIndexWithoutAnimation(index, allowEvenIfDisabled, forceAnimation)
    end
end

function ZO_ParametricScrollList:CalculateFirstSelectableIndex()
    for i = 1, #self.dataList do
        if self:CanSelect(i) then
            return i
        end
    end
    return 1
end

function ZO_ParametricScrollList:CalculateLastSelectableIndex()
    for i = #self.dataList, 1, -1 do
        if self:CanSelect(i) then
            return i
        end
    end
    return #self.dataList
end

function ZO_ParametricScrollList:SetSelectedDataByEval(eval)
    self:SetSelectedDataByRangedEval(eval, 1, #self.dataList)
end

function ZO_ParametricScrollList:SetPreviousSelectedDataByEval(eval, jumpType)
    local selectedIndex = self.targetSelectedIndex or self.selectedIndex
    if selectedIndex then
        return self:SetSelectedDataByRangedEval(eval, zo_max(selectedIndex - 1, 1), 1, jumpType)
    end
end

function ZO_ParametricScrollList:SetNextSelectedDataByEval(eval, jumpType)
    local selectedIndex = self.targetSelectedIndex or self.selectedIndex
    if selectedIndex then
        return self:SetSelectedDataByRangedEval(eval, zo_min(selectedIndex + 1, #self.dataList), #self.dataList, jumpType)
    end
end

function ZO_ParametricScrollList:GetNextSelectableIndex(currentIndex)
    local newIndex = currentIndex + 1
    while (newIndex <= #self.dataList) and (self:CanSelect(newIndex) == false) do
        newIndex = newIndex + 1
    end
    return newIndex
end

function ZO_ParametricScrollList:SetSelectedDataByRangedEval(eval, startIndex, endIndex, jumpType)
    local direction = startIndex < endIndex and 1 or -1
    for i = startIndex, endIndex, direction do
        local data = self.dataList[i]
        if CanSelectData(data) and eval(data) then
            self:SetSelectedIndex(i, nil, nil, jumpType)
            return true
        end
    end
    return false
end

local function ReleaseAllControls(self)
    self.visibleControls = {}
    self.dataIndexToControl = {}
    self.unseenControls = {}
    for templateName, dataTypeInfo in pairs(self.dataTypes) do
        dataTypeInfo.pool:ReleaseAllObjects()
    end
end

local function MoveSelectedToOldSelected(self)
    if self.targetSelectedIndex then
        self.oldSelectedData = self.selectedData
        self.oldSelectedDataTemplate = self.templateList and self.templateList[self.selectedIndex]
        self.oldSelectedIndex = self.targetSelectedIndex
        self.selectedIndex = nil
        self.targetSelectedIndex = nil
        self.selectedData = nil
    end
end

function ZO_ParametricScrollList:Clear()
    MoveSelectedToOldSelected(self)

    self.lastContinousTargetOffset = nil
    self:SetMoving(false)
    self.dataList = {}
    self.templateList = {}
    self.prePadding = {}
    self.postPadding = {}
    self.preSelectedOffsetAdditionalPadding = {}
    self.postSelectedOffsetAdditionalPadding = {}
    self.selectedCenterOffset = {}

    ReleaseAllControls(self)

    if self.currentCommitHistoryKey then
        self.commitHistoryDictionary[self.currentCommitHistoryKey] = { data = self.oldSelectedData, template = self.oldSelectedDataTemplate, selectedIndex = self.oldSelectedIndex }
    end

    if self.onClearedFunction then
        self.onClearedFunction(self)
    end
end

local function FindMatchingIndex(oldSelectedData, newDataList, equalityFunction, oldSelectedIndex)
    for i = oldSelectedIndex, #newDataList do
        if equalityFunction(oldSelectedData, newDataList[i]) then
            return i
        end
    end

    for i = zo_min(oldSelectedIndex - 1, #newDataList), 1, -1 do
        if equalityFunction(oldSelectedData, newDataList[i]) then
            return i
        end
    end

    return nil
end

function ZO_ParametricScrollList:SetKeyForNextCommit(key)
    self.nextCommitHistoryKey = key
end

function ZO_ParametricScrollList:CommitWithoutReselect()
    local NO_RESELECT = true
    self:Commit(NO_RESELECT)
end

function ZO_ParametricScrollList:Commit(dontReselect, blockSelectionChangedCallback)
    self.lastContinousTargetOffset = nil
    self:SetMoving(false)
    -- Order matters for the following functions; resetting our controls after MoveSelectedToOldSelected wipes the information about our selected data risks a UI error (ESO-644830).
    -- The OnRectChange callback in SetHandleDynamicViewProperties involves calling UpdateAnchors which will explode if we don't have a valid selected index.
    ReleaseAllControls(self)
    MoveSelectedToOldSelected(self)

    self.validGradientDirty = true
    local dataListSize = #self.dataList
    local hasItems = dataListSize > 0
    if hasItems then
        if self.sortFunction then
            table.sort(self.dataList, self.sortFunction)
        end

        local nextSelectedIndex = nil
        if dontReselect or self.reselectBehavior == ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR.RESET_TO_DEFAULT then
            nextSelectedIndex = self.defaultSelectedIndex
        else
            if self.nextCommitHistoryKey then
                local nextCommitHistory = self.commitHistoryDictionary[self.nextCommitHistoryKey]
                if nextCommitHistory then
                    nextSelectedIndex = nextCommitHistory.selectedIndex
                end
            else
                nextSelectedIndex = self.oldSelectedIndex
            end

            nextSelectedIndex = nextSelectedIndex or self.defaultSelectedIndex

            if self.reselectBehavior == ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR.MATCH_OR_RESELECT_OLD_INDEX or self.reselectBehavior == ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR.MATCH_OR_RESET_TO_DEFAULT then
                local oldSelectedData, oldSelectedDataTemplate

                if self.nextCommitHistoryKey and self.nextCommitHistoryKey ~= self.currentCommitHistoryKey then
                    local nextCommitHistory = self.commitHistoryDictionary[self.nextCommitHistoryKey]
                    if nextCommitHistory then
                        oldSelectedData, oldSelectedDataTemplate = nextCommitHistory.data, nextCommitHistory.template
                    end
                else
                    oldSelectedData, oldSelectedDataTemplate = self.oldSelectedData, self.oldSelectedDataTemplate
                end

                if oldSelectedDataTemplate then
                    local equalityFunction = self.dataTypes[oldSelectedDataTemplate].equalityFunction
                    local matchingIndex = FindMatchingIndex(oldSelectedData, self.dataList, equalityFunction, nextSelectedIndex)
                    if matchingIndex then
                        nextSelectedIndex = matchingIndex
                    elseif self.reselectBehavior ==  ZO_PARAMETRIC_SCROLL_LIST_RESELECT_BEHAVIOR.MATCH_OR_RESET_TO_DEFAULT then
                        nextSelectedIndex = self.defaultSelectedIndex
                    end
                    -- If neither condition was met, it was already set to the oldSelectedIndex/history key info (MATCH_OR_RESELECT_OLD_INDEX)
                end
            end
        end

        while (nextSelectedIndex <= dataListSize) and (self:CanSelect(nextSelectedIndex) == false) do
            nextSelectedIndex = nextSelectedIndex + 1
        end
        if nextSelectedIndex > dataListSize then
            nextSelectedIndex = dataListSize
        end

        local ALLOW_EVEN_IF_DISABLED = true
        local FORCE_ANIMATION = true
        local DEFAULT_JUMP_TYPE = nil
        local RESELECTING_DURING_REBUILD = true
        self:SetSelectedIndex(nextSelectedIndex, ALLOW_EVEN_IF_DISABLED, FORCE_ANIMATION, DEFAULT_JUMP_TYPE, blockSelectionChangedCallback, RESELECTING_DURING_REBUILD)

        local INITIAL_UPDATE = true
        self:UpdateAnchors(self:CalculateSelectedIndexOffsetWithDrag(), INITIAL_UPDATE, RESELECTING_DURING_REBUILD, blockSelectionChangedCallback)

        -- If the selectedData is set in the setup function (such as for the header), the code above
        --  may have picked a non-selectable item if this is the first time the items were setup. As
        --  such, we need to check for that condition, and move to the next item if we hit it.
        if not CanSelectData(self.selectedData) then
            -- NOTE: MoveNext() will skip over additional unselectable items internally.
            if matchingIndex == dataListSize then
                self:MovePrevious()
            else
                self:MoveNext()
            end
        end
    end

    local oldSelectedData = self.oldSelectedData
    self.oldSelectedData = nil
    self.oldSelectedDataTemplate = nil
    self.oldSelectedIndex = nil

    if self.noItemsLabel then
        self.noItemsLabel:SetHidden(hasItems)
    end

    local hideArrows = dataListSize <= 1
    if self.previousArrow then
        self.previousArrow:SetHidden(hideArrows)
    end
    if self.nextArrow then
        self.nextArrow:SetHidden(hideArrows)
    end

    self.currentCommitHistoryKey, self.nextCommitHistoryKey = self.nextCommitHistoryKey, nil

    if hasItems then
        if self.onCommitWithItemsFunction then
            self.onCommitWithItemsFunction(self)
        end
    else
        if blockSelectionChangedCallback ~= true then
            self:FireCallbacks("SelectedDataChanged", self, nil, oldSelectedData, nil, self.targetSelectedIndex)
        end
    end
end

function ZO_ParametricScrollList:GetSelectedData()
    return self.selectedData
end

function ZO_ParametricScrollList:GetTargetData()
    local targetIndex = self:CalculateSelectedIndexOffsetWithDrag()
    return self:GetDataForDataIndex(targetIndex)
end

function ZO_ParametricScrollList:GetTargetIndex()
    return self:CalculateSelectedIndexOffsetWithDrag()
end

function ZO_ParametricScrollList:GetTargetControl()
    local targetIndex = self:CalculateSelectedIndexOffsetWithDrag()
    return self.dataIndexToControl[targetIndex]
end

function ZO_ParametricScrollList:GetControl()
    return self.control
end

function ZO_ParametricScrollList:GetScrollControl()
    return self.scrollControl
end

function ZO_ParametricScrollList:SetPlaySoundFunction(fn)
    self.onPlaySoundFunction = fn
end

function ZO_ParametricScrollList:SetMouseEnabled(mouseEnabled)
    self.control:SetMouseEnabled(mouseEnabled)
end

--[[ Private API ]]--
local function GetControlDimensionForMode(mode, control)
    return mode == PARAMETRIC_SCROLL_LIST_VERTICAL and control:GetHeight() or control:GetWidth()
end

local function TransformAnchorOffsetsForMode(mode, offsetX, offsetY)
    if mode == PARAMETRIC_SCROLL_LIST_VERTICAL then
        return offsetY, offsetX
    end
    return offsetX, offsetY
end

local function GetStartOfControl(mode, control)
    return mode == PARAMETRIC_SCROLL_LIST_VERTICAL and control:GetTop() or control:GetLeft()
end

local function GetEndOfControl(mode, control)
    return mode == PARAMETRIC_SCROLL_LIST_VERTICAL and control:GetBottom() or control:GetRight()
end

function ZO_ParametricScrollList:CalculateSelectedIndexOffsetWithDrag()
    return self.targetSelectedIndex or self.selectedIndex or 0
end

function ZO_ParametricScrollList:OnUpdate()
    if #self.dataList > 0 and self.lastContinousTargetOffset then
        local continousTargetOffset = self:CalculateSelectedIndexOffsetWithDrag()
        if self.dragging then
            self:SetMoving(true)
            self:UpdateAnchors(continousTargetOffset)
        elseif zo_abs(self.lastContinousTargetOffset - continousTargetOffset) > .01 then
            self:SetMoving(true)
            self:UpdateAnchors(self:CalculateNextLerpedContinousOffset(continousTargetOffset))
        elseif self.isMoving then
            self:SetMoving(false)
            self:UpdateAnchors(continousTargetOffset)
        end
    end
end

local SELECTION_LERP_RATE = 0.2
function ZO_ParametricScrollList:CalculateNextLerpedContinousOffset(continousTargetOffset)
    return zo_deltaNormalizedLerp(self.lastContinousTargetOffset, continousTargetOffset, SELECTION_LERP_RATE)
end

function ZO_ParametricScrollList:GetSelectedControl()
    return self.dataIndexToControl[self.selectedIndex]
end

function ZO_ParametricScrollList:EnableAnimation(enabled)
    self.animationEnabled = enabled
end

function ZO_ParametricScrollList:IsDirectionalInputEnabled()
    return self.directionalInputEnabled
end

function ZO_ParametricScrollList:SetDirectionalInputEnabled(enabled)
    self.directionalInputEnabled = enabled
    if enabled then
        DIRECTIONAL_INPUT:Activate(self, self.control)
    else
        DIRECTIONAL_INPUT:Deactivate(self)
    end
end

function ZO_ParametricScrollList:UpdateDirectionalInput()
    local result = self.movementController:CheckMovement()

    if self.customDirectionalInputHandler and self.customDirectionalInputHandler(result) then
        return
    end

    if result == MOVEMENT_CONTROLLER_MOVE_NEXT then
        self:MoveNext()
    elseif result == MOVEMENT_CONTROLLER_MOVE_PREVIOUS then
        self:MovePrevious()
    end
end

--Will fire a callback with the directional input result
--Optionally you can return true to consume the result before the list processes it
function ZO_ParametricScrollList:SetCustomDirectionInputHandler(handler)
    self.customDirectionalInputHandler = handler
end

function ZO_ParametricScrollList:SetHideUnselectedControls(state)
    self.hideUnselectedControls = state
end

function ZO_ParametricScrollList:SetAnchorForEntryControl(control, anchor1, anchor2, offsetX, offsetY) 
    local anchorTo = self.control

    if self.alignToScreenCenter then
        anchorTo = self.alignToScreenCenterAnchor
    end

    control:ClearAnchors()
    control:SetAnchor(anchor1, anchorTo, anchor2, offsetX, offsetY)
end

function ZO_ParametricScrollList:SetEntryAnchors(entryAnchors)
    self.entryAnchors = entryAnchors
end

function ZO_ParametricScrollList:GetDesiredEntryAnchors()
    if self.entryAnchors then
       return self.entryAnchors[1], self.entryAnchors[2]
    elseif self.mode == PARAMETRIC_SCROLL_LIST_VERTICAL then
        if self.anchorOppositeSide then
            return BOTTOM, CENTER
        else
            return TOP, CENTER
        end
    else
        return TOP, TOP
    end
end

function ZO_ParametricScrollList:GetEntryFixedCenterOffset()
    if self.alignToScreenCenter then
        return self.fixedCenterOffset - self.alignToScreenCenterExpectedEntryHalfHeight
    end
    return self.fixedCenterOffset
end

local SELECTED = true
local UNSELECTED = false

function ZO_ParametricScrollList:UpdateAnchors(continousTargetOffset, initialUpdate, reselectingDuringRebuild, blockSelectionChangedCallback)
    self.visibleControls, self.unseenControls = self.unseenControls, self.visibleControls
    ZO_ClearTable(self.visibleControls)

    -- Find center control
    local newSelectedDataIndex = zo_round(continousTargetOffset)

    local centerControl, justCreated = self:AcquireControlAtDataIndex(newSelectedDataIndex)
    self.unseenControls[centerControl] = nil
    self.visibleControls[centerControl] = true
    local selectedData = self:GetDataForDataIndex(newSelectedDataIndex)
    local selectedDataChanged = self.selectedIndex ~= newSelectedDataIndex
    local oldSelectedData = self.selectedData

    if self.soundEnabled and not self.jumping and selectedDataChanged and oldSelectedData then
        if newSelectedDataIndex > self.selectedIndex then
            self.onPlaySoundFunction(ZO_PARAMETRIC_MOVEMENT_TYPES.MOVE_NEXT)
        else
            self.onPlaySoundFunction(ZO_PARAMETRIC_MOVEMENT_TYPES.MOVE_PREVIOUS)
        end
    end

    self.selectedData = selectedData
    self.selectedIndex = newSelectedDataIndex

    if justCreated or selectedDataChanged or initialUpdate then
        self:RunSetupOnControl(centerControl, self.selectedIndex, SELECTED, reselectingDuringRebuild, self.enabled, self.active)
    end

    local fixedCenterOffset = self:GetEntryFixedCenterOffset()

    local centerControlDimension = GetControlDimensionForMode(self.mode, centerControl)
    local baseOffset = newSelectedDataIndex - continousTargetOffset
    local preCenterPadding, postCenterPadding = self:GetPaddingForDataIndex(newSelectedDataIndex, 0, baseOffset)
    local centerOffset = centerControlDimension * baseOffset
    local centerSelectedOffsetAdditionalPrePadding, centerSelectedOffsetAdditionalPostPadding = self:GetSelectedAdditionalPaddingForDataIndex(newSelectedDataIndex)
    local centerSelectedOffset = self.selectedCenterOffset[newSelectedDataIndex]

    local parametricFunction = self:GetParametricFunctionForDataIndex(newSelectedDataIndex)
    if parametricFunction then
        parametricFunction(centerControl, 0, baseOffset)
    end
    
    local entryAnchor1, entryAnchor2 = self:GetDesiredEntryAnchors()

    local centerOffsetX, centerOffsetY = TransformAnchorOffsetsForMode(self.mode, centerOffset * self.centerDampingFactor + fixedCenterOffset + centerSelectedOffset * (1 - baseOffset), 0)
    self:SetAnchorForEntryControl(centerControl, entryAnchor1, entryAnchor2, centerOffsetX, centerOffsetY)

    if not self.hideUnselectedControls then
        -- Layout items before the center
        do
            local prevControlOffsets = centerOffset - (self.anchorOppositeSide and centerControlDimension or 0) + preCenterPadding + centerSelectedOffset * (1 - baseOffset)
            local startOfScrollContainer = GetStartOfControl(self.mode, self.control)
            for dataIndex = newSelectedDataIndex - 1, 1, -1 do
                local control = self:AcquireAndSetupControl(dataIndex, selectedDataChanged, initialUpdate, oldSelectedData, UNSELECTED, reselectingDuringRebuild)

                local distanceFromCenter = newSelectedDataIndex - dataIndex
                local preSelectedOffsetAdditionalPadding, _ = self:GetSelectedAdditionalPaddingForDataIndex(dataIndex)
                local parametricOffset = self:CalculateParametricOffset(centerSelectedOffsetAdditionalPrePadding, preSelectedOffsetAdditionalPadding, distanceFromCenter, baseOffset)
    
                local controlDimension = GetControlDimensionForMode(self.mode, control)
                local prePadding, postPadding = self:GetPaddingForDataIndex(dataIndex, distanceFromCenter, baseOffset)

                local parametricFunction = self:GetParametricFunctionForDataIndex(dataIndex)
                if parametricFunction then
                    parametricFunction(control, distanceFromCenter, baseOffset)
                end

                prevControlOffsets = prevControlOffsets - (self.anchorOppositeSide and 0 or controlDimension) - parametricOffset - postPadding
                self:SetAnchorForEntryControl(control, entryAnchor1, entryAnchor2, TransformAnchorOffsetsForMode(self.mode, prevControlOffsets + fixedCenterOffset, 0))
                prevControlOffsets = prevControlOffsets - prePadding - (self.anchorOppositeSide and controlDimension or 0) 

                if GetStartOfControl(self.mode, control) <= startOfScrollContainer then
                    break
                end
            end
        end

        -- Layout items after the center
        do
            local prevControlOffsets = centerOffset + (self.anchorOppositeSide and 0 or centerControlDimension) + postCenterPadding + centerSelectedOffset * (1 - baseOffset)
            local endOfScrollContainer = GetEndOfControl(self.mode, self.control)
            for dataIndex = newSelectedDataIndex + 1, #self.dataList do
                local control = self:AcquireAndSetupControl(dataIndex, selectedDataChanged, initialUpdate, oldSelectedData, UNSELECTED, reselectingDuringRebuild)

                local distanceFromCenter = newSelectedDataIndex - dataIndex
                local _, postSelectedOffsetAdditionalPadding = self:GetSelectedAdditionalPaddingForDataIndex(dataIndex)
                local parametricOffset = self:CalculateParametricOffset(centerSelectedOffsetAdditionalPostPadding, postSelectedOffsetAdditionalPadding, distanceFromCenter, baseOffset)
                local additionalBottomParametricOffset = self:CalculateAdditionalBottomParametricOffset(distanceFromCenter, baseOffset)

                local prePadding, postPadding = self:GetPaddingForDataIndex(dataIndex, distanceFromCenter, baseOffset)

                local parametricFunction = self:GetParametricFunctionForDataIndex(dataIndex)
                if parametricFunction then
                    parametricFunction(control, distanceFromCenter, baseOffset)
                end

                local controlDimension = GetControlDimensionForMode(self.mode, control)

                prevControlOffsets = prevControlOffsets + parametricOffset + prePadding + additionalBottomParametricOffset + (self.anchorOppositeSide and controlDimension or 0)

                self:SetAnchorForEntryControl(control, entryAnchor1, entryAnchor2, TransformAnchorOffsetsForMode(self.mode, prevControlOffsets + fixedCenterOffset, 0))

                prevControlOffsets = prevControlOffsets + (self.anchorOppositeSide and 0 or controlDimension) + postPadding

                if GetEndOfControl(self.mode, control) >= endOfScrollContainer then
                    break
                end
            end
        end
    end

    -- Remove unseen controls
    do
        for control in pairs(self.unseenControls) do
            self.unseenControls[control] = nil
            self:ReleaseControl(control)
        end
    end

    self.lastContinousTargetOffset = continousTargetOffset

    if selectedDataChanged and blockSelectionChangedCallback ~= true then
        local reachedTarget = (newSelectedDataIndex == self:CalculateSelectedIndexOffsetWithDrag())
        self:FireCallbacks("SelectedDataChanged", self, self.selectedData, oldSelectedData, reachedTarget, self.targetSelectedIndex)

        self:UpdateScrollArrows()

        self:SetJumping(not reachedTarget)
    end

    self:EnsureValidGradient()
end

function ZO_ParametricScrollList:RefreshNoItemLabelPosition()
    if self.noItemsLabel then
        local halfHeightOffset = self.noItemsLabel:GetTextHeight() / 2

        local entryAnchor1, entryAnchor2 = self:GetDesiredEntryAnchors()

        local centerOffsetX, centerOffsetY = TransformAnchorOffsetsForMode(self.mode, self.fixedCenterOffset - halfHeightOffset, 0)

        self:SetAnchorForEntryControl(self.noItemsLabel, entryAnchor1, entryAnchor2, centerOffsetX, centerOffsetY)
    end
end

local HAND_OFF_SPEED_FACTOR = 2
local function CalculateStandardOffset(distanceFromCenter, continuousParametricOffset, min, max, startAdditionalPadding, endAdditionalPadding, additionalPaddingEasingFunc)
    local parametricValue = zo_abs(zo_clamp(distanceFromCenter - continuousParametricOffset, -2, 2))


    local hasAdditionalPadding = (endAdditionalPadding ~= 0 or startAdditionalPadding ~= 0) and distanceFromCenter >= -1 and distanceFromCenter <= 1

    -- If easing function wasn't specified for additional padding, use the default
    if not additionalPaddingEasingFunc then
        additionalPaddingEasingFunc = ZO_EaseInQuartic
    end

    if parametricValue > 1 then
        local additionalPadding = hasAdditionalPadding and zo_lerp(startAdditionalPadding, endAdditionalPadding, zo_saturate(additionalPaddingEasingFunc(parametricValue - 1))) or 0
        return zo_lerp(max, min, zo_saturate(ZO_EaseInQuartic(parametricValue - 1) * HAND_OFF_SPEED_FACTOR)) + additionalPadding
    end

    local additionalPadding = hasAdditionalPadding and zo_lerp(endAdditionalPadding, startAdditionalPadding, zo_saturate(additionalPaddingEasingFunc(parametricValue))) or 0
    return zo_lerp(min, max, zo_saturate(ZO_EaseInQuartic(parametricValue) * HAND_OFF_SPEED_FACTOR)) + additionalPadding
end

function ZO_ParametricScrollList:CalculateParametricOffset(startAdditionalPadding, endAdditionalPadding, distanceFromCenter, continuousParametricOffset, additionalPaddingEasingFunc)
    return CalculateStandardOffset(distanceFromCenter, continuousParametricOffset, self.minOffset, self.maxOffset, startAdditionalPadding, endAdditionalPadding, additionalPaddingEasingFunc)
end

function ZO_ParametricScrollList:CalculateAdditionalBottomParametricOffset(distanceFromCenter, continuousParametricOffset, additionalPaddingEasingFunc)
    return CalculateStandardOffset(distanceFromCenter, continuousParametricOffset, self.additonalMinBottomOffset, self.additonalMaxBottomOffset, 0, 0, additionalPaddingEasingFunc)
end

function ZO_ParametricScrollList:GetSetupFunctionForDataIndex(dataIndex)
    local templateName = self.templateList[dataIndex]
    return self.dataTypes[templateName].setupFunction
end

function ZO_ParametricScrollList:RunSetupOnControl(control, dataIndex, selected, reselectingDuringRebuild, enabled, active)
    local setupFunction = self:GetSetupFunctionForDataIndex(dataIndex)
    local data = self:GetDataForDataIndex(dataIndex)
    setupFunction(control, data, selected, reselectingDuringRebuild, enabled, active)
    local dataTypeInfo = self.dataTypes[control.templateName]
    if dataTypeInfo.headerSetupFunction then
        dataTypeInfo.headerSetupFunction(control:GetNamedChild("Header"), data, selected, reselectingDuringRebuild, enabled, active)
    end
end

function ZO_ParametricScrollList:GetParametricFunctionForDataIndex(dataIndex)
    local templateName = self.templateList[dataIndex]
    return self.dataTypes[templateName].parametricFunction
end

function ZO_ParametricScrollList:GetDataForDataIndex(dataIndex)
    return self.dataList[dataIndex]
end

function ZO_ParametricScrollList:GetControlFromData(data)
    for i = 1, #self.dataList do
        if self.dataList[i] == data then
            return self.dataIndexToControl[i]
        end
    end
end

function ZO_ParametricScrollList:GetAllVisibleControls()
    return self.visibleControls
end

function ZO_ParametricScrollList:SetHeaderPadding(defaultPadding, selectedPadding)
    self.headerDefaultPadding = defaultPadding
    self.headerSelectedPadding = selectedPadding
end

function ZO_ParametricScrollList:GetHasHeaderForDataIndex(dataIndex)
    local templateInfo = self.dataTypes[self.templateList[dataIndex]]
    local nextTemplateInfo = self.dataTypes[self.templateList[dataIndex + 1]]

    local isHeader = templateInfo and templateInfo.hasHeader
    local nextHeader = nextTemplateInfo and nextTemplateInfo.hasHeader

    return isHeader, nextHeader
end

function ZO_ParametricScrollList:GetSelectedAdditionalPaddingForDataIndex(dataIndex)
    local preSelectedOffsetAdditionalPadding = self.preSelectedOffsetAdditionalPadding[dataIndex]
    local postSelectedOffsetAdditionalPadding = self.postSelectedOffsetAdditionalPadding[dataIndex]

    if (not preSelectedOffsetAdditionalPadding) or (not postSelectedOffsetAdditionalPadding) then
        local isHeader, isNextHeader = self:GetHasHeaderForDataIndex(dataIndex)

        preSelectedOffsetAdditionalPadding = preSelectedOffsetAdditionalPadding or (isHeader and self.headerSelectedPadding) or 0
        postSelectedOffsetAdditionalPadding = postSelectedOffsetAdditionalPadding or (isNextHeader and self.headerSelectedPadding) or 0
    end

    return preSelectedOffsetAdditionalPadding, postSelectedOffsetAdditionalPadding
end

function ZO_ParametricScrollList:GetPaddingForDataIndex(dataIndex, distanceFromCenter, continousParametricOffset)
    local prePadding = self.prePadding[dataIndex]
    local postPadding = self.postPadding[dataIndex]
    if not postPadding then
        local isHeader, isNextHeader = self:GetHasHeaderForDataIndex(dataIndex)
        postPadding = postPadding or (isNextHeader and self.headerDefaultPadding) or 0
    end

    local rawParametricValue = zo_clamp(distanceFromCenter - continousParametricOffset, -1, 1)
    if rawParametricValue == 0 then
        return prePadding + self.universalPrePadding, postPadding + self.universalPostPadding
    end

    local parametricValue = zo_abs(rawParametricValue)
    if parametricValue > .5 then
        parametricValue = 1 - (parametricValue - .5) / .5
    else
        parametricValue = parametricValue * 2
    end

    local preParametricValue, postParametricValue
    if rawParametricValue < 0 then
        preParametricValue = parametricValue
        postParametricValue = 0
    else
        preParametricValue = 0
        postParametricValue = parametricValue
    end

    return zo_lerp(prePadding, prePadding * .25, preParametricValue) + self.universalPrePadding, zo_lerp(postPadding, postPadding * .25, postParametricValue) + self.universalPostPadding
end


local function HasEditControl(control)
    if control:GetType() == CT_EDITBOX then
        return true
    else
        local numChildren = control:GetNumChildren()
        if numChildren > 0 then
            for i = 1, numChildren do
                local child = control:GetChild(i)
                if child and HasEditControl(child) then
                    return true
                end
            end
        end

        return false
    end
end

function ZO_ParametricScrollList:AcquireControlAtDataIndex(dataIndex)
    do
        local control = self.dataIndexToControl[dataIndex]
        if control then
            self.unseenControls[control] = nil
            self.visibleControls[control] = true
            return control, false
        end
    end

    local templateName = self.templateList[dataIndex]
    local control, key = self.dataTypes[templateName].pool:AcquireObject()
    control.key = key
    control.templateName = templateName
    control.dataIndex = dataIndex

    --Check if one of the children of this control is an edit box the first time we create one of these
    if self.dataTypes[templateName].hasEditControl == nil then
        self.dataTypes[templateName].hasEditControl = HasEditControl(control)
    end

    self.dataIndexToControl[dataIndex] = control
    self.visibleControls[control] = true

    return control, true
end

function ZO_ParametricScrollList:ReleaseControl(control)
    local templateName = control.templateName

    self.dataIndexToControl[control.dataIndex] = nil
    
    local pool = self.dataTypes[templateName].pool
    pool:ReleaseObject(control.key)
end

function ZO_ParametricScrollList:AcquireAndSetupControl(dataIndex, selectedDataChanged, initialUpdate, oldSelectedData, selected, reselectingDuringRebuild)
    local control, justCreated = self:AcquireControlAtDataIndex(dataIndex)
    local data = self:GetDataForDataIndex(dataIndex)
    if justCreated or initialUpdate or (selectedDataChanged and oldSelectedData == data) then
        self:RunSetupOnControl(control, dataIndex, selected, reselectingDuringRebuild, self.enabled, self.active)
    end

    return control
end

local function FindEditControl(control)
    if control:GetType() == CT_EDITBOX then
        return control
    else
        local numChildren = control:GetNumChildren()
        if numChildren > 0 then
            for i = 1, numChildren do
                local child = control:GetChild(i)
                local editControl = FindEditControl(child)
                if(editControl ~= nil) then
                    return FindEditControl(child)
                end
            end
        end

        return nil
    end
end


function ZO_ParametricScrollList:SetMoving(isMoving)
    if self.isMoving ~= isMoving then
        self.isMoving = isMoving

        --check for edit controls that need to be defocused before moving the current selection
        if(isMoving and self:DoesTemplateHaveEditBox(self.selectedIndex)) then
            local currentSelectedControl = self:GetSelectedControl()
            local editControl = FindEditControl(currentSelectedControl)
            editControl:LoseFocus()
        end

        self:FireCallbacks("MovementChanged", self, isMoving)

        if self.selectionHighlightControl then
            self.selectionHighlightControl:SetHidden(isMoving)
        end

        if not isMoving then
            self:UpdateScrollArrows()
        end
    end
end

function ZO_ParametricScrollList:DoesTemplateHaveEditBox(dataIndex)
    local templateName = self.templateList[dataIndex]
    return self.dataTypes[templateName].hasEditControl
end

--Set this to true if you plan to change the list size or view properties (like center offsets) between commits. For example, if the list is anchored on the top and bottom and changes size when the user resizes the screen.
function ZO_ParametricScrollList:SetHandleDynamicViewProperties(handleDynamicViewProperties)
    if self.mode == PARAMETRIC_SCROLL_LIST_VERTICAL then
        if handleDynamicViewProperties ~= self.handleDynamicViewProperties then
            self.handleDynamicViewProperties = handleDynamicViewProperties
            if handleDynamicViewProperties then
                self.scrollControl:SetHandler("OnRectChanged", function(scrollControl, newLeft, newTop, newRight, newBottom, oldLeft, oldTop, oldRight, oldBottom)
                    --if the edges of the scroll area changed we need to fix up the gradients
                    if not zo_floatsAreEqual(newTop, oldTop) or not zo_floatsAreEqual(newBottom, oldBottom) then
                        --If the list has less than ZO_VERTICAL_PARAMETRIC_LIST_DEFAULT_FADE_GRADIENT_SIZE UI units from its center control to the scroll top or bottom then automatically size the fade gradients to not overlap the center control.
                        self.validGradientDirty = true

                        --If the list changes height we need to update anchors because that function is responsible for hiding controls that went out of view or showing controls that are now in view.
                        self:RefreshVisible()
                    end
                end)
            else
                self.scrollControl:SetHandler("OnRectChanged", nil)
            end
        end
    end
end

function ZO_ParametricScrollList:EnsureValidGradient()
    if self.handleDynamicViewProperties and self.validGradientDirty then
        local listStart = GetStartOfControl(self.mode, self.scrollControl)
        local listEnd = GetEndOfControl(self.mode, self.scrollControl)
        local listMid = listStart + (GetControlDimensionForMode(self.mode, self.scrollControl) / 2.0)
        if self.alignToScreenCenter and self.alignToScreenCenterAnchor then
            listMid = GetStartOfControl(self.mode, self.alignToScreenCenterAnchor)
        end

        listMid = listMid + self.fixedCenterOffset

        local hasHeaders = false
        for templateName, dataTypeInfo in pairs(self.dataTypes) do
            if dataTypeInfo.hasHeader then
                hasHeaders = true
                break
            end
        end

        local selectedControlBufferStart = 0
        if hasHeaders then
            selectedControlBufferStart = selectedControlBufferStart - self.headerSelectedPadding + DEFAULT_EXPECTED_HEADER_HEIGHT
        end

        local selectedControlBufferEnd = DEFAULT_EXPECTED_ENTRY_HEIGHT
        if self.alignToScreenCenterExpectedEntryHalfHeight then
            selectedControlBufferEnd = self.alignToScreenCenterExpectedEntryHalfHeight * 2.0
        end

        -- Have some small minimum effect
        local MINIMUM_ALLOWED_FADE_GRADIENT = 32
        local gradientMaxStart = zo_max(listMid - listStart - selectedControlBufferStart, MINIMUM_ALLOWED_FADE_GRADIENT)
        local gradientMaxEnd = zo_max(listEnd - listMid - selectedControlBufferEnd, MINIMUM_ALLOWED_FADE_GRADIENT)

        local gradientStartSize = zo_min(gradientMaxStart, ZO_VERTICAL_PARAMETRIC_LIST_DEFAULT_FADE_GRADIENT_SIZE)
        local gradientEndSize = zo_min(gradientMaxEnd, ZO_VERTICAL_PARAMETRIC_LIST_DEFAULT_FADE_GRADIENT_SIZE)

        local FIRST_FADE_GRADIENT = 1
        local SECOND_FADE_GRADIENT = 2
        local GRADIENT_TEX_CORD_0 = 0
        local GRADIENT_TEX_CORD_1 = 1
        local GRADIENT_TEX_CORD_NEG_1 = -1
        self.scrollControl:SetFadeGradient(FIRST_FADE_GRADIENT, GRADIENT_TEX_CORD_0, GRADIENT_TEX_CORD_1, gradientStartSize)
        self.scrollControl:SetFadeGradient(SECOND_FADE_GRADIENT, GRADIENT_TEX_CORD_0, GRADIENT_TEX_CORD_NEG_1, gradientEndSize)

        self.validGradientDirty = false
    end
end

function ZO_ParametricScrollList:SetGradient(gradientIndex, gradientSize)
    self.scrollControl:SetFadeGradient(gradientIndex, gradientSize)
end

function ZO_ParametricScrollList:SetJumping(isJumping)
    self.jumping = isJumping
end

function ZO_ParametricScrollList:SetSoundEnabled(isSoundEnabled)
    self.soundEnabled = isSoundEnabled
end

--in case you don't want your list to default to the first entry in the list!
function ZO_ParametricScrollList:SetDefaultSelectedIndex(defaultSelectedIndex)
    self.defaultSelectedIndex = defaultSelectedIndex
end

function ZO_ParametricScrollList:WhenInactiveSetTargetControlHidden(hidden)
    if not self.active then
        local targetControl = self:GetTargetControl()
        targetControl:SetHidden(hidden)
        if targetControl.headerControl then
            targetControl.headerControl:SetHidden(hidden)
        end
    end
end

function ZO_ParametricScrollList_OnMouseWheel(control, delta)
    if control.scrollList.active then
        if delta > 0 then
            control.scrollList:MovePrevious()
        else
            control.scrollList:MoveNext()
        end
    end
end