/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "LyShine_precompiled.h"
#include "UiDynamicScrollBoxComponent.h"

#include "UiElementComponent.h"
#include "UiNavigationHelpers.h"
#include "UiLayoutHelpers.h"

#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>
#include <AzCore/Component/ComponentApplicationBus.h>

#include <LyShine/Bus/UiTransform2dBus.h>
#include <LyShine/Bus/UiCanvasBus.h>
#include <LyShine/Bus/UiLayoutCellBus.h>
#include <LyShine/Bus/UiLayoutCellDefaultBus.h>

////////////////////////////////////////////////////////////////////////////////////////////////////
//! UiDynamicScrollBoxDataBus Behavior context handler class
class BehaviorUiDynamicScrollBoxDataBusHandler
    : public UiDynamicScrollBoxDataBus::Handler
    , public AZ::BehaviorEBusHandler
{
public:
    AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiDynamicScrollBoxDataBusHandler, "{74FA95AB-D4C2-40B8-8568-1B174BF577C0}", AZ::SystemAllocator,
        GetNumElements, GetElementWidth, GetElementHeight, GetNumSections, GetNumElementsInSection,
        GetElementInSectionWidth, GetElementInSectionHeight, GetSectionHeaderWidth, GetSectionHeaderHeight);

    int GetNumElements() override
    {
        int numElements = 0;
        CallResult(numElements, FN_GetNumElements);
        return numElements;
    }

    float GetElementWidth(int index) override
    {
        float width = 0.0f;
        CallResult(width, FN_GetElementWidth, index);
        return width;
    }

    float GetElementHeight(int index) override
    {
        float height = 0.0f;
        CallResult(height, FN_GetElementHeight, index);
        return height;
    }

    int GetNumSections() override
    {
        int numSections = 0;
        CallResult(numSections, FN_GetNumSections);
        return numSections;
    }

    int GetNumElementsInSection(int sectionIndex) override
    {
        int numElementsInSection = 0;
        CallResult(numElementsInSection, FN_GetNumElementsInSection, sectionIndex);
        return numElementsInSection;
    }

    float GetElementInSectionWidth(int sectionIndex, int index) override
    {
        float width = 0.0f;
        CallResult(width, FN_GetElementInSectionWidth, sectionIndex, index);
        return width;
    }

    float GetElementInSectionHeight(int sectionIndex, int index) override
    {
        float height = 0.0f;
        CallResult(height, FN_GetElementInSectionHeight, sectionIndex, index);
        return height;
    }

    float GetSectionHeaderWidth(int sectionIndex) override
    {
        float width = 0.0f;
        CallResult(width, FN_GetSectionHeaderWidth, sectionIndex);
        return width;
    }

    float GetSectionHeaderHeight(int sectionIndex) override
    {
        float height = 0.0f;
        CallResult(height, FN_GetSectionHeaderHeight, sectionIndex);
        return height;
    }
};

////////////////////////////////////////////////////////////////////////////////////////////////////
//! UiDynamicScrollBoxElementNotificationBus Behavior context handler class
class BehaviorUiDynamicScrollBoxElementNotificationBusHandler
    : public UiDynamicScrollBoxElementNotificationBus::Handler
    , public AZ::BehaviorEBusHandler
{
public:
    AZ_EBUS_BEHAVIOR_BINDER(BehaviorUiDynamicScrollBoxElementNotificationBusHandler, "{4D166273-4D12-45A4-BC42-A7FF59A2092E}", AZ::SystemAllocator,
        OnElementBecomingVisible, OnPrepareElementForSizeCalculation,
        OnElementInSectionBecomingVisible, OnPrepareElementInSectionForSizeCalculation,
        OnSectionHeaderBecomingVisible, OnPrepareSectionHeaderForSizeCalculation);

    void OnElementBecomingVisible(AZ::EntityId entityId, int index) override
    {
        Call(FN_OnElementBecomingVisible, entityId, index);
    }

    void OnPrepareElementForSizeCalculation(AZ::EntityId entityId, int index) override
    {
        Call(FN_OnPrepareElementForSizeCalculation, entityId, index);
    }

    void OnElementInSectionBecomingVisible(AZ::EntityId entityId, int sectionIndex, int index) override
    {
        Call(FN_OnElementInSectionBecomingVisible, entityId, sectionIndex, index);
    }

    void OnPrepareElementInSectionForSizeCalculation(AZ::EntityId entityId, int sectionIndex, int index) override
    {
        Call(FN_OnPrepareElementInSectionForSizeCalculation, entityId, sectionIndex, index);
    }

    void OnSectionHeaderBecomingVisible(AZ::EntityId entityId, int sectionIndex) override
    {
        Call(FN_OnSectionHeaderBecomingVisible, entityId, sectionIndex);
    }

    void OnPrepareSectionHeaderForSizeCalculation(AZ::EntityId entityId, int sectionIndex) override
    {
        Call(FN_OnPrepareSectionHeaderForSizeCalculation, entityId, sectionIndex);
    }
};

////////////////////////////////////////////////////////////////////////////////////////////////////
// PUBLIC MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
UiDynamicScrollBoxComponent::CachedElementInfo::CachedElementInfo()
    : m_size(-1.0f)
    , m_accumulatedSize(-1.0f)
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
UiDynamicScrollBoxComponent::UiDynamicScrollBoxComponent()
    : m_autoRefreshOnPostActivate(true)
    , m_defaultNumElements(0)
    , m_variableItemElementSize(false)
    , m_autoCalculateItemElementSize(true)
    , m_estimatedItemElementSize(0.0f)
    , m_hasSections(false)
    , m_defaultNumSections(1)
    , m_stickyHeaders(false)
    , m_variableHeaderElementSize(false)
    , m_autoCalculateHeaderElementSize(true)
    , m_estimatedHeaderElementSize(0.0f)
    , m_averageElementSize(0.0f)
    , m_numElementsUsedForAverage(0)
    , m_lastCalculatedVisibleContentOffset(0.0f)
    , m_isVertical(true)
    , m_firstDisplayedElementIndex(-1)
    , m_lastDisplayedElementIndex(-1)
    , m_firstVisibleElementIndex(-1)
    , m_lastVisibleElementIndex(-1)
    , m_numElements(0)
    , m_listPreparedForDisplay(false)
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
UiDynamicScrollBoxComponent::~UiDynamicScrollBoxComponent()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::RefreshContent()
{
    if (!m_listPreparedForDisplay)
    {
        PrepareListForDisplay();
    }

    ResizeContentToFitElements();

    ClearDisplayedElements();

    bool keepAtEndIfWasAtEnd = false;
    if (AnyElementTypesHaveEstimatedSizes())
    {
        // Check if the content's pivot is at the end (bottom or right)
        AZ::EntityId contentEntityId;
        EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);
        if (contentEntityId.IsValid())
        {
            AZ::Vector2 pivot(0.0f, 0.0f);
            EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);

            if (m_isVertical)
            {
                keepAtEndIfWasAtEnd = pivot.GetY() == 1.0f;
            }
            else
            {
                keepAtEndIfWasAtEnd = pivot.GetX() == 1.0f;
            }
        }
    }

    UpdateElementVisibility(keepAtEndIfWasAtEnd);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::AddElementsToEnd(int numElementsToAdd, bool scrollToEndIfWasAtEnd)
{
    AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "AddElementsToEnd() is only supported after the first content refresh");
    if (!m_listPreparedForDisplay)
    {
        return;
    }

    AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "AddElementsToEnd() can only be used on lists that are not divided into sections");

    if (numElementsToAdd > 0 && !m_hasSections)
    {
        m_numElements += numElementsToAdd;

        // Calculate new content size
        float sizeDiff = 0.0f;
        if (!m_variableElementSize[ElementType::Item])
        {
            sizeDiff = numElementsToAdd * m_prototypeElementSize[ElementType::Item];
        }
        else
        {
            // Add cache entries for the new elements
            m_cachedElementInfo.insert(m_cachedElementInfo.end(), numElementsToAdd, CachedElementInfo());

            for (int i = m_numElements - numElementsToAdd; i < m_numElements; i++)
            {
                sizeDiff += GetAndCacheVariableElementSize(i);
            }

            if (m_autoCalculateElementSize[ElementType::Item])
            {
                DisableElementsForAutoSizeCalculation();
            }

            UpdateAverageElementSize(numElementsToAdd, sizeDiff);
        }

        bool scrollToEnd = scrollToEndIfWasAtEnd && IsScrolledToEnd();
        if (scrollToEnd)
        {
            float scrollDiff = CalculateContentEndDeltaAfterSizeChange(sizeDiff);
            AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);

            if (!IsScrolledToEnd())
            {
                ScrollToEnd();
            }
            else
            {
                UpdateElementVisibility(true);
            }
        }
        else
        {
            float scrollDiff = CalculateContentBeginningDeltaAfterSizeChange(sizeDiff);
            AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);

            UpdateElementVisibility();
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::RemoveElementsFromFront(int numElementsToRemove)
{
    AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "RemoveElementsFromFront() is only supported after the first content refresh");
    if (!m_listPreparedForDisplay)
    {
        return;
    }

    AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "RemoveElementsFromFront() can only be used on lists that are not divided into sections");

    if (numElementsToRemove > 0 && !m_hasSections)
    {
        AZ_Warning("UiDynamicScrollBoxComponent", numElementsToRemove <= m_numElements, "attempting to remove more elements than are in the list");

        numElementsToRemove = AZ::GetClamp(numElementsToRemove, 0, m_numElements);

        float sizeDiff = 0.0f;
        if (!m_variableElementSize[ElementType::Item])
        {
            sizeDiff = numElementsToRemove * m_prototypeElementSize[ElementType::Item];
        }
        else
        {
            // Get accumulated size being removed
            sizeDiff = GetVariableSizeElementOffset(numElementsToRemove - 1) + GetVariableElementSize(numElementsToRemove - 1);

            // Update cached element info
            m_cachedElementInfo.erase(m_cachedElementInfo.begin(), m_cachedElementInfo.begin() + numElementsToRemove);

            // Update accumulated sizes
            int newElementCount = m_numElements - numElementsToRemove;
            for (int i = 0; i < newElementCount; i++)
            {
                if (m_cachedElementInfo[i].m_accumulatedSize >= 0.0f)
                {
                    m_cachedElementInfo[i].m_accumulatedSize -= sizeDiff;
                }
            }
        }
        sizeDiff = -sizeDiff;

        m_numElements -= numElementsToRemove;

        if (numElementsToRemove > 0)
        {
            ClearDisplayedElements();

            float scrollDiff = CalculateContentBeginningDeltaAfterSizeChange(sizeDiff) - sizeDiff;
            AdjustContentSizeAndScrollOffsetByDelta(sizeDiff, scrollDiff);

            UpdateElementVisibility();
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::ScrollToEnd()
{
    AZ_Warning("UiDynamicScrollBoxComponent", m_listPreparedForDisplay, "ScrollToEnd() is only supported after the first content refresh");
    if (!m_listPreparedForDisplay)
    {
        return;
    }

    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return;
    }

    // Get content's parent
    AZ::EntityId contentParentEntityId;
    EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
    if (!contentParentEntityId.IsValid())
    {
        return;
    }

    // Get content's rect in canvas space
    UiTransformInterface::Rect contentRect;
    EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);

    // Get content parent's rect in canvas space
    UiTransformInterface::Rect parentRect;
    EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);

    float scrollDelta = 0.0f;
    if (m_isVertical)
    {
        if (contentRect.bottom > parentRect.bottom)
        {
            scrollDelta = parentRect.bottom - contentRect.bottom;
        }
    }
    else
    {
        if (contentRect.right > parentRect.right)
        {
            scrollDelta = parentRect.right - contentRect.right;
        }
    }

    if (scrollDelta != 0.0f)
    {
        AdjustContentSizeAndScrollOffsetByDelta(0.0f, scrollDelta);

        UpdateElementVisibility(true);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::GetElementIndexOfChild(AZ::EntityId childElement)
{
    AZ::EntityId immediateChild = GetImmediateContentChildFromDescendant(childElement);

    for (const auto& e : m_displayedElements)
    {
        if (e.m_element == immediateChild)
        {
            if (!m_hasSections)
            {
                return e.m_elementIndex;
            }
            else
            {
                return e.m_indexInfo.m_itemIndexInSection;
            }
        }
    }

    return -1;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::GetSectionIndexOfChild(AZ::EntityId childElement)
{
    AZ_Warning("UiDynamicScrollBoxComponent", m_hasSections, "GetSectionIndexOfChild() can only be used on lists that are divided into sections");

    if (m_hasSections)
    {
        AZ::EntityId immediateChild = GetImmediateContentChildFromDescendant(childElement);

        for (const auto& e : m_displayedElements)
        {
            if (e.m_element == immediateChild)
            {
                return e.m_indexInfo.m_sectionIndex;
            }
        }
    }

    return -1;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetChildAtElementIndex(int index)
{
    AZ::EntityId elementId;

    AZ_Warning("UiDynamicScrollBoxComponent", !m_hasSections, "GetChildAtElementIndex() can only be used on lists that are not divided into sections");

    if (!m_hasSections)
    {
        elementId = FindDisplayedElementWithIndex(index);
    }

    return elementId;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId  UiDynamicScrollBoxComponent::GetChildAtSectionAndElementIndex(int sectionIndex, int index)
{
    AZ::EntityId elementId;

    AZ_Warning("UiDynamicScrollBoxComponent", m_hasSections, "GetChildElementAtSectionAndLocationIndex() can only be used on lists that are divided into sections");

    if (m_hasSections)
    {
        for (const auto& e : m_displayedElements)
        {
            if (e.m_indexInfo.m_sectionIndex == sectionIndex && e.m_indexInfo.m_itemIndexInSection == index)
            {
                elementId = e.m_element;
                break;
            }
        }
    }

    return elementId;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetAutoRefreshOnPostActivate()
{
    return m_autoRefreshOnPostActivate;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetAutoRefreshOnPostActivate(bool autoRefresh)
{
    m_autoRefreshOnPostActivate = autoRefresh;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetPrototypeElement()
{
    return m_itemPrototypeElement;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetPrototypeElement(AZ::EntityId prototypeElement)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_itemPrototypeElement = prototypeElement;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetElementsVaryInSize()
{
    return m_variableItemElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetElementsVaryInSize(bool varyInSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_variableItemElementSize = varyInSize;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetAutoCalculateVariableElementSize()
{
    return m_autoCalculateItemElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetAutoCalculateVariableElementSize(bool autoCalculateSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_autoCalculateItemElementSize = autoCalculateSize;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetEstimatedVariableElementSize()
{
    return m_estimatedItemElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetEstimatedVariableElementSize(float estimatedSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_estimatedItemElementSize = AZ::GetMax(estimatedSize, 0.0f);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetSectionsEnabled()
{
    return m_hasSections;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetSectionsEnabled(bool sectionsEnabled)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_hasSections = sectionsEnabled;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetPrototypeHeader()
{
    return m_headerPrototypeElement;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetPrototypeHeader(AZ::EntityId prototypeHeader)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_headerPrototypeElement = prototypeHeader;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetHeadersSticky()
{
    return m_stickyHeaders;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetHeadersSticky(bool stickyHeaders)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_stickyHeaders = stickyHeaders;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetHeadersVaryInSize()
{
    return m_variableHeaderElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetHeadersVaryInSize(bool varyInSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_variableHeaderElementSize = varyInSize;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::GetAutoCalculateVariableHeaderSize()
{
    return m_autoCalculateHeaderElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetAutoCalculateVariableHeaderSize(bool autoCalculateSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_autoCalculateHeaderElementSize = autoCalculateSize;
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetEstimatedVariableHeaderSize()
{
    return m_estimatedHeaderElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetEstimatedVariableHeaderSize(float estimatedSize)
{
    AZ_Warning("UiDynamicScrollBoxComponent", !m_listPreparedForDisplay, "Changing properties is only supported before the first content refresh");

    if (!m_listPreparedForDisplay)
    {
        m_estimatedHeaderElementSize = AZ::GetMax(estimatedSize, 0.0f);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::OnScrollOffsetChanging(AZ::Vector2 newScrollOffset)
{
    UpdateElementVisibility();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::OnScrollOffsetChanged(AZ::Vector2 newScrollOffset)
{
    UpdateElementVisibility();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::InGamePostActivate()
{
    if (m_autoRefreshOnPostActivate)
    {
        RefreshContent();
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::OnCanvasSpaceRectChanged(AZ::EntityId entityId, const UiTransformInterface::Rect& oldRect, const UiTransformInterface::Rect& newRect)
{
    // If old rect equals new rect, size changed due to initialization
    bool sizeChanged = (oldRect == newRect) || (!oldRect.GetSize().IsClose(newRect.GetSize(), 0.05f));

    if (sizeChanged)
    {
        UpdateElementVisibility();
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::OnUiElementBeingDestroyed()
{
    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        if (m_prototypeElement[i].IsValid())
        {
            EBUS_EVENT_ID(m_prototypeElement[i], UiElementBus, DestroyElement);
            m_prototypeElement[i].SetInvalid();
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// PUBLIC STATIC MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////

void UiDynamicScrollBoxComponent::Reflect(AZ::ReflectContext* context)
{
    AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context);

    if (serializeContext)
    {
        serializeContext->Class<UiDynamicScrollBoxComponent, AZ::Component>()
            ->Version(1)
            ->Field("AutoRefreshOnPostActivate", &UiDynamicScrollBoxComponent::m_autoRefreshOnPostActivate)
            ->Field("PrototypeElement", &UiDynamicScrollBoxComponent::m_itemPrototypeElement)
            ->Field("VariableElementSize", &UiDynamicScrollBoxComponent::m_variableItemElementSize)
            ->Field("AutoCalcElementSize", &UiDynamicScrollBoxComponent::m_autoCalculateItemElementSize)
            ->Field("EstimatedElementSize", &UiDynamicScrollBoxComponent::m_estimatedItemElementSize)
            ->Field("DefaultNumElements", &UiDynamicScrollBoxComponent::m_defaultNumElements)
            ->Field("HasSections", &UiDynamicScrollBoxComponent::m_hasSections)
            ->Field("HeaderPrototypeElement", &UiDynamicScrollBoxComponent::m_headerPrototypeElement)
            ->Field("StickyHeaders", &UiDynamicScrollBoxComponent::m_stickyHeaders)
            ->Field("VariableHeaderSize", &UiDynamicScrollBoxComponent::m_variableHeaderElementSize)
            ->Field("AutoCalcHeaderSize", &UiDynamicScrollBoxComponent::m_autoCalculateHeaderElementSize)
            ->Field("EstimatedHeaderSize", &UiDynamicScrollBoxComponent::m_estimatedHeaderElementSize)
            ->Field("DefaultNumSections", &UiDynamicScrollBoxComponent::m_defaultNumSections);

        AZ::EditContext* ec = serializeContext->GetEditContext();
        if (ec)
        {
            auto editInfo = ec->Class<UiDynamicScrollBoxComponent>("DynamicScrollBox",
                    "A component that dynamically sets up scroll box content as a horizontal or vertical list of elements that\n"
                    "are cloned from a prototype element. Only the minimum number of elements are created for efficient scrolling.\n"
                    "The scroll box's content element's first child acts as the prototype element.");

            editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "UI")
                ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiDynamicScrollBox.png")
                ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiDynamicScrollBox.png")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
                ->Attribute(AZ::Edit::Attributes::AutoExpand, true);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoRefreshOnPostActivate, "Refresh on activate",
                "Whether the list should automatically prepare and refresh its content post activation.");

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_itemPrototypeElement, "Prototype element",
                "The prototype element to be used for the elements in the list. If empty, the prototype element will default to the first child of the content element.");

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_variableItemElementSize, "Variable element size",
                "Whether elements in the list can vary in size. If not, the element size is fixed and is determined by the prototype element.")
                ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoCalculateItemElementSize, "Auto calc element size",
                "Whether element sizes should be auto calculated or whether they should be requested.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_variableItemElementSize);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_estimatedItemElementSize, "Estimated element size",
                "The element size to use as an estimate before the element appears in the view. If set to 0, sizes will be calculated up front.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_variableItemElementSize)
                ->Attribute(AZ::Edit::Attributes::Min, 0.0f);

            editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiDynamicScrollBoxComponent::m_defaultNumElements, "Default num elements",
                "The default number of elements in the list.")
                ->Attribute(AZ::Edit::Attributes::Min, 0);

            editInfo->ClassElement(AZ::Edit::ClassElements::Group, "Sections")
                ->Attribute(AZ::Edit::Attributes::AutoExpand, true);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_hasSections, "Enabled",
                "Whether the list should be divided into sections with headers.")
                ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_headerPrototypeElement, "Prototype header",
                "The prototype element to be used for the section headers in the list.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_stickyHeaders, "Sticky headers",
                "Whether headers should stick to the beginning of the visible list area.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_variableHeaderElementSize, "Variable header size",
                "Whether headers in the list can vary in size. If not, the header size is fixed and is determined by the prototype element.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections)
                ->Attribute(AZ::Edit::Attributes::ChangeNotify, AZ_CRC("RefreshEntireTree", 0xefbc823c));

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_autoCalculateHeaderElementSize, "Auto calc header size",
                "Whether header sizes should be auto calculated or whether they should be requested.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::HeadersHaveVariableSizes);

            editInfo->DataElement(0, &UiDynamicScrollBoxComponent::m_estimatedHeaderElementSize, "Estimated header size",
                "The header size to use as an estimate before the header appears in the view. If set to 0, sizes will be calculated up front.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::HeadersHaveVariableSizes)
                ->Attribute(AZ::Edit::Attributes::Min, 0.0f);

            editInfo->DataElement(AZ::Edit::UIHandlers::SpinBox, &UiDynamicScrollBoxComponent::m_defaultNumSections, "Default num sections",
                "The default number of sections in the list.")
                ->Attribute(AZ::Edit::Attributes::Visibility, &UiDynamicScrollBoxComponent::m_hasSections)
                ->Attribute(AZ::Edit::Attributes::Min, 1);
        }
    }

    AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
    if (behaviorContext)
    {
        behaviorContext->EBus<UiDynamicScrollBoxBus>("UiDynamicScrollBoxBus")
            ->Event("RefreshContent", &UiDynamicScrollBoxBus::Events::RefreshContent)
            ->Event("AddElementsToEnd", &UiDynamicScrollBoxBus::Events::AddElementsToEnd)
            ->Event("RemoveElementsFromFront", &UiDynamicScrollBoxBus::Events::RemoveElementsFromFront)
            ->Event("ScrollToEnd", &UiDynamicScrollBoxBus::Events::ScrollToEnd)
            ->Event("GetElementIndexOfChild", &UiDynamicScrollBoxBus::Events::GetElementIndexOfChild)
            ->Event("GetSectionIndexOfChild", &UiDynamicScrollBoxBus::Events::GetSectionIndexOfChild)
            ->Event("GetChildAtElementIndex", &UiDynamicScrollBoxBus::Events::GetChildAtElementIndex)
            ->Event("GetChildAtSectionAndElementIndex", &UiDynamicScrollBoxBus::Events::GetChildAtSectionAndElementIndex)
            ->Event("GetAutoRefreshOnPostActivate", &UiDynamicScrollBoxBus::Events::GetAutoRefreshOnPostActivate)
            ->Event("SetAutoRefreshOnPostActivate", &UiDynamicScrollBoxBus::Events::SetAutoRefreshOnPostActivate)
            ->Event("GetPrototypeElement", &UiDynamicScrollBoxBus::Events::GetPrototypeElement)
            ->Event("SetPrototypeElement", &UiDynamicScrollBoxBus::Events::SetPrototypeElement)
            ->Event("GetElementsVaryInSize", &UiDynamicScrollBoxBus::Events::GetElementsVaryInSize)
            ->Event("SetElementsVaryInSize", &UiDynamicScrollBoxBus::Events::SetElementsVaryInSize)
            ->Event("GetAutoCalculateVariableElementSize", &UiDynamicScrollBoxBus::Events::GetAutoCalculateVariableElementSize)
            ->Event("SetAutoCalculateVariableElementSize", &UiDynamicScrollBoxBus::Events::SetAutoCalculateVariableElementSize)
            ->Event("GetEstimatedVariableElementSize", &UiDynamicScrollBoxBus::Events::GetEstimatedVariableElementSize)
            ->Event("SetEstimatedVariableElementSize", &UiDynamicScrollBoxBus::Events::SetEstimatedVariableElementSize)
            ->Event("GetSectionsEnabled", &UiDynamicScrollBoxBus::Events::GetSectionsEnabled)
            ->Event("SetSectionsEnabled", &UiDynamicScrollBoxBus::Events::SetSectionsEnabled)
            ->Event("GetPrototypeHeader", &UiDynamicScrollBoxBus::Events::GetPrototypeHeader)
            ->Event("SetPrototypeHeader", &UiDynamicScrollBoxBus::Events::SetPrototypeHeader)
            ->Event("GetHeadersSticky", &UiDynamicScrollBoxBus::Events::GetHeadersSticky)
            ->Event("SetHeadersSticky", &UiDynamicScrollBoxBus::Events::SetHeadersSticky)
            ->Event("GetHeadersVaryInSize", &UiDynamicScrollBoxBus::Events::GetHeadersVaryInSize)
            ->Event("SetHeadersVaryInSize", &UiDynamicScrollBoxBus::Events::SetHeadersVaryInSize)
            ->Event("GetAutoCalculateVariableHeaderSize", &UiDynamicScrollBoxBus::Events::GetAutoCalculateVariableHeaderSize)
            ->Event("SetAutoCalculateVariableHeaderSize", &UiDynamicScrollBoxBus::Events::SetAutoCalculateVariableHeaderSize)
            ->Event("GetEstimatedVariableHeaderSize", &UiDynamicScrollBoxBus::Events::GetEstimatedVariableHeaderSize)
            ->Event("SetEstimatedVariableHeaderSize", &UiDynamicScrollBoxBus::Events::SetEstimatedVariableHeaderSize)
            ;

        behaviorContext->EBus<UiDynamicScrollBoxDataBus>("UiDynamicScrollBoxDataBus")
            ->Handler<BehaviorUiDynamicScrollBoxDataBusHandler>();

        behaviorContext->EBus<UiDynamicScrollBoxElementNotificationBus>("UiDynamicScrollBoxElementNotificationBus")
            ->Handler<BehaviorUiDynamicScrollBoxElementNotificationBusHandler>();
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// PROTECTED MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::Activate()
{
    UiDynamicScrollBoxBus::Handler::BusConnect(GetEntityId());
    UiInitializationBus::Handler::BusConnect(GetEntityId());
    UiElementNotificationBus::Handler::BusConnect(GetEntityId());
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::Deactivate()
{
    UiDynamicScrollBoxBus::Handler::BusDisconnect();
    UiInitializationBus::Handler::BusDisconnect();
    if (UiTransformChangeNotificationBus::Handler::BusIsConnected())
    {
        UiTransformChangeNotificationBus::Handler::BusDisconnect();
    }
    if (UiScrollBoxNotificationBus::Handler::BusIsConnected())
    {
        UiScrollBoxNotificationBus::Handler::BusDisconnect();
    }
    UiElementNotificationBus::Handler::BusDisconnect();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::PrepareListForDisplay()
{
    if (m_listPreparedForDisplay)
    {
        return;
    }

    // Set whether list is vertical or horizontal
    m_isVertical = true;
    EBUS_EVENT_ID_RESULT(m_isVertical, GetEntityId(), UiScrollBoxBus, GetIsVerticalScrollingEnabled);

    m_variableElementSize[ElementType::Item] = m_variableItemElementSize;
    m_autoCalculateElementSize[ElementType::Item] = m_variableItemElementSize ? m_autoCalculateItemElementSize : false;
    m_estimatedElementSize[ElementType::Item] = m_variableItemElementSize ? m_estimatedItemElementSize : 0.0f;
    m_variableElementSize[ElementType::SectionHeader] = m_hasSections ? m_variableHeaderElementSize : false;
    m_autoCalculateElementSize[ElementType::SectionHeader] = (m_hasSections && m_variableHeaderElementSize) ? m_autoCalculateHeaderElementSize : false;
    m_estimatedElementSize[ElementType::SectionHeader] = (m_hasSections && m_variableHeaderElementSize) ? m_estimatedHeaderElementSize : 0.0f;

    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        m_prototypeElement[i].SetInvalid();
    }

    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    int numChildren = 0;
    EBUS_EVENT_ID_RESULT(numChildren, contentEntityId, UiElementBus, GetNumChildElements);

    // Make sure the item prototype element isn't pointing to itself (the dynamic scroll box) or an ancestor,
    // otherwise this scroll box will spawn scroll boxes recursively ad infinitum.
    if (IsValidPrototype(m_itemPrototypeElement))
    {
        m_prototypeElement[ElementType::Item] = m_itemPrototypeElement;
    }
    else
    {
        if (m_itemPrototypeElement.IsValid())
        {
            AZ_Warning("UiDynamicScrollBoxComponent", false,
                "The prototype element is not safe for cloning. "
                "This scroll box's prototype element contains the scroll box itself which can result in recursively spawning scroll boxes. "
                "Please change the prototype element to a nonancestral entity.");
        }

        // Find the prototype element as the first child of the content element
        if (numChildren > 0)
        {
            AZ::EntityId prototypeEntityId;
            EBUS_EVENT_ID_RESULT(prototypeEntityId, contentEntityId, UiElementBus, GetChildEntityId, 0);
            m_prototypeElement[ElementType::Item] = prototypeEntityId;
        }
    }

    if (m_hasSections)
    {
        if (IsValidPrototype(m_headerPrototypeElement))
        {
            // Prototype header element is defined in properties
            m_prototypeElement[ElementType::SectionHeader] = m_headerPrototypeElement;
        }
        else if(m_headerPrototypeElement.IsValid())
        {
            AZ_Warning("UiDynamicScrollBoxComponent", false,
                "The selected prototype header is not safe for cloning. "
                "This scroll box's prototype header contains the scroll box itself which can result in recursively spawning scroll boxes. "
                "Please change the header to a nonancestral entity.");
        }
    }

    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        m_isPrototypeElementNavigable[i] = false;
        m_prototypeElementSize[i] = 0.0f;

        if (m_prototypeElement[i].IsValid())
        {
            m_isPrototypeElementNavigable[i] = UiNavigationHelpers::IsElementInteractableAndNavigable(m_prototypeElement[i]);

            // Store the size of the item prototype element for future content element size calculations
            AZ::Vector2 prototypeElementSize(0.0f, 0.0f);
            EBUS_EVENT_ID_RESULT(prototypeElementSize, m_prototypeElement[i], UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

            m_prototypeElementSize[i] = (m_isVertical ? prototypeElementSize.GetY() : prototypeElementSize.GetX());

            // Set anchors to top or left
            SetElementAnchors(m_prototypeElement[i]);
        }
    }

    AZ::Entity* contentEntity = GetContentEntity();
    if (contentEntity)
    {
        // Get the content entity's element component
        UiElementComponent* elementComponent = contentEntity->FindComponent<UiElementComponent>();
        AZ_Assert(elementComponent, "entity has no UiElementComponent");

        if (elementComponent)
        {
            // Remove any extra elements
            for (int i = numChildren - 1; i >= 0; i--)
            {
                AZ::EntityId entityId;
                EBUS_EVENT_ID_RESULT(entityId, contentEntityId, UiElementBus, GetChildEntityId, i);

                // Remove the child element
                elementComponent->RemoveChild(entityId);

                if (!IsPrototypeElement(entityId))
                {
                    EBUS_EVENT_ID(entityId, UiElementBus, DestroyElement);
                }
            }
        }

        // Get the content's parent
        AZ::EntityId contentParentEntityId;
        EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);

        // Create an entity that will be used as the sticky header
        m_currentStickyHeader.m_elementIndex = -1;
        m_currentStickyHeader.m_indexInfo.m_sectionIndex = -1;
        m_currentStickyHeader.m_indexInfo.m_itemIndexInSection = -1;
        m_currentStickyHeader.m_type = ElementType::SectionHeader;
        if (m_hasSections && m_stickyHeaders && contentParentEntityId.IsValid())
        {
            m_currentStickyHeader.m_element = ClonePrototypeElement(ElementType::SectionHeader, contentParentEntityId);
            EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, false);
        }

        // Listen for canvas space rect changes of the content's parent
        if (contentParentEntityId.IsValid())
        {
            UiTransformChangeNotificationBus::Handler::BusConnect(contentParentEntityId);
        }

        // Listen to scrollbox scrolling events
        UiScrollBoxNotificationBus::Handler::BusConnect(GetEntityId());
    }

    m_listPreparedForDisplay = true;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Entity* UiDynamicScrollBoxComponent::GetContentEntity() const
{
    AZ::Entity* contentEntity = nullptr;

    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (contentEntityId.IsValid())
    {
        EBUS_EVENT_RESULT(contentEntity, AZ::ComponentApplicationBus, FindEntity, contentEntityId);
    }

    return contentEntity;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::ClonePrototypeElement(ElementType elementType, AZ::EntityId parentEntityId) const
{
    AZ::EntityId element;

    // Clone the prototype element and add it as a child of the specified parent (defaults to content entity)
    AZ::Entity* prototypeEntity = nullptr;
    EBUS_EVENT_RESULT(prototypeEntity, AZ::ComponentApplicationBus, FindEntity, m_prototypeElement[elementType]);
    if (prototypeEntity)
    {
        if (!parentEntityId.IsValid())
        {
            AZ::EntityId contentEntityId;
            EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

            parentEntityId = contentEntityId;
        }

        // Find the parent entity
        AZ::Entity* parentEntity = nullptr;
        EBUS_EVENT_RESULT(parentEntity, AZ::ComponentApplicationBus, FindEntity, parentEntityId);

        if (parentEntity)
        {
            AZ::EntityId canvasEntityId;
            EBUS_EVENT_ID_RESULT(canvasEntityId, GetEntityId(), UiElementBus, GetCanvasEntityId);

            AZ::Entity* clonedElement = nullptr;
            EBUS_EVENT_ID_RESULT(clonedElement, canvasEntityId, UiCanvasBus, CloneElement, prototypeEntity, parentEntity);

            if (clonedElement)
            {
                element = clonedElement->GetId();
            }
        }
    }

    return element;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::IsPrototypeElement(AZ::EntityId entityId) const
{
    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        if (m_prototypeElement[i] == entityId)
        {
            return true;
        }
    }

    return false;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AllPrototypeElementsValid() const
{
    return (m_prototypeElement[ElementType::Item].IsValid() && (!m_hasSections || m_prototypeElement[ElementType::SectionHeader].IsValid()));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AnyPrototypeElementsNavigable() const
{
    return (m_isPrototypeElementNavigable[ElementType::Item] || (m_hasSections && m_isPrototypeElementNavigable[ElementType::SectionHeader]));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AnyElementTypesHaveVariableSize() const
{
    return (m_variableElementSize[ElementType::Item] || (m_hasSections && m_variableElementSize[ElementType::SectionHeader]));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AnyElementTypesHaveEstimatedSizes() const
{
    return (m_estimatedElementSize[ElementType::Item] > 0.0f || (m_hasSections && m_estimatedElementSize[ElementType::SectionHeader] > 0.0f));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AllElementTypesHaveEstimatedSizes() const
{
    return (m_estimatedElementSize[ElementType::Item] > 0.0f && (!m_hasSections || m_estimatedElementSize[ElementType::SectionHeader] > 0.0f));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::StickyHeadersEnabled() const
{
    return (m_hasSections && m_stickyHeaders && m_currentStickyHeader.m_element.IsValid());
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::ResizeContentToFitElements()
{
    if (!AllPrototypeElementsValid())
    {
        return;
    }

    // Get the number of elements in the list
    if (!m_hasSections)
    {
        m_sections.clear();

        m_numElements = m_defaultNumElements;
        EBUS_EVENT_ID_RESULT(m_numElements, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumElements);
    }
    else
    {
        int numSections = m_defaultNumSections;
        EBUS_EVENT_ID_RESULT(numSections, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumSections);
        numSections = AZ::GetMax(numSections, 1);

        m_sections.clear();
        m_sections.reserve(numSections);
        m_numElements = 0;
        for (int i = 0; i < numSections; i++)
        {
            int numItems = m_defaultNumElements;
            EBUS_EVENT_ID_RESULT(numItems, GetEntityId(), UiDynamicScrollBoxDataBus, GetNumElementsInSection, i);

            Section section;
            section.m_index = i;
            section.m_numItems = numItems;
            section.m_headerElementIndex = m_numElements;
            m_sections.push_back(section);

            m_numElements += 1 + section.m_numItems;
        }
    }

    // Calculate new content size
    float newSize = 0.0f;
    if (!AnyElementTypesHaveVariableSize())
    {
        if (!m_hasSections)
        {
            newSize = m_numElements * m_prototypeElementSize[ElementType::Item];
        }
        else
        {
            int numHeaders = m_sections.size();
            int numItems = m_numElements - numHeaders;
            newSize = numHeaders * m_prototypeElementSize[ElementType::SectionHeader] + numItems * m_prototypeElementSize[ElementType::Item];
        }
    }
    else
    {
        // Some element types have variable element sizes

        // Reset cached element info
        m_cachedElementInfo.clear();
        m_cachedElementInfo.reserve(m_numElements);
        m_cachedElementInfo.insert(m_cachedElementInfo.end(), m_numElements, CachedElementInfo());

        if (AllElementTypesHaveEstimatedSizes())
        {
            if (!m_hasSections)
            {
                newSize = m_numElements * m_estimatedElementSize[ElementType::Item];
            }
            else
            {
                int numHeaders = m_sections.size();
                int numItems = m_numElements - numHeaders;
                newSize = numHeaders * m_estimatedElementSize[ElementType::SectionHeader] + numItems * m_estimatedElementSize[ElementType::Item];
            }
        }
        else
        {
            for (int i = 0; i < m_numElements; i++)
            {
                newSize += GetAndCacheVariableElementSize(i);
            }

            DisableElementsForAutoSizeCalculation();
        }

        m_averageElementSize = 0.0f;
        m_numElementsUsedForAverage = 0;

        UpdateAverageElementSize(m_numElements, newSize);
    }

    // Resize content element
    ResizeContentElement(newSize);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::ResizeContentElement(float newSize) const
{
    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return;
    }

    // Get current content size
    AZ::Vector2 curContentSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    float curSize = m_isVertical ? curContentSize.GetY() : curContentSize.GetX();

    if (newSize != curSize)
    {
        // Resize content element

        UiTransform2dInterface::Offsets offsets;
        EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);

        AZ::Vector2 pivot;
        EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);

        float sizeDiff = newSize - curSize;

        if (m_isVertical)
        {
            offsets.m_top -= sizeDiff * pivot.GetY();
            offsets.m_bottom += sizeDiff * (1.0f - pivot.GetY());
        }
        else
        {
            offsets.m_left -= sizeDiff * pivot.GetX();
            offsets.m_right += sizeDiff * (1.0f - pivot.GetX());
        }

        EBUS_EVENT_ID(contentEntityId, UiTransform2dBus, SetOffsets, offsets);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::AdjustContentSizeAndScrollOffsetByDelta(float sizeDelta, float scrollDelta) const
{
    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return;
    }

    // Get content size
    AZ::Vector2 contentSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(contentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    if (sizeDelta != 0.0f)
    {
        if (m_isVertical)
        {
            contentSize.SetY(contentSize.GetY() + sizeDelta);
        }
        else
        {
            contentSize.SetX(contentSize.GetX() + sizeDelta);
        }
    }

    // Get scroll offset
    AZ::Vector2 scrollOffset(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(scrollOffset, GetEntityId(), UiScrollBoxBus, GetScrollOffset);

    if (scrollDelta != 0.0f)
    {
        if (m_isVertical)
        {
            scrollOffset.SetY(scrollOffset.GetY() + scrollDelta);
        }
        else
        {
            scrollOffset.SetX(scrollOffset.GetX() + scrollDelta);
        }
    }

    EBUS_EVENT_ID(GetEntityId(), UiScrollBoxBus, ChangeContentSizeAndScrollOffset, contentSize, scrollOffset);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::CalculateVariableElementSize(int index)
{
    float size = 0.0f;

    AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);

    if (index < 0 || index >= m_numElements)
    {
        return size;
    }

    ElementType elementType = GetElementTypeAtIndex(index);

    if (!m_autoCalculateElementSize[elementType])
    {
        if (m_isVertical)
        {
            if (!m_hasSections)
            {
                EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementHeight, index);
            }
            else
            {
                ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
                if (elementType == ElementType::Item)
                {
                    EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementInSectionHeight, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
                }
                else if (elementType == ElementType::SectionHeader)
                {
                    EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetSectionHeaderHeight, elementIndexInfo.m_sectionIndex);
                }
                else
                {
                    AZ_Assert(false, "unknown element type");
                }
            }
        }
        else
        {
            if (!m_hasSections)
            {
                EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementWidth, index);
            }
            else
            {
                ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
                if (elementType == ElementType::Item)
                {
                    EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetElementInSectionWidth, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
                }
                else if (elementType == ElementType::SectionHeader)
                {
                    EBUS_EVENT_ID_RESULT(size, GetEntityId(), UiDynamicScrollBoxDataBus, GetSectionHeaderWidth, elementIndexInfo.m_sectionIndex);
                }
                else
                {
                    AZ_Assert(false, "unknown element type");
                }
            }
        }
    }
    else
    {
        AZ::EntityId elementForAutoSizeCalculation = GetElementForAutoSizeCalculation(elementType);

        // Auto calculate the size of the element
        AZ_Assert(elementForAutoSizeCalculation.IsValid(), "elementForAutoSizeCalculation is invalid");

        // Notify listeners to setup this element for auto calculation
        if (!m_hasSections)
        {
            EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareElementForSizeCalculation, elementForAutoSizeCalculation, index);
        }
        else
        {
            ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(index);
            if (elementType == ElementType::Item)
            {
                EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareElementInSectionForSizeCalculation, elementForAutoSizeCalculation, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
            }
            else if (elementType == ElementType::SectionHeader)
            {
                EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnPrepareSectionHeaderForSizeCalculation, elementForAutoSizeCalculation, elementIndexInfo.m_sectionIndex);
            }
            else
            {
                AZ_Assert(false, "unknown element type");
            }
        }
        size = AutoCalculateElementSize(elementForAutoSizeCalculation);
    }

    // Cache the calculated size
    m_cachedElementInfo[index].m_size = size;

    return size;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetAndCacheVariableElementSize(int index)
{
    float size = 0.0f;

    AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);

    if (index < 0 || index >= m_numElements)
    {
        return size;
    }

    if (m_cachedElementInfo[index].m_size >= 0.0f)
    {
        // Use the cached size
        size = m_cachedElementInfo[index].m_size;
    }
    else
    {
        ElementType elementType = GetElementTypeAtIndex(index);

        if (!m_variableElementSize[elementType])
        {
            // Use the prototype element size
            size = m_prototypeElementSize[elementType];

            // Cache the calculated size
            m_cachedElementInfo[index].m_size = size;

            // Cache the accumulated size
            m_cachedElementInfo[index].m_accumulatedSize = GetVariableSizeElementOffset(index) + size;
        }
        else if (m_estimatedElementSize[elementType] > 0.0f)
        {
            // Uses the estimated element size
            size = m_estimatedElementSize[elementType];
        }
        else
        {
            size = CalculateVariableElementSize(index);

            // Cache the accumulated size
            m_cachedElementInfo[index].m_accumulatedSize = GetVariableSizeElementOffset(index) + size;
        }
    }

    return size;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetVariableElementSize(int index) const
{
    float size = 0.0f;

    AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);

    if (index < 0 || index >= m_numElements)
    {
        return size;
    }

    if (m_cachedElementInfo[index].m_size >= 0.0f)
    {
        // Use the cached size
        size = m_cachedElementInfo[index].m_size;
    }
    else
    {
        ElementType elementType = GetElementTypeAtIndex(index);

        if (m_estimatedElementSize[elementType] > 0.0f)
        {
            // Uses the estimated element size
            size = m_estimatedElementSize[elementType];
        }
        else
        {
            AZ_Assert(false, "GetVariableElementSize is being called before size is known");
        }
    }

    return size;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::GetLastKnownAccumulatedSizeIndex(int index, int numElementsWithUnknownSizeOut[ElementType::NumElementTypes]) const
{
    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        numElementsWithUnknownSizeOut[i] = 0;
    }

    for (int i = index - 1; i >= 0; i--)
    {
        if (m_cachedElementInfo[i].m_accumulatedSize >= 0.0f)
        {
            return i;
        }

        ElementType elementType = GetElementTypeAtIndex(i);
        ++numElementsWithUnknownSizeOut[elementType];
    }

    return -1;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetElementOffsetAtIndex(int index) const
{
    float offset = 0.0f;

    if (!AnyElementTypesHaveVariableSize())
    {
        offset = GetFixedSizeElementOffset(index);
    }
    else
    {
        offset = GetVariableSizeElementOffset(index);
    }

    return offset;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetFixedSizeElementOffset(int index) const
{
    float offset = 0.0f;

    if (!m_hasSections)
    {
        offset = m_prototypeElementSize[ElementType::Item] * index;
    }
    else
    {
        int numHeaders = 0;
        int numItems = 0;

        int numSections = m_sections.size();
        if (numSections > 0)
        {
            if (index > m_sections[numSections - 1].m_headerElementIndex)
            {
                numHeaders = numSections;
            }
            else
            {
                for (int i = 0; i < numSections; i++)
                {
                    if (index <= m_sections[i].m_headerElementIndex)
                    {
                        numHeaders = i;
                        break;
                    }
                }
            }

            numItems = index - numHeaders;
        }

        offset = numHeaders * m_prototypeElementSize[ElementType::SectionHeader] + numItems * m_prototypeElementSize[ElementType::Item];
    }

    return offset;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetVariableSizeElementOffset(int index) const
{
    float offset = 0.0f;

    AZ_Assert(index >= 0 && index < m_numElements, "index %d out of range", index);

    if (index < 0 || index >= m_numElements)
    {
        return offset;
    }

    if (index > 0)
    {
        if (m_cachedElementInfo[index - 1].m_accumulatedSize >= 0.0f)
        {
            offset = m_cachedElementInfo[index - 1].m_accumulatedSize;
        }
        else
        {
            // Calculate the accumulated size
            int numElementsWithUnknownSizeOut[ElementType::NumElementTypes];
            int lastKnownIndex = GetLastKnownAccumulatedSizeIndex(index, numElementsWithUnknownSizeOut);

            offset = lastKnownIndex >= 0 ? m_cachedElementInfo[lastKnownIndex].m_accumulatedSize : 0.0f;
            for (int i = 0; i < ElementType::NumElementTypes; i++)
            {
                offset += numElementsWithUnknownSizeOut[i] * m_estimatedElementSize[i];
            }
        }
    }

    return offset;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::UpdateAverageElementSize(int numAddedElements, float sizeDelta)
{
    float curTotalSize = m_averageElementSize * m_numElementsUsedForAverage;

    m_numElementsUsedForAverage += numAddedElements;
    m_averageElementSize = (m_numElementsUsedForAverage > 0) ? (AZ::GetMax(curTotalSize + sizeDelta, 0.0f) / m_numElementsUsedForAverage) : 0.0f;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::ClearDisplayedElements()
{
    for (const auto& e : m_displayedElements)
    {
        ElementType elementType = e.m_type;
        m_recycledElements[elementType].push_front(e.m_element);

        // Disable element
        EBUS_EVENT_ID(e.m_element, UiElementBus, SetIsEnabled, false);
    }

    m_displayedElements.clear();

    m_firstDisplayedElementIndex = -1;
    m_lastDisplayedElementIndex = -1;
    m_firstVisibleElementIndex = -1;
    m_lastVisibleElementIndex = -1;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::FindDisplayedElementWithIndex(int index) const
{
    AZ::EntityId elementId;

    for (const auto& e : m_displayedElements)
    {
        if (e.m_elementIndex == index)
        {
            elementId = e.m_element;
            break;
        }
    }

    return elementId;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::GetVisibleAreaSize() const
{
    float visibleAreaSize = 0.0f;

    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return visibleAreaSize;
    }

    // Get content's parent
    AZ::EntityId contentParentEntityId;
    EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
    if (!contentParentEntityId.IsValid())
    {
        return visibleAreaSize;
    }

    // Get content parent's size in canvas space
    AZ::Vector2 contentParentSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(contentParentSize, contentParentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    if (m_isVertical)
    {
        visibleAreaSize = contentParentSize.GetY();
    }
    else
    {
        visibleAreaSize = contentParentSize.GetX();
    }

    return visibleAreaSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::AreAnyElementsVisible(AZ::Vector2& visibleContentBoundsOut) const
{
    if (m_numElements == 0)
    {
        return false;
    }

    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return false;
    }

    // Get content's parent
    AZ::EntityId contentParentEntityId;
    EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
    if (!contentParentEntityId.IsValid())
    {
        return false;
    }

    // Get content's rect in canvas space
    UiTransformInterface::Rect contentRect;
    EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);

    // Get content parent's rect in canvas space
    UiTransformInterface::Rect parentRect;
    EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);

    // Check if any items are visible
    AZ::Vector2 minA(contentRect.left, contentRect.top);
    AZ::Vector2 maxA(contentRect.right, contentRect.bottom);
    AZ::Vector2 minB(parentRect.left, parentRect.top);
    AZ::Vector2 maxB(parentRect.right, parentRect.bottom);

    bool boxesIntersect = true;

    if (maxA.GetX() < minB.GetX() || // a is left of b
        minA.GetX() > maxB.GetX() || // a is right of b
        maxA.GetY() < minB.GetY() || // a is above b
        minA.GetY() > maxB.GetY())   // a is below b
    {
        boxesIntersect = false;   // no overlap
    }

    if (boxesIntersect)
    {
        // Set visible content bounds
        if (m_isVertical)
        {
            // Set top offset
            visibleContentBoundsOut.SetX(AZ::GetMax(parentRect.top - contentRect.top, 0.0f));

            // Set bottom offset
            visibleContentBoundsOut.SetY(AZ::GetMin(parentRect.bottom, contentRect.bottom) - contentRect.top);
        }
        else
        {
            // Set left offset
            visibleContentBoundsOut.SetX(AZ::GetMax(parentRect.left - contentRect.left, 0.0f));

            // Set right offset
            visibleContentBoundsOut.SetY(AZ::GetMin(parentRect.right, contentRect.right) - contentRect.left);
        }
    }

    return boxesIntersect;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::UpdateElementVisibility(bool keepAtEndIfWasAtEnd)
{
    // Calculate which elements are visible
    int firstVisibleElementIndex = -1;
    int lastVisibleElementIndex = -1;
    int firstDisplayedElementIndex = -1;
    int lastDisplayedElementIndex = -1;
    int firstDisplayedElementIndexWithSizeChange = -1;
    float totalElementSizeChange = 0.0f;
    float scrollChange = 0.0f;

    AZ::Vector2 visibleContentBounds(0.0f, 0.0f);
    bool elementsVisible = AreAnyElementsVisible(visibleContentBounds);

    if (elementsVisible)
    {
        CalculateVisibleElementIndices(keepAtEndIfWasAtEnd,
            visibleContentBounds,
            firstVisibleElementIndex,
            lastVisibleElementIndex,
            firstDisplayedElementIndex,
            lastDisplayedElementIndex,
            firstDisplayedElementIndexWithSizeChange,
            totalElementSizeChange,
            scrollChange);
    }

    m_lastCalculatedVisibleContentOffset = visibleContentBounds.GetX();
    if (totalElementSizeChange != 0.0f)
    {
        m_lastCalculatedVisibleContentOffset += CalculateContentBeginningDeltaAfterSizeChange(totalElementSizeChange);
    }

    if (StickyHeadersEnabled())
    {
        UpdateStickyHeader(firstVisibleElementIndex, lastVisibleElementIndex, m_lastCalculatedVisibleContentOffset);
    }

    // Remove the elements that are no longer being displayed
    m_displayedElements.remove_if(
        [this, firstDisplayedElementIndex, lastDisplayedElementIndex](const DisplayedElement& e)
    {
        if ((firstDisplayedElementIndex < 0) || (e.m_elementIndex < firstDisplayedElementIndex) || (e.m_elementIndex > lastDisplayedElementIndex))
        {
            // This element is no longer being displayed, move it to the recycled elements list
            m_recycledElements[e.m_type].push_front(e.m_element);

            // Disable element
            EBUS_EVENT_ID(e.m_element, UiElementBus, SetIsEnabled, false);

            // Remove element from the displayed element list
            return true;
        }
        else
        {
            return false;
        }
    }
    );

    // Add the newly displayed elements
    if (firstDisplayedElementIndex >= 0)
    {
        for (int i = firstDisplayedElementIndex; i <= lastDisplayedElementIndex; i++)
        {
            if (!IsElementDisplayedAtIndex(i))
            {
                ElementType elementType = GetElementTypeAtIndex(i);
                ElementIndexInfo elementIndexInfo = GetElementIndexInfoFromIndex(i);

                AZ::EntityId element = GetElementForDisplay(elementType);
                DisplayedElement elementEntry;
                elementEntry.m_element = element;
                elementEntry.m_elementIndex = i;
                elementEntry.m_indexInfo = elementIndexInfo;
                elementEntry.m_type = elementType;

                m_displayedElements.push_front(elementEntry);

                if (m_variableElementSize[elementType])
                {
                    SizeVariableElementAtIndex(element, i);
                }

                PositionElementAtIndex(element, i);

                // Notify listeners that this element is about to be displayed
                if (!m_hasSections)
                {
                    EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnElementBecomingVisible, element, i);
                }
                else
                {
                    if (elementType == ElementType::Item)
                    {
                        EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnElementInSectionBecomingVisible, element, elementIndexInfo.m_sectionIndex, elementIndexInfo.m_itemIndexInSection);
                    }
                    else if (elementType == ElementType::SectionHeader)
                    {
                        EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnSectionHeaderBecomingVisible, element, elementIndexInfo.m_sectionIndex);
                    }
                    else
                    {
                        AZ_Assert(false, "unknown element type");
                    }
                }
            }
            else
            {
                if (firstDisplayedElementIndexWithSizeChange >= 0 && firstDisplayedElementIndexWithSizeChange <= i)
                {
                    AZ::EntityId element = FindDisplayedElementWithIndex(i);
                    PositionElementAtIndex(element, i);
                }
            }
        }
    }

    m_firstVisibleElementIndex = firstVisibleElementIndex;
    m_lastVisibleElementIndex = lastVisibleElementIndex;
    m_firstDisplayedElementIndex = firstDisplayedElementIndex;
    m_lastDisplayedElementIndex = lastDisplayedElementIndex;

    if (totalElementSizeChange != 0.0f || scrollChange != 0.0f)
    {
        AdjustContentSizeAndScrollOffsetByDelta(totalElementSizeChange, scrollChange);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::CalculateVisibleElementIndices(bool keepAtEndIfWasAtEnd,
    const AZ::Vector2& visibleContentBounds,
    int& firstVisibleElementIndexOut,
    int& lastVisibleElementIndexOut,
    int& firstDisplayedElementIndexOut,
    int& lastDisplayedElementIndexOut,
    int& firstDisplayedElementIndexWithSizeChangeOut,
    float& totalElementSizeChangeOut,
    float& scrollChangeOut)
{
    firstVisibleElementIndexOut = -1;
    lastVisibleElementIndexOut = -1;
    firstDisplayedElementIndexOut = -1;
    lastDisplayedElementIndexOut = -1;
    firstDisplayedElementIndexWithSizeChangeOut = -1;
    totalElementSizeChangeOut = 0.0f;
    scrollChangeOut = 0.0f;

    if (!AllPrototypeElementsValid())
    {
        return;
    }

    bool addedExtraElementsForNavigation = false;

    if (!AnyElementTypesHaveVariableSize())
    {
        // All elements are the same size
        FindVisibleElementIndicesForFixedSizes(visibleContentBounds, firstVisibleElementIndexOut, lastVisibleElementIndexOut);
    }
    else
    {
        // Elements vary in size

        if (AnyElementTypesHaveEstimatedSizes())
        {
            // We may not have the real sizes of all the elements yet

            // Find the first elment index that's visible and that will remain in the same position
            int visibleElementIndex = -1;

            bool keepAtEnd = keepAtEndIfWasAtEnd && IsScrolledToEnd();
            if (keepAtEnd)
            {
                visibleElementIndex = m_numElements - 1;
            }
            else
            {
                visibleElementIndex = FindVisibleElementIndexToRemainInPlace(visibleContentBounds);
            }

            // Calculate the first and last visible elements without moving the beginning (top or left) of the specified visible element index
            CalculateVisibleElementIndicesFromVisibleElementIndex(visibleElementIndex,
                visibleContentBounds,
                keepAtEnd,
                firstVisibleElementIndexOut,
                lastVisibleElementIndexOut,
                firstDisplayedElementIndexOut,
                lastDisplayedElementIndexOut,
                firstDisplayedElementIndexWithSizeChangeOut,
                totalElementSizeChangeOut,
                scrollChangeOut);

            addedExtraElementsForNavigation = true;
        }
        else
        {
            // We have the real sizes of all the elements

            // Estimate a first visible element index
            int estimatedFirstVisibleElementIndex = EstimateFirstVisibleElementIndex(visibleContentBounds);

            // Look for the real new first visible element index
            float curElementEnd = 0.0f;
            firstVisibleElementIndexOut = FindFirstVisibleElementIndex(estimatedFirstVisibleElementIndex, visibleContentBounds, curElementEnd);

            // Now find the last visible element index
            lastVisibleElementIndexOut = firstVisibleElementIndexOut;
            while (curElementEnd < visibleContentBounds.GetY() && lastVisibleElementIndexOut < m_numElements - 1)
            {
                ++lastVisibleElementIndexOut;
                curElementEnd += GetVariableElementSize(lastVisibleElementIndexOut);
            }
        }
    }

    if (!addedExtraElementsForNavigation)
    {
        firstDisplayedElementIndexOut = firstVisibleElementIndexOut;
        lastDisplayedElementIndexOut = lastVisibleElementIndexOut;

        AddExtraElementsForNavigation(firstDisplayedElementIndexOut, lastDisplayedElementIndexOut);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::UpdateStickyHeader(int firstVisibleElementIndex, int lastVisibleElementIndex, float visibleContentBeginning)
{
    // Find which header should currently be sticky
    if (firstVisibleElementIndex >= 0)
    {
        ElementIndexInfo firstVisibleElementIndexInfo = GetElementIndexInfoFromIndex(firstVisibleElementIndex);
        int newStickyHeaderElementIndex = m_sections[firstVisibleElementIndexInfo.m_sectionIndex].m_headerElementIndex;
        if (newStickyHeaderElementIndex != m_currentStickyHeader.m_elementIndex)
        {
            if (m_currentStickyHeader.m_elementIndex < 0)
            {
                EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, true);
            }

            m_currentStickyHeader.m_elementIndex = newStickyHeaderElementIndex;
            m_currentStickyHeader.m_indexInfo.m_sectionIndex = firstVisibleElementIndexInfo.m_sectionIndex;

            if (m_variableElementSize[ElementType::SectionHeader])
            {
                SizeVariableElementAtIndex(m_currentStickyHeader.m_element, m_currentStickyHeader.m_elementIndex);
            }

            EBUS_EVENT_ID(GetEntityId(), UiDynamicScrollBoxElementNotificationBus, OnSectionHeaderBecomingVisible, m_currentStickyHeader.m_element, m_currentStickyHeader.m_indexInfo.m_sectionIndex);
        }

        float stickyHeaderOffset = 0.0f;

        // Check if the current sticky header is being pushed out of the way by another visible header
        int firstVisibleHeaderIndex = FindFirstVisibleHeaderIndex(firstVisibleElementIndex, lastVisibleElementIndex, m_currentStickyHeader.m_elementIndex);
        if (firstVisibleHeaderIndex >= 0)
        {
            // Get the beginning of the first visible header
            float firstVisibleHeaderBeginning = GetElementOffsetAtIndex(firstVisibleHeaderIndex);

            // Get the end of the current sticky header
            float stickyHeaderSize = !m_variableElementSize[ElementType::SectionHeader] ? m_prototypeElementSize[ElementType::SectionHeader] : GetVariableElementSize(m_currentStickyHeader.m_elementIndex);
            float stickyHeaderEnd = visibleContentBeginning + stickyHeaderSize;

            // Adjust sticky header offset
            if (firstVisibleHeaderBeginning < stickyHeaderEnd)
            {
                stickyHeaderOffset = firstVisibleHeaderBeginning - stickyHeaderEnd;
            }
        }

        SetElementOffsets(m_currentStickyHeader.m_element, stickyHeaderOffset);
    }
    else
    {
        m_currentStickyHeader.m_elementIndex = -1;
        m_currentStickyHeader.m_indexInfo.m_sectionIndex = -1;

        // Hide the sticky header
        EBUS_EVENT_ID(m_currentStickyHeader.m_element, UiElementBus, SetIsEnabled, false);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::FindFirstVisibleHeaderIndex(int firstVisibleElementIndex, int lastVisibleElementIndex, int excludeIndex)
{
    int firstVisibleHeaderIndex = -1;

    for (int i = firstVisibleElementIndex; i <= lastVisibleElementIndex; i++)
    {
        if (i != excludeIndex && GetElementTypeAtIndex(i) == ElementType::SectionHeader)
        {
            firstVisibleHeaderIndex = i;
            break;
        }
    }

    return firstVisibleHeaderIndex;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::FindVisibleElementIndicesForFixedSizes(const AZ::Vector2& visibleContentBounds,
    int& firstVisibleElementIndexOut,
    int& lastVisibleElementIndexOut) const
{
    float itemSize = m_prototypeElementSize[ElementType::Item];
    float beginningVisibleOffset = visibleContentBounds.GetX();
    float endVisibleOffset = visibleContentBounds.GetY();

    if (!m_hasSections)
    {
        if (itemSize > 0.0f)
        {
            // Calculate first visible element index
            firstVisibleElementIndexOut = max(static_cast<int>(ceil(beginningVisibleOffset / itemSize)) - 1, 0);

            // Calculate last visible element index
            lastVisibleElementIndexOut = static_cast<int>(ceil(endVisibleOffset / itemSize)) - 1;
            int lastElementIndex = max(m_numElements - 1, 0);
            Limit(lastVisibleElementIndexOut, 0, lastElementIndex);
        }
    }
    else
    {
        float headerSize = m_prototypeElementSize[ElementType::SectionHeader];

        if (itemSize > 0.0f || headerSize > 0.0f)
        {
            // Calculate first and last visible element indices
            float curElementOffset = 0.0f;
            int curSectionIndex = 0;
            for (int i = 0; i < 2; i++)
            {
                int& visibleElementIndex = (i == 0) ? firstVisibleElementIndexOut : lastVisibleElementIndexOut;
                float visibleOffset = (i == 0) ? beginningVisibleOffset : endVisibleOffset;

                for (; curSectionIndex < m_sections.size(); curSectionIndex++)
                {
                    float headerElementEnd = curElementOffset + headerSize;
                    if (headerElementEnd >= visibleOffset)
                    {
                        visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex;
                        break;
                    }
                    else
                    {
                        float sectionEnd = headerElementEnd + itemSize * m_sections[curSectionIndex].m_numItems;
                        if (sectionEnd >= visibleOffset)
                        {
                            int numItems = 0;
                            if (itemSize > 0.0f)
                            {
                                numItems = static_cast<int>(ceil((visibleOffset - headerElementEnd) / itemSize));
                            }
                            visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex + numItems;
                            break;
                        }
                        else if (curSectionIndex == m_sections.size() - 1)
                        {
                            visibleElementIndex = m_sections[curSectionIndex].m_headerElementIndex + m_sections[curSectionIndex].m_numItems;
                            break;
                        }

                        curElementOffset = sectionEnd;
                    }
                }
            }
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::FindVisibleElementIndexToRemainInPlace(const AZ::Vector2& visibleContentBounds) const
{
    int visibleElementIndex = -1;

    // Try and find the first previously visible element that's still visible
    int firstPrevVisibleIndexStillVisible = -1;
    if (m_firstVisibleElementIndex >= 0)
    {
        // Check if any of the previously visible elements are still visible
        float prevFirstVisibleBeginning = GetVariableSizeElementOffset(m_firstVisibleElementIndex);
        float prevLastVisibleEnd = GetVariableSizeElementOffset(m_lastVisibleElementIndex) + GetVariableElementSize(m_lastVisibleElementIndex);

        if (!(prevFirstVisibleBeginning > visibleContentBounds.GetY() || prevLastVisibleEnd < visibleContentBounds.GetX()))
        {
            // Find the first previously visible element that's still visible
            for (int index = m_firstVisibleElementIndex; index <= m_lastVisibleElementIndex; index++)
            {
                if (GetVariableSizeElementOffset(index) + GetVariableElementSize(index) >= visibleContentBounds.GetX())
                {
                    firstPrevVisibleIndexStillVisible = index;
                    break;
                }
            }
        }
    }

    if (firstPrevVisibleIndexStillVisible >= 0)
    {
        visibleElementIndex = firstPrevVisibleIndexStillVisible;
    }
    else
    {
        // No previously visible elements are still visible, so find the first element that's about to become visible

        // Estimate a first visible element index
        int estimatedFirstVisibleElementIndex = EstimateFirstVisibleElementIndex(visibleContentBounds);

        // Look for the real new first visible element index
        float firstVisibleElementEnd = 0.0f;
        visibleElementIndex = FindFirstVisibleElementIndex(estimatedFirstVisibleElementIndex, visibleContentBounds, firstVisibleElementEnd);

        // We actually want the first visible element who's beginning (top or left) is visible if we don't know the first visible element's real size.
        // This is so that we don't end up having to calculate the size of more elements if the real size of the first visible
        // element ends up being smaller than the estimated size
        if (m_cachedElementInfo[visibleElementIndex].m_size < 0.0f && visibleElementIndex < m_numElements - 1)
        {
            float firstVisibleElementBeginning = firstVisibleElementEnd - GetVariableElementSize(visibleElementIndex);
            if (firstVisibleElementBeginning < visibleContentBounds.GetX() && firstVisibleElementEnd < visibleContentBounds.GetY())
            {
                ++visibleElementIndex;
            }
        }
    }

    return visibleElementIndex;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::AddExtraElementsForNavigation(int& firstDisplayedElementIndexOut, int& lastDisplayedElementIndexOut) const
{
    if (AnyPrototypeElementsNavigable())
    {
        if (firstDisplayedElementIndexOut > 0)
        {
            --firstDisplayedElementIndexOut;
            if (m_hasSections)
            {
                int newFirstDisplayedElementIndex = firstDisplayedElementIndexOut;
                while (!m_isPrototypeElementNavigable[GetElementTypeAtIndex(newFirstDisplayedElementIndex)] && newFirstDisplayedElementIndex >= 0)
                {
                    --newFirstDisplayedElementIndex;
                }
                if (newFirstDisplayedElementIndex >= 0)
                {
                    firstDisplayedElementIndexOut = newFirstDisplayedElementIndex;
                }
            }
        }

        if (lastDisplayedElementIndexOut > -1 && lastDisplayedElementIndexOut < m_numElements - 1)
        {
            ++lastDisplayedElementIndexOut;
            if (m_hasSections)
            {
                int newLastDisplayedElementIndex = lastDisplayedElementIndexOut;
                while (!m_isPrototypeElementNavigable[GetElementTypeAtIndex(newLastDisplayedElementIndex)] && newLastDisplayedElementIndex < m_numElements)
                {
                    ++newLastDisplayedElementIndex;
                }
                if (newLastDisplayedElementIndex < m_numElements)
                {
                    lastDisplayedElementIndexOut = newLastDisplayedElementIndex;
                }
            }
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::EstimateFirstVisibleElementIndex(const AZ::Vector2& visibleContentBounds) const
{
    // Estimate an index size that will be close to the new first visible element index
    int estimatedElementIndex = 0;

    if (m_averageElementSize > 0.0f)
    {
        if (m_firstVisibleElementIndex >= 0)
        {
            // Check how much scrolling has occurred
            float scrollDelta = visibleContentBounds.GetX() - m_lastCalculatedVisibleContentOffset;
            // Estimate the number of elements within the scroll delta
            int estimatedElementIndexOffset = max(static_cast<int>(ceil(fabs(scrollDelta / m_averageElementSize))) - 1, 0);
            estimatedElementIndex = m_firstVisibleElementIndex + (scrollDelta > 0.0f ? estimatedElementIndexOffset : -estimatedElementIndexOffset);
        }
        else
        {
            estimatedElementIndex = max(static_cast<int>(ceil(visibleContentBounds.GetX() / m_averageElementSize)) - 1, 0);
        }
    }

    estimatedElementIndex = AZ::GetClamp(estimatedElementIndex, 0, m_numElements - 1);

    return estimatedElementIndex;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::FindFirstVisibleElementIndex(int estimatedIndex, const AZ::Vector2& visibleContentBounds, float& firstVisibleElementEndOut) const
{
    int curElementIndex = estimatedIndex;
    float curElementPos = GetVariableSizeElementOffset(curElementIndex);
    if (curElementPos <= visibleContentBounds.GetX())
    {
        // Traverse down to find the real new first visible element index
        curElementPos += GetVariableElementSize(curElementIndex);
        while (curElementPos < visibleContentBounds.GetX() && curElementIndex < m_numElements - 1)
        {
            ++curElementIndex;
            curElementPos += GetVariableElementSize(curElementIndex);
        }
    }
    else
    {
        // Traverse up to find the real new first visible element index
        while (curElementPos > visibleContentBounds.GetX() && curElementIndex > 0)
        {
            --curElementIndex;
            curElementPos -= GetVariableElementSize(curElementIndex);
        }
        curElementPos += GetVariableElementSize(curElementIndex);
    }

    firstVisibleElementEndOut = curElementPos;

    return curElementIndex;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::CalculateVisibleSpaceBeforeAndAfterElement(int visibleElementIndex,
    bool keepAtEnd,
    float visibleAreaBeginning,
    float& spaceLeftBeforeOut,
    float& spaceLeftAfterOut) const
{
    float visibleAreaSize = GetVisibleAreaSize();

    // Calculate space left in the visible area
    spaceLeftBeforeOut = 0.0f;
    spaceLeftAfterOut = 0.0f;
    if (keepAtEnd)
    {
        spaceLeftAfterOut = 0.0f;
        spaceLeftBeforeOut = AZ::GetMax(visibleAreaSize - GetVariableElementSize(visibleElementIndex), 0.0f);
    }
    else
    {
        float visibleElementBeginning = GetVariableSizeElementOffset(visibleElementIndex);
        float visibleElementEnd = visibleElementBeginning + GetVariableElementSize(visibleElementIndex);
        spaceLeftBeforeOut = AZ::GetMax(visibleElementBeginning - visibleAreaBeginning, 0.0f);
        spaceLeftAfterOut = AZ::GetMax(visibleAreaSize - (visibleElementEnd - visibleAreaBeginning), 0.0f);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::CalculateVisibleElementIndicesFromVisibleElementIndex(int visibleElementIndex,
    const AZ::Vector2& visibleContentBound,
    bool keepAtEnd,
    int& firstVisibleElementIndexOut,
    int& lastVisibleElementIndexOut,
    int& firstDisplayedElementIndexOut,
    int& lastDisplayedElementIndexOut,
    int& firstDisplayedElementIndexWithSizeChangeOut,
    float& totalElementSizechangeOut,
    float& scrollChangeOut)
{
    // From the current element index that we know is going to be at least partly visible,
    // traverse up and down to find the real first and last visible element indices

    // Track the total change in element size
    float totalSizeChange = 0.0f;

    // Track the total change in size of elements that are positioned before the passed in
    // visible element index who's beginning (top or left) will remain in the same position
    float totalSizeChangeBeforeFixedVisibleElement = 0.0f;

    // Keep track of the index of the first element who's size changed
    firstDisplayedElementIndexWithSizeChangeOut = -1;

    // Check if we need to calculate the real size for the known visible element index
    if (m_cachedElementInfo[visibleElementIndex].m_size < 0.0f)
    {
        float prevSize = GetVariableElementSize(visibleElementIndex);
        float newSize = CalculateVariableElementSize(visibleElementIndex);

        totalSizeChange = newSize - prevSize;
        firstDisplayedElementIndexWithSizeChangeOut = visibleElementIndex;
    }

    // Calculate visible space remaining
    float spaceLeftBefore = 0.0f;
    float spaceLeftAfter = 0.0f;
    CalculateVisibleSpaceBeforeAndAfterElement(visibleElementIndex, keepAtEnd, visibleContentBound.GetX(), spaceLeftBefore, spaceLeftAfter);

    firstVisibleElementIndexOut = visibleElementIndex;
    lastVisibleElementIndexOut = visibleElementIndex;
    firstDisplayedElementIndexOut = firstVisibleElementIndexOut;
    lastDisplayedElementIndexOut = lastVisibleElementIndexOut;

    bool extraElementsNeededForNavigation = AnyPrototypeElementsNavigable();

    // Traverse up or left
    bool hadSpaceLeft = true;
    bool addedExtraElements = false;
    while ((spaceLeftBefore > 0.0f || !addedExtraElements) && firstDisplayedElementIndexOut > 0)
    {
        if (spaceLeftBefore <= 0.0f)
        {
            if (hadSpaceLeft)
            {
                firstVisibleElementIndexOut = firstDisplayedElementIndexOut;
                hadSpaceLeft = false;
            }

            if (!extraElementsNeededForNavigation)
            {
                break;
            }

            addedExtraElements = !m_hasSections || m_isPrototypeElementNavigable[GetElementTypeAtIndex(firstDisplayedElementIndexOut - 1)];
        }

        --firstDisplayedElementIndexOut;
        if (m_cachedElementInfo[firstDisplayedElementIndexOut].m_size >= 0.0f)
        {
            spaceLeftBefore -= m_cachedElementInfo[firstDisplayedElementIndexOut].m_size;
        }
        else
        {
            // Calculate this element's size
            float prevSize = GetVariableElementSize(firstDisplayedElementIndexOut);
            float newSize = CalculateVariableElementSize(firstDisplayedElementIndexOut);

            float sizeChange = newSize - prevSize;
            totalSizeChange += sizeChange;

            if (firstDisplayedElementIndexOut <= visibleElementIndex)
            {
                totalSizeChangeBeforeFixedVisibleElement += sizeChange;
            }

            spaceLeftBefore -= newSize;

            if (firstDisplayedElementIndexWithSizeChangeOut < 0 || firstDisplayedElementIndexOut < firstDisplayedElementIndexWithSizeChangeOut)
            {
                firstDisplayedElementIndexWithSizeChangeOut = firstDisplayedElementIndexOut;
            }
        }
    }

    if (hadSpaceLeft)
    {
        firstVisibleElementIndexOut = firstDisplayedElementIndexOut;
    }

    // Traverse down or right
    hadSpaceLeft = true;
    addedExtraElements = false;
    while ((spaceLeftAfter > 0.0f || !addedExtraElements) && lastDisplayedElementIndexOut < m_numElements - 1)
    {
        if (spaceLeftAfter <= 0.0f)
        {
            if (hadSpaceLeft)
            {
                lastVisibleElementIndexOut = lastDisplayedElementIndexOut;
                hadSpaceLeft = false;
            }

            if (!extraElementsNeededForNavigation)
            {
                break;
            }

            addedExtraElements = !m_hasSections || m_isPrototypeElementNavigable[GetElementTypeAtIndex(lastDisplayedElementIndexOut + 1)];
        }

        ++lastDisplayedElementIndexOut;
        if (m_cachedElementInfo[lastDisplayedElementIndexOut].m_size >= 0.0f)
        {
            spaceLeftAfter -= m_cachedElementInfo[lastDisplayedElementIndexOut].m_size;
        }
        else
        {
            // Calculate this element's size
            float prevSize = GetVariableElementSize(lastDisplayedElementIndexOut);
            float newSize = CalculateVariableElementSize(lastDisplayedElementIndexOut);

            float sizeChange = newSize - prevSize;
            totalSizeChange += sizeChange;

            if (lastDisplayedElementIndexOut <= visibleElementIndex)
            {
                totalSizeChangeBeforeFixedVisibleElement += sizeChange;
            }

            spaceLeftAfter -= newSize;

            if (firstDisplayedElementIndexWithSizeChangeOut < 0 || lastDisplayedElementIndexOut < firstDisplayedElementIndexWithSizeChangeOut)
            {
                firstDisplayedElementIndexWithSizeChangeOut = lastDisplayedElementIndexOut;
            }
        }
    }

    if (hadSpaceLeft)
    {
        lastVisibleElementIndexOut = lastDisplayedElementIndexOut;
    }

    if (StickyHeadersEnabled())
    {
        // Check which header should currently be sticky and calculate its size if needed
        if (firstVisibleElementIndexOut >= 0)
        {
            ElementIndexInfo firstVisibleElementIndexInfo = GetElementIndexInfoFromIndex(firstVisibleElementIndexOut);
            int stickyHeaderElementIndex = m_sections[firstVisibleElementIndexInfo.m_sectionIndex].m_headerElementIndex;

            if (m_cachedElementInfo[stickyHeaderElementIndex].m_size < 0.0f)
            {
                // Calculate this element's size    
                float prevSize = GetVariableElementSize(stickyHeaderElementIndex);
                float newSize = CalculateVariableElementSize(stickyHeaderElementIndex);

                float sizeChange = newSize - prevSize;
                totalSizeChange += sizeChange;

                // Cache the accumulated size
                m_cachedElementInfo[stickyHeaderElementIndex].m_accumulatedSize = GetVariableSizeElementOffset(stickyHeaderElementIndex) + newSize;

                // Update accumulated sizes for all elements after the sticky header and before the first displayed element who's size changed.
                // The rest of the cache updates for the displayed elements who's size changed will be handled below
                for (int index = stickyHeaderElementIndex + 1; index < AZ::GetMax(firstDisplayedElementIndexWithSizeChangeOut, firstDisplayedElementIndexOut); index++)
                {
                    if (m_cachedElementInfo[index].m_accumulatedSize >= 0.0f)
                    {
                        m_cachedElementInfo[index].m_accumulatedSize += sizeChange;
                    }
                }

                if (stickyHeaderElementIndex <= visibleElementIndex)
                {
                    totalSizeChangeBeforeFixedVisibleElement += sizeChange;
                }

                if (firstDisplayedElementIndexWithSizeChangeOut < 0 || stickyHeaderElementIndex < firstDisplayedElementIndexWithSizeChangeOut)
                {
                    firstDisplayedElementIndexWithSizeChangeOut = stickyHeaderElementIndex;
                }
            }
        }
    }

    DisableElementsForAutoSizeCalculation();

    // Update the cache info
    if (firstDisplayedElementIndexWithSizeChangeOut >= 0)
    {
        // Cache the accumulated sizes for the displayed elements who's sizes were just calculated and cached
        int startIndex = AZ::GetMax(firstDisplayedElementIndexWithSizeChangeOut, firstDisplayedElementIndexOut);
        float curPos = GetVariableSizeElementOffset(startIndex);
        for (int index = startIndex; index <= lastDisplayedElementIndexOut; index++)
        {
            curPos += m_cachedElementInfo[index].m_size;
            m_cachedElementInfo[index].m_accumulatedSize = curPos;
        }

        // Update accumulated sizes for all elements after the last displayed element
        for (int index = lastDisplayedElementIndexOut + 1; index < m_numElements; index++)
        {
            if (m_cachedElementInfo[index].m_accumulatedSize >= 0.0f)
            {
                m_cachedElementInfo[index].m_accumulatedSize += totalSizeChange;
            }
        }
    }

    UpdateAverageElementSize(0, totalSizeChange);
    
    scrollChangeOut = 0.0f;
    if (totalSizeChange != 0.0f)
    {
        if (keepAtEnd)
        {
            scrollChangeOut = CalculateContentEndDeltaAfterSizeChange(totalSizeChange);
        }
        else
        {
            scrollChangeOut = CalculateContentBeginningDeltaAfterSizeChange(totalSizeChange);
        }
    }
    if (!keepAtEnd)
    {
        scrollChangeOut -= totalSizeChangeBeforeFixedVisibleElement;
    }

    totalElementSizechangeOut = totalSizeChange;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::CalculateContentBeginningDeltaAfterSizeChange(float contentSizeDelta) const
{
    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return 0.0f;
    }

    // Get current content size
    AZ::Vector2 curContentSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    float curSize = m_isVertical ? curContentSize.GetY() : curContentSize.GetX();

    UiTransform2dInterface::Offsets offsets;
    EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);

    AZ::Vector2 pivot;
    EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);

    float beginningDelta = 0.0f;
    if (m_isVertical)
    {
        beginningDelta = contentSizeDelta * pivot.GetY();
    }
    else
    {
        beginningDelta = contentSizeDelta * pivot.GetX();
    }

    return beginningDelta;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::CalculateContentEndDeltaAfterSizeChange(float contentSizeDelta) const
{
    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return 0.0f;
    }

    // Get current content size
    AZ::Vector2 curContentSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(curContentSize, contentEntityId, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    float curSize = m_isVertical ? curContentSize.GetY() : curContentSize.GetX();

    UiTransform2dInterface::Offsets offsets;
    EBUS_EVENT_ID_RESULT(offsets, contentEntityId, UiTransform2dBus, GetOffsets);

    AZ::Vector2 pivot;
    EBUS_EVENT_ID_RESULT(pivot, contentEntityId, UiTransformBus, GetPivot);

    float endDelta = 0.0f;
    if (m_isVertical)
    {
        // Restore end
        endDelta = -contentSizeDelta * (1.0f - pivot.GetY());
    }
    else
    {
        // Restore end
        endDelta = -contentSizeDelta * (1.0f - pivot.GetX());
    }

    return endDelta;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::IsScrolledToEnd() const
{
    // Find the content element
    AZ::EntityId contentEntityId;
    EBUS_EVENT_ID_RESULT(contentEntityId, GetEntityId(), UiScrollBoxBus, GetContentEntity);

    if (!contentEntityId.IsValid())
    {
        return false;
    }

    // Get content's parent
    AZ::EntityId contentParentEntityId;
    EBUS_EVENT_ID_RESULT(contentParentEntityId, contentEntityId, UiElementBus, GetParentEntityId);
    if (!contentParentEntityId.IsValid())
    {
        return false;
    }

    // Get content's rect in canvas space
    UiTransformInterface::Rect contentRect;
    EBUS_EVENT_ID(contentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, contentRect);

    // Get content parent's rect in canvas space
    UiTransformInterface::Rect parentRect;
    EBUS_EVENT_ID(contentParentEntityId, UiTransformBus, GetCanvasSpaceRectNoScaleRotate, parentRect);

    bool scrolledToEnd = false;
    if (m_isVertical)
    {
        scrolledToEnd = parentRect.bottom >= contentRect.bottom;
    }
    else
    {
        scrolledToEnd = parentRect.right >= contentRect.right;
    }

    return scrolledToEnd;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::IsElementDisplayedAtIndex(int index) const
{
    if (m_firstDisplayedElementIndex < 0)
    {
        return false;
    }

    return ((index >= m_firstDisplayedElementIndex) && (index <= m_lastDisplayedElementIndex));
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetElementForDisplay(ElementType elementType)
{
    AZ::EntityId element;

    // Check if there is an existing element
    if (!m_recycledElements[elementType].empty())
    {
        element = m_recycledElements[elementType].front();
        m_recycledElements[elementType].pop_front();

        // Enable element
        EBUS_EVENT_ID(element, UiElementBus, SetIsEnabled, true);
    }
    else
    {
        element = ClonePrototypeElement(elementType);
    }

    return element;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetElementForAutoSizeCalculation(ElementType elementType)
{
    if (!m_clonedElementForAutoSizeCalculation[elementType].IsValid())
    {
        m_clonedElementForAutoSizeCalculation[elementType] = ClonePrototypeElement(elementType);
    }
    else
    {
        // Enable element
        EBUS_EVENT_ID(m_clonedElementForAutoSizeCalculation[elementType], UiElementBus, SetIsEnabled, true);
    }

    return m_clonedElementForAutoSizeCalculation[elementType];
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::DisableElementsForAutoSizeCalculation() const
{
    for (int i = 0; i < ElementType::NumElementTypes; i++)
    {
        if (m_clonedElementForAutoSizeCalculation[i].IsValid())
        {
            // Disable element
            EBUS_EVENT_ID(m_clonedElementForAutoSizeCalculation[i], UiElementBus, SetIsEnabled, false);
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
float UiDynamicScrollBoxComponent::AutoCalculateElementSize(AZ::EntityId elementForAutoSizeCalculation) const
{
    float size = 0.0f;

    if (m_isVertical)
    {
        size = UiLayoutHelpers::GetLayoutElementTargetHeight(elementForAutoSizeCalculation);
    }
    else
    {
        size = UiLayoutHelpers::GetLayoutElementTargetWidth(elementForAutoSizeCalculation);
    }

    return size;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SizeVariableElementAtIndex(AZ::EntityId element, int index) const
{
    // Get current element size
    AZ::Vector2 curElementSize(0.0f, 0.0f);
    EBUS_EVENT_ID_RESULT(curElementSize, element, UiTransformBus, GetCanvasSpaceSizeNoScaleRotate);

    float curSize = m_isVertical ? curElementSize.GetY() : curElementSize.GetX();

    // Get new element size
    float newSize = GetVariableElementSize(index);

    if (newSize != curSize)
    {
        // Resize the element

        UiTransform2dInterface::Offsets offsets;
        EBUS_EVENT_ID_RESULT(offsets, element, UiTransform2dBus, GetOffsets);

        AZ::Vector2 pivot;
        EBUS_EVENT_ID_RESULT(pivot, element, UiTransformBus, GetPivot);

        float sizeDiff = newSize - curSize;

        if (m_isVertical)
        {
            offsets.m_top -= sizeDiff * pivot.GetY();
            offsets.m_bottom += sizeDiff * (1.0f - pivot.GetY());
        }
        else
        {
            offsets.m_left -= sizeDiff * pivot.GetX();
            offsets.m_right += sizeDiff * (1.0f - pivot.GetX());
        }

        EBUS_EVENT_ID(element, UiTransform2dBus, SetOffsets, offsets);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::PositionElementAtIndex(AZ::EntityId element, int index) const
{
    // Position offsets based on index
    float offset = GetElementOffsetAtIndex(index);

    SetElementOffsets(element, offset);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetElementAnchors(AZ::EntityId element) const
{
    // Get the element anchors
    UiTransform2dInterface::Anchors anchors;
    EBUS_EVENT_ID_RESULT(anchors, element, UiTransform2dBus, GetAnchors);

    if (m_isVertical)
    {
        // Set anchors to top of parent
        anchors.m_top = 0.0f;
        anchors.m_bottom = 0.0f;
    }
    else
    {
        // Set anchors to left of parent
        anchors.m_left = 0.0f;
        anchors.m_right = 0.0f;
    }

    EBUS_EVENT_ID(element, UiTransform2dBus, SetAnchors, anchors, false, false);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiDynamicScrollBoxComponent::SetElementOffsets(AZ::EntityId element, float offset) const
{
    // Get the element offsets
    UiTransform2dInterface::Offsets offsets;
    EBUS_EVENT_ID_RESULT(offsets, element, UiTransform2dBus, GetOffsets);

    if ((m_isVertical && offsets.m_top != offset) || (!m_isVertical && offsets.m_left != offset))
    {
        if (m_isVertical)
        {
            float height = offsets.m_bottom - offsets.m_top;
            offsets.m_top = offset;
            offsets.m_bottom = offsets.m_top + height;
        }
        else
        {
            float width = offsets.m_right - offsets.m_left;
            offsets.m_left = offset;
            offsets.m_right = offsets.m_left + width;
        }

        EBUS_EVENT_ID(element, UiTransform2dBus, SetOffsets, offsets);
    }
}



////////////////////////////////////////////////////////////////////////////////////////////////////
UiDynamicScrollBoxComponent::ElementType UiDynamicScrollBoxComponent::GetElementTypeAtIndex(int index) const
{
    ElementType elementType = ElementType::Item;

    if (m_hasSections)
    {
        for (int i = 0; i < m_sections.size(); i++)
        {
            if (m_sections[i].m_headerElementIndex == index)
            {
                elementType = ElementType::SectionHeader;
                break;
            }
        }
    }

    return elementType;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
UiDynamicScrollBoxComponent::ElementIndexInfo UiDynamicScrollBoxComponent::GetElementIndexInfoFromIndex(int index) const
{
    ElementIndexInfo elementIndexInfo;
    elementIndexInfo.m_sectionIndex = -1;
    elementIndexInfo.m_itemIndexInSection = index;

    if (m_hasSections)
    {
        for (int i = 0; i < m_sections.size(); i++)
        {
            if (index <= m_sections[i].m_headerElementIndex + m_sections[i].m_numItems)
            {
                elementIndexInfo.m_sectionIndex = i;
                elementIndexInfo.m_itemIndexInSection = (index - m_sections[i].m_headerElementIndex) - 1; // for headers, this will be set to -1
                break;
            }
        }
    }

    return elementIndexInfo;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
int UiDynamicScrollBoxComponent::GetIndexFromElementIndexInfo(const ElementIndexInfo& elementIndexInfo) const
{
    int index = elementIndexInfo.m_itemIndexInSection;

    if (m_hasSections)
    {
        index += m_sections[elementIndexInfo.m_sectionIndex].m_headerElementIndex + 1;
    }

    return index;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::EntityId UiDynamicScrollBoxComponent::GetImmediateContentChildFromDescendant(AZ::EntityId childElement) const
{
    AZ::EntityId immediateChild;

    AZ::Entity* contentEntity = GetContentEntity();
    if (contentEntity)
    {
        immediateChild = childElement;
        AZ::Entity* parent = nullptr;
        EBUS_EVENT_ID_RESULT(parent, immediateChild, UiElementBus, GetParent);
        while (parent && parent != contentEntity)
        {
            immediateChild = parent->GetId();
            EBUS_EVENT_ID_RESULT(parent, immediateChild, UiElementBus, GetParent);
        }

        if (parent != contentEntity)
        {
            immediateChild.SetInvalid();
        }
    }

    return immediateChild;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::HeadersHaveVariableSizes() const
{
    return m_hasSections && m_variableHeaderElementSize;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiDynamicScrollBoxComponent::IsValidPrototype(AZ::EntityId entityId) const
{
    // Entities containing the scroll box itself are not safe to clone as they will respawn this
    // scroll box and result in infinite recursive spawning.
    if (!entityId.IsValid() || entityId == GetEntityId())
    {
        return false;
    }

    bool isEntityAncestor;
    UiElementBus::EventResult(isEntityAncestor, GetEntityId(), &UiElementBus::Events::IsAncestor, entityId);
    return !isEntityAncestor;
}