ZO_HorizontalScrollList = ZO_Object:Subclass()

ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES = 
{
    INITIAL_UPDATE = 1,
    MOVE_RIGHT = 2,
    MOVE_LEFT = 3,
    -- LAST allows derived classes to start their movement enumerations after the base movements 
    LAST = 4,
}

ZO_HORIZONTAL_SCROLL_LIST_DISPLAY_FIXED_NUMBER_OF_ENTRIES = 1
ZO_HORIZONTAL_SCROLL_LIST_ANCHOR_ENTRIES_AT_FIXED_DISTANCE = 2

function ZO_HorizontalScrollListPlaySound(type)
    if type == ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.INITIAL_UPDATE then
        PlaySound(SOUNDS.HOR_LIST_ITEM_SELECTED)
    elseif type == ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_RIGHT then
        PlaySound(SOUNDS.HOR_LIST_ITEM_SELECTED)
    elseif type == ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_LEFT then
        PlaySound(SOUNDS.HOR_LIST_ITEM_SELECTED)
    end
end

--[[ Public  API ]]--
function ZO_HorizontalScrollList:New(...)
    local horizontalScrollList = ZO_Object.New(self)
    horizontalScrollList:Initialize(...)
    return horizontalScrollList
end

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

local DEFAULT_NUM_VISIBLE_ENTRIES = 5

-- for best results numVisibleEntries should be odd
function ZO_HorizontalScrollList:Initialize(control, templateName, numVisibleEntries, setupFunction, equalityFunction, onCommitWithItemsFunction, onClearedFunction)
    self.control = control
    control.horizontalScrollList = self
    self.scrollControl = control:GetNamedChild("Scroll")

    self.setupFunction = setupFunction
    self.equalityFunction = equalityFunction or DefaultEqualityFunction
    self.onCommitWithItemsFunction = onCommitWithItemsFunction
    self.onClearedFunction = onClearedFunction

    self.numVisibleEntries = (numVisibleEntries or DEFAULT_NUM_VISIBLE_ENTRIES) + 2
    self.halfNumVisibleEntries = zo_floor(self.numVisibleEntries * .5)

    self.controlEntryWidth = control:GetWidth() / (self.numVisibleEntries - 2)
    self.offsetBetweenEntries = 0
    self.displayEntryType = ZO_HORIZONTAL_SCROLL_LIST_DISPLAY_FIXED_NUMBER_OF_ENTRIES

    self.leftArrow = self.control:GetNamedChild("LeftArrow")
    self.rightArrow = self.control:GetNamedChild("RightArrow")

    self.allowWrapping = false

    self.isMoving = false

    self.enabled = true

    self.onPlaySoundFunction = ZO_HorizontalScrollListPlaySound

    local function OnDragStart(clickedControl, button)
        if self.enabled and self:CanScroll() then
            self.dragging = true
            self.draggingXStart = GetUIMousePosition()
        end
    end

    local function OnDragStop()
        if self.dragging then
            self.dragging = false

            local totalDeltaX = GetUIMousePosition() - self.draggingXStart
            local lastFrameDeltaX = GetUIMouseDeltas() * 15

            self:SetSelectedIndex(zo_round((self:CalculateSelectedIndexOffset() + totalDeltaX + lastFrameDeltaX) / self.controlEntryWidth))
        end
    end

    local function OnMouseUp(clickedControl, button, upInside)
        if self.dragging then
            OnDragStop()
        elseif button == MOUSE_BUTTON_INDEX_LEFT and upInside and self.enabled then
            self:SelectControl(clickedControl)
            if self.onControlClicked then
                self.onControlClicked(clickedControl, button, upInside)
            end
        end
    end

    self.control:SetHandler("OnDragStart", OnDragStart)
    self.control:SetHandler("OnMouseUp", OnDragStop)

    self.controls = {}
    for i = 1, self.numVisibleEntries do
        local entryControl = CreateControlFromVirtual("$(parent)", self.scrollControl, templateName, i)
        entryControl:SetHandler("OnDragStart", OnDragStart)
        entryControl:SetHandler("OnMouseUp", OnMouseUp)
        self.controls[i] = entryControl
    end

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

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

    self:Clear()
end

function ZO_HorizontalScrollList:SetAllowWrapping(allowWrapping)
    self.allowWrapping = allowWrapping
end

function ZO_HorizontalScrollList:SetOnMovementChangedCallback(onMovementChangedCallback)
    self.onMovementChangedCallback = onMovementChangedCallback
end

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

        self.dragging = false

        self:RefreshVisible()
    end
end

function ZO_HorizontalScrollList:SetScaleExtents(minScale, maxScale)
    self.minScale = minScale
    self.maxScale = maxScale
end

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

function ZO_HorizontalScrollList:GetNoItemText()
    return self.noItemsText
end

function ZO_HorizontalScrollList:SetDisplayEntryType(displayEntryType)
    self.displayEntryType = displayEntryType
end

function ZO_HorizontalScrollList:SetOffsetBetweenEntries(offsetBetweenEntries)
    self.offsetBetweenEntries = offsetBetweenEntries
end

function ZO_HorizontalScrollList:IsMoving()
    return self.isMoving
end

function ZO_HorizontalScrollList:SetSelectionHighlightInfo(selectionHighlightControl, selectionHighlightAnimation)
    self.selectionHighlightControl = selectionHighlightControl
    self.selectionHighlightAnimation = selectionHighlightAnimation
end

-- use to override the calculated entry width when we have a scroll list
-- whose width is not equivalent to the number of visible entries
function ZO_HorizontalScrollList:SetEntryWidth(entryWidth)
    self.controlEntryWidth = entryWidth
end

function ZO_HorizontalScrollList:RefreshVisible()
    if #self.list > 0 then
        local INITIAL_UPDATE = true
        if self.isMoving then
            self:UpdateAnchors(self.lastPrimaryControlOffsetX, INITIAL_UPDATE)
        else
            local RESELECTING_DURING_REBUILD = true
            self:UpdateAnchors(self:CalculateSelectedIndexOffsetWithDrag(), INITIAL_UPDATE, RESELECTING_DURING_REBUILD)
        end
    end
end

function ZO_HorizontalScrollList:AddEntry(data)
    self.list[#self.list + 1] = data
end

function ZO_HorizontalScrollList:MoveLeft(isAutoScrollEvent)
    if self:CanScroll() then
        self:SetSelectedIndex((self.selectedIndex or 0) - 1, nil, nil, nil, isAutoScrollEvent)
    end
end

function ZO_HorizontalScrollList:MoveRight(isAutoScrollEvent)
    if self:CanScroll() then
        self:SetSelectedIndex((self.selectedIndex or 0) + 1, nil, nil, nil, isAutoScrollEvent)
    end
end

function ZO_HorizontalScrollList:SetSelectedIndex(selectedIndex, allowEvenIfDisabled, withoutAnimation, reselectingDuringRebuild, isAutoScrollEvent)
    if self.enabled or allowEvenIfDisabled then
        self.lastScrollTime = GetFrameTimeSeconds()
        self.lastInteractionAutomatic = isAutoScrollEvent

        if selectedIndex and not self.allowWrapping then
            selectedIndex = zo_clamp(selectedIndex, 1 - #self.list, 0)
        end

        if self.selectedIndex ~= selectedIndex then
            if self.onTargetDataChangedCallback then
                local oldData = self.targetData
                local targetIndex = self:CalculateDataIndexFromOffset(-selectedIndex)
                self.targetData = self.list[targetIndex]
                self.onTargetDataChangedCallback(self.targetData, oldData, reselectingDuringRebuild)
            end

            if self.selectedIndex and self.active then
                if selectedIndex > self.selectedIndex then
                    self.onPlaySoundFunction(ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_RIGHT)
                else
                    self.onPlaySoundFunction(ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_LEFT)
                end
            end
            self.selectedIndex = selectedIndex
        end
    end

    if withoutAnimation then
        self:UpdateAnchors(self:CalculateSelectedIndexOffsetWithDrag())
    end
end

function ZO_HorizontalScrollList:SetSelectedDataIndex(dataIndex, allowEvenIfDisabled, withoutAnimation)
    self:SetSelectedIndex(1 - dataIndex, allowEvenIfDisabled, withoutAnimation)
end

function ZO_HorizontalScrollList:Clear()
    self.lastPrimaryControlOffsetX = nil
    self.oldSelectedData = self.selectedData
    self.selectedIndex = nil
    self.selectedData = nil
    self:SetMoving(false)
    self.list = {}

    for i, entry in ipairs(self.controls) do
        entry:SetHidden(true)
    end
    if self.onClearedFunction then
        self.onClearedFunction(self)
    end
end

local function FindMatchingIndex(oldSelectedData, halfNumVisibleEntries, newDataList, equalityFunction)
    if oldSelectedData then
        for newDataIndex, newData in ipairs(newDataList) do
            if equalityFunction(oldSelectedData, newData) then
                return 1 - newDataIndex
            end
        end
    end
    return nil
end

function ZO_HorizontalScrollList:FindIndexFromData(oldSelectedData, equalityFunction)
    if (not equalityFunction) then
        equalityFunction = function (oldData, newData) return (oldData == newData) end
    end
    return FindMatchingIndex(oldSelectedData, self.halfNumVisibleEntries, self.list, equalityFunction)
end

function ZO_HorizontalScrollList:Commit()
    local hasItems = #self.list > 0
    if hasItems then
        for i, entry in ipairs(self.controls) do
            entry:SetHidden(false)
        end

        local matchingIndex = FindMatchingIndex(self.oldSelectedData, self.halfNumVisibleEntries, self.list, self.equalityFunction)
        local reselectingDuringRebuild = matchingIndex ~= nil
        local ALLOW_EVEN_IF_DISABLED = true
        self:SetSelectedIndex(matchingIndex or 0, ALLOW_EVEN_IF_DISABLED, reselectingDuringRebuild)

        local INITIAL_UPDATE = true
        self:UpdateAnchors(self:CalculateSelectedIndexOffsetWithDrag(), INITIAL_UPDATE, reselectingDuringRebuild)
    end

    self.oldSelectedData = nil

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

    local hideArrows = not self:CanScroll()
    self.leftArrow:SetHidden(hideArrows)
    self.rightArrow:SetHidden(hideArrows)

    if hasItems and self.onCommitWithItemsFunction then
        self.onCommitWithItemsFunction(self)
    end
end

function ZO_HorizontalScrollList:GetSelectedData()
    return self.selectedData
end

function ZO_HorizontalScrollList:GetControl()
    return self.control
end

function ZO_HorizontalScrollList:GetSelectedIndex()
    return self.selectedIndex
end

function ZO_HorizontalScrollList:GetCenterControl()
    return self.controls[self.halfNumVisibleEntries + 1]
end

function ZO_HorizontalScrollList:GetNumItems()
    return #self.list
end

function ZO_HorizontalScrollList:IsEmpty()
    return #self.list == 0
end

function ZO_HorizontalScrollList:CanScroll()
    return #self.list > 1
end

function ZO_HorizontalScrollList:ApplyTemplateToControls(template)
    for i, entry in ipairs(self.controls) do
        ApplyTemplateToControl(entry, template)
    end
end

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

function ZO_HorizontalScrollList:SetAutoScroll(movementType, autoScrollDuration, postInteractionDuration)
    self.autoScrollMovementType = movementType
    self.autoScrollDuration = autoScrollDuration
    self.autoScrollPostInteractionDuration = postInteractionDuration or autoScrollDuration
end

function ZO_HorizontalScrollList:ResetAutoScrollTimer()
    self.lastScrollTime = GetFrameTimeSeconds()
    self.lastInteractionAutomatic = false
end

--[[ Private API ]]--

function ZO_HorizontalScrollList:OnUpdate(currentFrameTimeSeconds)
    if #self.list > 0 and self.lastPrimaryControlOffsetX then
        local targetOffsetX = self:CalculateSelectedIndexOffsetWithDrag()
        if self.dragging then
            self:SetMoving(true)
            self:UpdateAnchors(targetOffsetX)
        elseif zo_abs(targetOffsetX - self.lastPrimaryControlOffsetX) > 2 then
            self:SetMoving(true)
            local xOffset = zo_deltaNormalizedLerp(self.lastPrimaryControlOffsetX, targetOffsetX, .2)
            self:UpdateAnchors(xOffset)
        elseif self.isMoving then
            self:SetMoving(false)
            self:UpdateAnchors(targetOffsetX)
        elseif self.autoScrollMovementType and self:CanScroll() then
            local currentDuration = self.lastInteractionAutomatic and self.autoScrollDuration or self.autoScrollPostInteractionDuration
            if currentFrameTimeSeconds > self.lastScrollTime + currentDuration then
                local IS_AUTO_EVENT = true
                if self.autoScrollMovementType == ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_LEFT then
                    self:MoveLeft(IS_AUTO_EVENT)
                elseif self.autoScrollMovementType == ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.MOVE_RIGHT then
                    self:MoveRight(IS_AUTO_EVENT)
                end
            end
        end
    end
end

function ZO_HorizontalScrollList:CalculateSelectedIndexOffset()
    if self.selectedIndex then
        return self.selectedIndex * self.controlEntryWidth
    end
    return 0
end

function ZO_HorizontalScrollList:CalculateSelectedIndexOffsetWithDrag()
    if self.dragging then
        if self.allowWrapping then
            return self:CalculateSelectedIndexOffset() + (GetUIMousePosition() - self.draggingXStart)
        end
        return zo_clamp(self:CalculateSelectedIndexOffset() + (GetUIMousePosition() - self.draggingXStart), -self.controlEntryWidth * (#self.list - 1), 0)
    end
    return self:CalculateSelectedIndexOffset()
end

function ZO_HorizontalScrollList:CalculateOffsetIndex(controlIndex, newVisibleIndex)
    return (controlIndex - newVisibleIndex) - self.halfNumVisibleEntries - 1
end

function ZO_HorizontalScrollList:CalculateControlIndexFromOffset(offsetIndex, newVisibleIndex)
    return offsetIndex + newVisibleIndex + self.halfNumVisibleEntries + 1
end

function ZO_HorizontalScrollList:CalculateDataIndexFromOffset(offsetIndex)
    return offsetIndex % #self.list + 1
end

function ZO_HorizontalScrollList:SetOnSelectedDataChangedCallback(onSelectedDataChangedCallback)
    self.onSelectedDataChangedCallback = onSelectedDataChangedCallback
end

function ZO_HorizontalScrollList:SetOnTargetDataChangedCallback(onTargetDataChangedCallback)
    self.onTargetDataChangedCallback = onTargetDataChangedCallback
end

function ZO_HorizontalScrollList:UpdateAnchors(primaryControlOffsetX, initialUpdate, reselectingDuringRebuild)
    if self.isUpdatingAnchors then return end
    self.isUpdatingAnchors = true

    local oldPrimaryControlOffsetX = self.lastPrimaryControlOffsetX or 0
    local oldVisibleIndex = zo_round(oldPrimaryControlOffsetX / self.controlEntryWidth)
    local newVisibleIndex = zo_round(primaryControlOffsetX / self.controlEntryWidth)

    local visibleIndicesChanged = oldVisibleIndex ~= newVisibleIndex
    local oldData = self.selectedData
    for i, control in ipairs(self.controls) do
        local index = self:CalculateOffsetIndex(i, newVisibleIndex)
        if not (self.allowWrapping and self:CanScroll()) and (index >= #self.list or index < 0) then
            control:SetHidden(true)
        else
            control:SetHidden(false)

            if initialUpdate or visibleIndicesChanged then
                local dataIndex = self:CalculateDataIndexFromOffset(index)
                local selected = i == self.halfNumVisibleEntries + 1

                local data = self.list[dataIndex]
                if selected then
                    self.selectedData = data
                    if not reselectingDuringRebuild and self.selectionHighlightAnimation and not self.selectionHighlightAnimation:IsPlaying() then
                        self.selectionHighlightAnimation:PlayFromStart()
                    end
                    if not initialUpdate and not reselectingDuringRebuild and self.dragging then
                        self.onPlaySoundFunction(ZO_HORIZONTALSCROLLLIST_MOVEMENT_TYPES.INITIAL_UPDATE)
                    end
                end
                self.setupFunction(control, data, selected, reselectingDuringRebuild, self.enabled, self.selectedFromParent)
            end

            local offsetX = primaryControlOffsetX + index * self.controlEntryWidth
            control:ClearAnchors()
            if self.displayEntryType == ZO_HORIZONTAL_SCROLL_LIST_DISPLAY_FIXED_NUMBER_OF_ENTRIES then
                self:SetDefaultEntryAnchor(control, offsetX)
            else
                self:AnchorEntryAtFixedOffset(control, offsetX, index, newVisibleIndex)
            end

            if self.minScale and self.maxScale then
                local amount = ZO_EaseInQuintic(zo_max(1.0 - zo_abs(offsetX) / (self.control:GetWidth() * .5), 0.0))
                control:SetScale(zo_lerp(self.minScale, self.maxScale, amount))
            end
        end
    end

    self.lastPrimaryControlOffsetX = primaryControlOffsetX

    self.leftArrow:SetEnabled(self.enabled and (self.allowWrapping or newVisibleIndex ~= 0))
    self.rightArrow:SetEnabled(self.enabled and (self.allowWrapping or newVisibleIndex ~= 1 - #self.list))

    self.isUpdatingAnchors = false

    if (self.selectedData ~= oldData or initialUpdate) and self.onSelectedDataChangedCallback then
        self.onSelectedDataChangedCallback(self.selectedData, oldData, reselectingDuringRebuild)
    end
end

function ZO_HorizontalScrollList:SetDefaultEntryAnchor(control, offsetX)
    control:SetAnchor(CENTER, self.control, CENTER, offsetX)
end

function ZO_HorizontalScrollList:AnchorEntryAtFixedOffset(control, offsetX, index, newVisibleIndex)
    local controlHalfWidth = self.controlEntryWidth / 2
    -- check if control is on the left side
    if offsetX < -controlHalfWidth then
        local nextControlIndex = self:CalculateControlIndexFromOffset(index + 1, newVisibleIndex)
        local nextControl = self.controls[nextControlIndex]
        control:SetAnchor(RIGHT, nextControl, LEFT, -self.offsetBetweenEntries)
    -- check if control is on the right side
    elseif offsetX > controlHalfWidth then
        local previousControlIndex = self:CalculateControlIndexFromOffset(index - 1, newVisibleIndex)
        local previousControl = self.controls[previousControlIndex]
        control:SetAnchor(LEFT, previousControl, RIGHT, self.offsetBetweenEntries)
    else
        self:SetDefaultEntryAnchor(control, offsetX)
    end
end

function ZO_HorizontalScrollList:SetOnControlClicked(onControlClicked)
    self.onControlClicked = onControlClicked
end

function ZO_HorizontalScrollList:SelectControl(controlToSelect)
    for i, control in ipairs(self.controls) do
        if controlToSelect == control then
            self:SetSelectedIndex((self.selectedIndex or 0) - (i - self.halfNumVisibleEntries - 1))
            break
        end
    end
end

function ZO_HorizontalScrollList:SelectControlFromCondition(conditionFunction)
    if (conditionFunction) then
        for i, control in ipairs(self.controls) do
            if conditionFunction(control) then
                self:SetSelectedIndex((self.selectedIndex or 0) - (i - self.halfNumVisibleEntries - 1))
                break
            end
        end
    end
end

function ZO_HorizontalScrollList:SetMoving(isMoving)
    if self.isMoving ~= isMoving then
        self.isMoving = isMoving
        if self.onMovementChangedCallback then
            self.onMovementChangedCallback(self, isMoving)
        end

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

--This is for the case where a horizontal scroll list is an entry in another list
-- and you want to change the horizontals look based on it being selected
function ZO_HorizontalScrollList:SetSelectedFromParent(selected)
    self.selectedFromParent = selected
end

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

function ZO_HorizontalScrollList_OnMouseWheel(control, delta)
    local horizontalScrollList = control.horizontalScrollList
    if horizontalScrollList:CanScroll() then
        if delta > 0 then
            horizontalScrollList:MoveLeft()
        else
            horizontalScrollList:MoveRight()
        end
    end
end
