/*
* 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 "UiMarkupButtonComponent.h"

#include <LyShine/Bus/UiCanvasBus.h>
#include <LyShine/Bus/UiMarkupButtonBus.h>

#include <AzCore/Math/Crc.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzCore/RTTI/BehaviorContext.h>

namespace
{
    //! Given a UI element on a canvas, return the mouse position.
    AZ::Vector2 GetMousePosition(AZ::EntityId entityId)
    {
        AZ::EntityId canvasId;
        EBUS_EVENT_ID_RESULT(canvasId, entityId, UiElementBus, GetCanvasEntityId);
        AZ::Vector2 mousePos = AZ::Vector2::CreateZero();
        EBUS_EVENT_ID_RESULT(mousePos, canvasId, UiCanvasBus, GetMousePosition);
        return mousePos;
    }

    //! Returns an index to the given list of clickable text rects that contains the given mouse position, otherwise returns negative.
    int FindClickableTextRectIndexFromCanvasSpacePoint(const AZ::Vector2& canvasSpacePosition, const UiClickableTextInterface::ClickableTextRects& clickableTextRects)
    {
        // Iterate through the clickable rects to find one that contains the point
        int clickableRectIndex = -1;
        const int numClickableRects = clickableTextRects.size();
        for (int i = 0; i < numClickableRects; ++i)
        {
            const auto& clickableRect = clickableTextRects[i];
            const UiTransformInterface::Rect& rect = clickableRect.rect;
            const bool containedX = canvasSpacePosition.GetX() >= rect.left && canvasSpacePosition.GetX() <= rect.right;
            if (containedX)
            {
                const bool containedY = canvasSpacePosition.GetY() >= rect.top && canvasSpacePosition.GetY() <= rect.bottom;
                if (containedY)
                {
                    clickableRectIndex = i;
                    break;
                }
            }
        }

        return clickableRectIndex;
    }

    //! Returns an index to the given list of clickable text rects that contains the given mouse position, otherwise returns negative.
    int FindClickableTextRectIndexFromViewportSpacePoint(AZ::EntityId entityId, const AZ::Vector2& mousePos, const UiClickableTextInterface::ClickableTextRects& clickableTextRects)
    {
        // first transform the mousePos from viewport space to "canvas space no-scale-rotate", which is the space that clickableTextRects
        // are stored in.
        AZ::Matrix4x4 transformFromViewport;
        EBUS_EVENT_ID(entityId, UiTransformBus, GetTransformFromViewport, transformFromViewport);
        AZ::Vector3 point3(mousePos.GetX(), mousePos.GetY(), 0.0f);
        point3 = transformFromViewport * point3;
        AZ::Vector2 canvasSpacePosition(point3.GetX(), point3.GetY());

        return FindClickableTextRectIndexFromCanvasSpacePoint(canvasSpacePosition, clickableTextRects);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
//! UiMarkupButtonNotificationBus Behavior context handler class
class UiMarkupButtonNotificationBusBehaviorHandler
    : public UiMarkupButtonNotificationsBus::Handler
    , public AZ::BehaviorEBusHandler
{
public:
    AZ_EBUS_BEHAVIOR_BINDER(UiMarkupButtonNotificationBusBehaviorHandler, "{ACCF73DC-86DD-4D1C-85B3-1E016BAAA495}", AZ::SystemAllocator,
        OnHoverStart,
        OnHoverEnd,
        OnPressed,
        OnReleased,
        OnClick);

    void OnHoverStart(int id, const AZStd::string& action, const AZStd::string& data) override
    {
        Call(FN_OnHoverStart, id, action, data);
    }

    void OnHoverEnd(int id, const AZStd::string& action, const AZStd::string& data) override
    {
        Call(FN_OnHoverEnd, id, action, data);
    }

    void OnPressed(int id, const AZStd::string& action, const AZStd::string& data) override
    {
        Call(FN_OnPressed, id, action, data);
    }

    void OnReleased(int id, const AZStd::string& action, const AZStd::string& data) override
    {
        Call(FN_OnReleased, id, action, data);
    }

    void OnClick(int id, const AZStd::string& action, const AZStd::string& data) override
    {
        Call(FN_OnClick, id, action, data);
    }
};

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

////////////////////////////////////////////////////////////////////////////////////////////////////
UiMarkupButtonComponent::UiMarkupButtonComponent()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
UiMarkupButtonComponent::~UiMarkupButtonComponent()
{
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Color UiMarkupButtonComponent::GetLinkColor()
{
    return m_linkColor;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::SetLinkColor(const AZ::Color& linkColor)
{
    m_linkColor = linkColor;
    OnLinkColorChanged();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
AZ::Color UiMarkupButtonComponent::GetLinkHoverColor()
{
    return m_linkHoverColor;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::SetLinkHoverColor(const AZ::Color& linkHoverColor)
{
    m_linkHoverColor = linkHoverColor;
    OnLinkHoverColorChanged();
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiMarkupButtonComponent::HandlePressed(AZ::Vector2 point, bool& shouldStayActive)
{
    const bool handled = UiInteractableComponent::HandlePressed(point, shouldStayActive);

    if (!handled)
    {
        return false;
    }

    const int clickableRectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), point, m_clickableTextRects);
    if (clickableRectIndex >= 0)
    {
        const int clickableId = m_clickableTextRects[clickableRectIndex].id;
        const AZStd::string& action = m_clickableTextRects[clickableRectIndex].action;
        const AZStd::string& data = m_clickableTextRects[clickableRectIndex].data;
        m_clickableRectPressedIndex = clickableRectIndex;
        EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnPressed, clickableId, action, data);
    }

    return handled;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiMarkupButtonComponent::HandleReleased(AZ::Vector2 point)
{
    const bool handled = UiInteractableComponent::HandleReleased(point);

    if (!handled)
    {
        m_clickableRectPressedIndex = -1;
        return false;
    }

    // This could be negative if the clickable text change since the pressed
    // event occurred (OnClickableTextChanged resets the pressed index value).
    if (m_clickableRectPressedIndex < 0)
    {
        EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnReleased, -1, AZStd::string(), AZStd::string());
    }
    else
    {
        const int pressedClickableId = m_clickableTextRects[m_clickableRectPressedIndex].id;
        const AZStd::string& action = m_clickableTextRects[m_clickableRectPressedIndex].action;
        const AZStd::string& data = m_clickableTextRects[m_clickableRectPressedIndex].data;
        EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnReleased, pressedClickableId, action, data);

        bool onClickTriggered = false;
        const int releasedClickableRectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), point, m_clickableTextRects);
        if (releasedClickableRectIndex >= 0)
        {
            // If the release happens on the pressed link ID, trigger a click.
            const int releasedClickableId = m_clickableTextRects[releasedClickableRectIndex].id;
            if (releasedClickableId == pressedClickableId)
            {
                EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnClick, pressedClickableId, action, data);
                onClickTriggered = true;
            }
        }

        if (!onClickTriggered)
        {
            // Clear the hover state now in case this entity is no longer 
            // being hovered. This can happen when the user releases the 
            // mouse outside of the clickable text rect.
            HandleClickableHoverEnd();
        }
    }

    m_clickableRectPressedIndex = -1;

    return handled;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::Update(float deltaTime)
{
    UiInteractableComponent::Update(deltaTime);
    UpdateHover();
}


////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::OnClickableTextChanged()
{
    m_clickableTextRects.clear();
    EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, GetClickableTextRects, m_clickableTextRects);

    // Reset all links back to their non-hover color
    int lastClickableId = -1;
    for (const auto& clickableText : m_clickableTextRects)
    {
        // Color is assigned by clickable ID and it's possible for 
        // multiple clickable text rects to share the same ID, so guard
        // against unnecessary calls to set the color for IDs that have
        // previously been set.
        if (lastClickableId != clickableText.id)
        {
            EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkColor);
            lastClickableId = clickableText.id;
        }
    }

    // Because the clickable text has changed, our current hover and pressed
    // states may no longer apply. Update it again based on the new clickable
    // text rects and current mouse position.
    m_clickableRectHoverIndex = -1;
    m_clickableRectPressedIndex = -1;
    UpdateHover();
}

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

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::Activate()
{
    UiInteractableComponent::Activate();
    UiMarkupButtonBus::Handler::BusConnect(GetEntityId());
    UiClickableTextNotificationsBus::Handler::BusConnect(GetEntityId());
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::Deactivate()
{
    UiInteractableComponent::Deactivate();
    UiMarkupButtonBus::Handler::BusDisconnect(GetEntityId());
    UiClickableTextNotificationsBus::Handler::BusDisconnect(GetEntityId());
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::UpdateHover()
{
    // Don't update hover state when we're actively being pressed. If we ever
    // add a pressed color, we could update this logic so that the pressed
    // color updates when the mouse moves on/off the clickable text.
    if (m_isHandlingEvents && !m_isPressed)
    {
        AZ::EntityId canvasId;
        EBUS_EVENT_ID_RESULT(canvasId, GetEntityId(), UiElementBus, GetCanvasEntityId);

        AZ::EntityId hoverInteractable;
        EBUS_EVENT_ID_RESULT(hoverInteractable, canvasId, UiCanvasBus, GetHoverInteractable);

        // Similarly, the hover interactable won't updated while another 
        // element is being pressed - we don't want to update hover state
        // of any clickable text (on any entity) while a press is happening.
        if (hoverInteractable == GetEntityId())
        {
            const int rectIndex = FindClickableTextRectIndexFromViewportSpacePoint(GetEntityId(), GetMousePosition(GetEntityId()), m_clickableTextRects);
            int rectIndexClickableId = -1;

            if (rectIndex >= 0)
            {
                rectIndexClickableId = m_clickableTextRects[rectIndex].id;
            }

            int hoverClickableId = -1;
            if (m_clickableRectHoverIndex >= 0)
            {
                hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id;
            }
            
            const bool enteringHover = rectIndexClickableId >= 0 && hoverClickableId < 0;
            const bool leavingHover = rectIndexClickableId < 0 && hoverClickableId >= 0;
            const bool switchingHoverRect = rectIndexClickableId >= 0 && hoverClickableId >= 0 && rectIndexClickableId != hoverClickableId;
            if (enteringHover)
            {
                HandleClickableHoverStart(rectIndex);
            }
            else if (leavingHover)
            {
                HandleClickableHoverEnd();
            }
            else if (switchingHoverRect)
            {
                HandleClickableHoverEnd();
                HandleClickableHoverStart(rectIndex);
            }
        }
        else
        {
            // Not being pressed or hovered, so reset the hover index element
            // just in case it is set (this can occur if we never receive a
            // release event for the interactable).
            HandleClickableHoverEnd();
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::HandleClickableHoverStart(int clickableRectIndex)
{
    m_clickableRectHoverIndex = clickableRectIndex;
    const int clickableId = m_clickableTextRects[m_clickableRectHoverIndex].id;

    // Set the link color prior to notification being triggered in case listeners want to set
    // the color themselves.
    EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableId, m_linkHoverColor);

    const AZStd::string& action = m_clickableTextRects[m_clickableRectHoverIndex].action;
    const AZStd::string& data = m_clickableTextRects[m_clickableRectHoverIndex].data;
    EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnHoverStart, clickableId, action, data);
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::HandleClickableHoverEnd()
{
    if (m_clickableRectHoverIndex >= 0)
    {
        const int clickableId = m_clickableTextRects[m_clickableRectHoverIndex].id;

        // Set the link color prior to notification being triggered in case listeners want to set
        // the color themselves.
        EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableId, m_linkColor);

        const AZStd::string& action = m_clickableTextRects[m_clickableRectHoverIndex].action;
        const AZStd::string& data = m_clickableTextRects[m_clickableRectHoverIndex].data;
        m_clickableRectHoverIndex = -1;
        EBUS_EVENT_ID(GetEntityId(), UiMarkupButtonNotificationsBus, OnHoverEnd, clickableId, action, data);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::OnLinkColorChanged()
{
    // If  a link is being hovered (e.g. if SetLinkColor called at runtime while a link is being hovered)
    // then we do not want to set the color of that link
    int hoverClickableId = -1;
    if (m_clickableRectHoverIndex >= 0)
    {
        hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id;
    }

    // Set all links to the new link color (unless they are currently being hovered)
    int lastClickableId = -1;
    for (const auto& clickableText : m_clickableTextRects)
    {
        // Color is assigned by clickable ID and it's possible for 
        // multiple clickable text rects to share the same ID, so guard
        // against unnecessary calls to set the color for IDs that have
        // previously been set.
        // We also don't want to set the text to the link color if it is currently being hovered.
        if (lastClickableId != clickableText.id && clickableText.id != hoverClickableId)
        {
            EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkColor);
            lastClickableId = clickableText.id;
        }
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////
void UiMarkupButtonComponent::OnLinkHoverColorChanged()
{
    // If  a link is being hovered (e.g. if SetLinkColor called at runtime while a link is being hovered)
    // then we want to set the color of that link to the new hover color
    int hoverClickableId = -1;
    if (m_clickableRectHoverIndex >= 0)
    {
        hoverClickableId = m_clickableTextRects[m_clickableRectHoverIndex].id;
    }

    // Set any hovered links to the new link hover color
    int lastClickableId = -1;
    for (const auto& clickableText : m_clickableTextRects)
    {
        // Color is assigned by clickable ID and it's possible for 
        // multiple clickable text rects to share the same ID, so guard
        // against unnecessary calls to set the color for IDs that have
        // previously been set.
        if (lastClickableId != clickableText.id)
        {
            // If it is currently being hovered then set its color to the new link hover color
            if (clickableText.id == hoverClickableId)
            {
                EBUS_EVENT_ID(GetEntityId(), UiClickableTextBus, SetClickableTextColor, clickableText.id, m_linkHoverColor);
            }

            lastClickableId = clickableText.id;
        }
    }
}

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

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

    if (serializeContext)
    {
        serializeContext->Class<UiMarkupButtonComponent, UiInteractableComponent>()
            ->Version(1, &VersionConverter)
            ->Field("LinkColor", &UiMarkupButtonComponent::m_linkColor)
            ->Field("LinkHoverColor", &UiMarkupButtonComponent::m_linkHoverColor);

        AZ::EditContext* ec = serializeContext->GetEditContext();
        if (ec)
        {
            auto editInfo = ec->Class<UiMarkupButtonComponent>("MarkupButton", "An interactable component for enabling clicks from markup text (mouse support only).");

            editInfo->ClassElement(AZ::Edit::ClassElements::EditorData, "")
                ->Attribute(AZ::Edit::Attributes::Category, "UI")
                // Need to request markup button component icons for LY ML
                ->Attribute(AZ::Edit::Attributes::Icon, "Editor/Icons/Components/UiMarkupButton.png")
                ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Editor/Icons/Components/Viewport/UiMarkupButton.png")
                ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC("UI", 0x27ff46b0))
                ->Attribute(AZ::Edit::Attributes::AutoExpand, true);

            editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiMarkupButtonComponent::m_linkColor, "Link Color", "Link text color.")
                ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiMarkupButtonComponent::OnLinkColorChanged);

            editInfo->DataElement(AZ::Edit::UIHandlers::Color, &UiMarkupButtonComponent::m_linkHoverColor, "Link Hover Color", "Link text hover color.")
                ->Attribute(AZ::Edit::Attributes::ChangeNotify, &UiMarkupButtonComponent::OnLinkHoverColorChanged);
        }
    }

    AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
    if (behaviorContext)
    {
        behaviorContext->EBus<UiMarkupButtonBus>("UiMarkupButtonBus")
            ->Event("GetLinkColor", &UiMarkupButtonBus::Events::GetLinkColor)
            ->Event("SetLinkColor", &UiMarkupButtonBus::Events::SetLinkColor)
            ->Event("GetLinkHoverColor", &UiMarkupButtonBus::Events::GetLinkHoverColor)
            ->Event("SetLinkHoverColor", &UiMarkupButtonBus::Events::SetLinkHoverColor);

        behaviorContext->EBus<UiMarkupButtonNotificationsBus>("UiMarkupButtonNotificationsBus")
            ->Handler<UiMarkupButtonNotificationBusBehaviorHandler>();
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
// PRIVATE STATIC MEMBER FUNCTIONS
////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////
bool UiMarkupButtonComponent::VersionConverter(AZ::SerializeContext& context,
    AZ::SerializeContext::DataElementNode& classElement)
{
    return true;
}

#include "Tests/internal/test_UiMarkupButtonComponent.cpp"