/*
* 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.
*
*/
#pragma once

#include <AzCore/Component/ComponentBus.h>
#include <AzCore/Math/MathUtils.h>
#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Matrix4x4.h>

////////////////////////////////////////////////////////////////////////////////////////////////////
// The UiTransformInterface provides an abstract bus interface for UI components that define the
// position and size of a UI element.
class UiTransformInterface
    : public AZ::ComponentBus
{
public: // types

    //! Struct that stores the 4 points of a (transformed) rectangle and provides access either as an array or via access functions
    struct RectPoints
    {
        enum Corner     // can be used as an index into RectPoints pt array but access via member functions preferred
        {
            Corner_TopLeft,
            Corner_TopRight,
            Corner_BottomRight,
            Corner_BottomLeft,

            Corner_Count
        };

        AZ::Vector2    pt[Corner_Count];  // in clockwise order: top left, top right, bottom right, bottom left

        RectPoints();
        RectPoints(float left, float right, float top, float bottom);

        AZ::Vector2& TopLeft();
        const AZ::Vector2& TopLeft() const;

        AZ::Vector2& TopRight();
        const AZ::Vector2& TopRight() const;

        AZ::Vector2& BottomRight();
        const AZ::Vector2& BottomRight() const;

        AZ::Vector2& BottomLeft();
        const AZ::Vector2& BottomLeft() const;

        AZ::Vector2 GetCenter() const;

        AZ::Vector2 GetAxisAlignedSize() const;
        AZ::Vector2 GetAxisAlignedTopLeft() const;
        AZ::Vector2 GetAxisAlignedTopRight() const;
        AZ::Vector2 GetAxisAlignedBottomRight() const;
        AZ::Vector2 GetAxisAlignedBottomLeft() const;
        void SetAxisAligned(float left, float right, float top, float bottom);

        RectPoints Transform(AZ::Matrix4x4& transform) const;
        RectPoints operator-(const RectPoints& rhs) const;
    };

    using RectPointsArray = AZStd::vector < UiTransformInterface::RectPoints >;

    //! Struct that stores the bounds of an axis-aligned rectangle
    struct Rect
    {
        float left;
        float right;
        float top;
        float bottom;

        void Set(float l, float r, float t, float b);

        float GetWidth() const;
        float GetHeight() const;

        float GetCenterX();
        float GetCenterY();

        AZ::Vector2 GetSize() const;
        AZ::Vector2 GetCenter();

        void MoveBy(AZ::Vector2 offset);

        RectPoints GetPoints();

        bool operator==(const Rect& rhs) const;

        bool operator!=(const Rect& rhs) const;
    };

    //! Enum used as a parameter to SetRecomputeFlags
    enum class Recompute
    {
        RectOnly,               //!< Only the rect (offsets or anchors for example) changed (this may affect transform if local scale or rotation)
        TransformOnly,          //!< Only the transform changed (canvas and viewport transforms must be recomputed)
        ViewportTransformOnly,  //!< Only the viewport transform changed (viewport transform must be recomputed)
        RectAndTransform        //!< Both rect and transform changed (all cached data must be recomputed)
    };

    //! Enum used as a parameter to SetScaleToDeviceMode
    //! The value determines how an element is scaled when the canvas reference size and actual size are different
    //! The comments below reference the canvas's "deviceScale". The deviceScale is target (actual) canvas size divided the reference canvas size
    enum class ScaleToDeviceMode
    {
        None,               //!< Default, this element is not affected by device resolution changes
        UniformScaleToFit,  //!< Apply a uniform scale which is the minimum of deviceScale.x and deviceScale.y
        UniformScaleToFill, //!< Apply a uniform scale which is the maximum of deviceScale.x and deviceScale.y
        UniformScaleToFitX, //!< Apply a uniform scale of deviceScale.x
        UniformScaleToFitY, //!< Apply a uniform scale of deviceScale.y
        NonUniformScale,    //!< Apply a non-uniform scale which is simply deviceScale
        ScaleXOnly,         //!< Scale the element only in the X dimension by deviceScale.x
        ScaleYOnly          //!< Scale the element only in the Y dimension by deviceScale.y
    };

public: // member functions

    virtual ~UiTransformInterface() {}

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get and set the properties of the transform component
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the rotation about the Z axis
    virtual float GetZRotation() = 0;

    //! Set the rotation about the Z axis
    virtual void SetZRotation(float rotation) = 0;

    //! Get the scale
    virtual AZ::Vector2 GetScale() = 0;

    //! Set the scale
    virtual void SetScale(AZ::Vector2 scale) = 0;

    //! Get the scale X
    virtual float GetScaleX() = 0;

    //! Set the scale X
    virtual void SetScaleX(float scale) = 0;

    //! Get the scale Y
    virtual float GetScaleY() = 0;

    //! Set the scale Y
    virtual void SetScaleY(float scale) = 0;

    //! Get the pivot point for the element
    virtual AZ::Vector2 GetPivot() = 0;

    //! Set the pivot point for the element
    virtual void SetPivot(AZ::Vector2 pivot) = 0;

    //! Get the pivot point for the element
    virtual float GetPivotX() = 0;

    //! Set the pivot point for the element
    virtual void SetPivotX(float pivot) = 0;

    //! Get the pivot point for the element
    virtual float GetPivotY() = 0;

    //! Set the pivot point for the element
    virtual void SetPivotY(float pivot) = 0;

    //! Get how the element and all its children will be scaled to allow for the difference
    //! between the authored canvas size and the actual viewport size.
    virtual ScaleToDeviceMode GetScaleToDeviceMode() = 0;

    //! Set how the element and all its children will be scaled to allow for the difference
    //! between the authored canvas size and the actual viewport size.
    virtual void SetScaleToDeviceMode(ScaleToDeviceMode scaleToDeviceMode) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get data in viewport space
    //
    // Viewport space is a 1-1 mapping to whatever viewport the UI canvas is being rendered to.
    // A position in viewport space is an offset in pixels from the top left of the viewport.
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the four points defining the rectangle of this element, in viewport space pixel coords
    virtual void GetViewportSpacePoints(RectPoints& points) = 0;

    //! Get the pivot for this element in viewport space
    virtual AZ::Vector2 GetViewportSpacePivot() = 0;

    //! Get the transform matrix to transform from canvas (noScaleRotate) space to viewport space
    virtual void GetTransformToViewport(AZ::Matrix4x4& mat) = 0;

    //! Get the transform matrix to transform from viewport space to canvas (noScaleRotate) space
    virtual void GetTransformFromViewport(AZ::Matrix4x4& mat) = 0;

    //! Apply the "to viewport" transform to the given points
    virtual void RotateAndScalePoints(RectPoints& points) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get data in canvas space
    //
    // Often canvas space and viewport space are the same thing. For example if a canvas is being
    // displayed full screen.
    // However, in other cases they are different. For example in the UI editor when Zoom and Pan allow
    // the canvas to be moved around in the viewport. In that case an offset of 1 does not mean 1 pixel
    // in the viewport. Canvas space is defined by the canvas size stored in the canvas.
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the four points defining the rectangle of this element, in canvas space coords
    virtual void GetCanvasSpacePoints(RectPoints& points) = 0;

    //! Get the pivot for this element in canvas space
    virtual AZ::Vector2 GetCanvasSpacePivot() = 0;

    //! Get the transform matrix to transform from canvasNoScaleRotate space to canvas space
    virtual void GetTransformToCanvasSpace(AZ::Matrix4x4& mat) = 0;
    
    //! Get the transform matrix to transform from canvas space to canvasNoScaleRotate space
    virtual void GetTransformFromCanvasSpace(AZ::Matrix4x4& mat) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get data in unrotated and unscaled canvas space (CanvasNoScaleRotate space)
    //
    // CanvasNoScaleRotate space is like canvas space but without any of the rotation or scaling in
    // any of the elments applied. So if none of the elements in the canvas have scale or rotation
    // the two are the same.
    //
    // CanvasNoScaleRotate space is like local space for an element but it does include all of the
    // parent's anchor and offset calculations so it is not really local space.
    //
    // This is a useful space to do calculations in because all elements are axis aligned and their
    // rectangle can be represented by a Rect struct rather requiring a RectPoints struct.
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the axis-aligned rect for the element without accounting for rotation or scale
    virtual void GetCanvasSpaceRectNoScaleRotate(Rect& rect) = 0;

    //! Get the rect points for the element without accounting for rotation or scale
    virtual void GetCanvasSpacePointsNoScaleRotate(RectPoints& points) = 0;

    //! Get the size for the element without accounting for rotation or scale
    virtual AZ::Vector2 GetCanvasSpaceSizeNoScaleRotate() = 0;

    //! Get the pivot for the element without accounting for rotation or scale
    virtual AZ::Vector2 GetCanvasSpacePivotNoScaleRotate() = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get data in/about local space
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the transform matrix to apply this element's rotation and scale about pivot
    virtual void GetLocalTransform(AZ::Matrix4x4& mat) = 0;

    //! Get the transform matrix to apply the inverse of this element's rotation and scale about pivot
    virtual void GetLocalInverseTransform(AZ::Matrix4x4& mat) = 0;

    //! Test whether this transform component has any scale or rotation
    virtual bool HasScaleOrRotation() = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Methods to get/set the element's position
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get the position for this element in viewport space (same as GetViewportSpacePivot)
    virtual AZ::Vector2 GetViewportPosition() = 0;

    //! Set the position for this element in viewport space
    virtual void SetViewportPosition(const AZ::Vector2& position) = 0;

    //! Get the position for this element in canvas space (same as GetCanvasSpacePivot)
    virtual AZ::Vector2 GetCanvasPosition() = 0;

    //! Set the position for this element in canvas space
    virtual void SetCanvasPosition(const AZ::Vector2& position) = 0;

    //! Get the position for this element relative to the center of the element's anchors
    virtual AZ::Vector2 GetLocalPosition() = 0;

    //! Set the position for this element relative to the center of the element's anchors
    virtual void SetLocalPosition(const AZ::Vector2& position) = 0;

    //! Get the X position for this element relative to the center of the element's anchors
    virtual float GetLocalPositionX() = 0;

    //! Set the X position for this element relative to the center of the element's anchors
    virtual void SetLocalPositionX(float position) = 0;

    //! Get the Y position for this element relative to the center of the element's anchors
    virtual float GetLocalPositionY() = 0;

    //! Set the Y position for this element relative to the center of the element's anchors
    virtual void SetLocalPositionY(float position) = 0;

    //! Move this element in viewport space
    virtual void MoveViewportPositionBy(const AZ::Vector2& offset) = 0;

    //! Move this element in canvas space
    virtual void MoveCanvasPositionBy(const AZ::Vector2& offset) = 0;

    //! Move this element relative to the center of the element's anchors
    virtual void MoveLocalPositionBy(const AZ::Vector2& offset) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Query functions
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Test if the given point (in viewport space) is in the rectangle of this element
    virtual bool IsPointInRect(AZ::Vector2 point) = 0;

    //! Test if the given rect (in viewport space) is in the rectangle of this element
    virtual bool BoundsAreOverlappingRect(const AZ::Vector2& bound0, const AZ::Vector2& bound1) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Optimization and caching
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Set the required dirty flags for the cached transforms and rect on this element and all its children
    virtual void SetRecomputeFlags(Recompute recompute) = 0;

    ////////////////////////////////////////////////////////////////////////////////////////////////////
    // Canvas space rect change
    ////////////////////////////////////////////////////////////////////////////////////////////////////

    //! Get whether the canvas space rect has changed since the last call to NotifyAndResetCanvasSpaceRectChange.
    //! May trigger a recompute of the rect if the recompute flag is dirty
    virtual bool HasCanvasSpaceRectChanged() = 0;

    //! Get whether the canvas space size has changed since the last call to NotifyAndResetCanvasSpaceRectChange.
    //! May trigger a recompute of the rect if the recompute flag is dirty
    virtual bool HasCanvasSpaceSizeChanged() = 0;

    //! Get whether the canvas space rect was changed due to initialization
    virtual bool HasCanvasSpaceRectChangedByInitialization() = 0;

    //! Send notification of canvas space rect change and reset to unchanged
    virtual void NotifyAndResetCanvasSpaceRectChange() = 0;

public: // static member data

    //! Only one component on a entity can implement the events
    static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single;
};

typedef AZ::EBus<UiTransformInterface> UiTransformBus;

////////////////////////////////////////////////////////////////////////////////////////////////////
//! Interface class that listeners need to implement
class UiTransformChangeNotification
    : public AZ::ComponentBus
{
public: // member functions

    virtual ~UiTransformChangeNotification(){}

    //! Called when an entity's transform (canvas space) has been modified
    virtual void OnCanvasSpaceRectChanged(AZ::EntityId entityId, const UiTransformInterface::Rect& oldRect, const UiTransformInterface::Rect& newRect) = 0;

    //! Called when an entity's transform (viewport space) has been modified
    virtual void OnTransformToViewportChanged() {}
};

typedef AZ::EBus<UiTransformChangeNotification> UiTransformChangeNotificationBus;


////////////////////////////////////////////////////////////////////////////////////////////////////
// RectPoints inline methods
////////////////////////////////////////////////////////////////////////////////////////////////////

inline UiTransformInterface::RectPoints::RectPoints()
{
}

inline UiTransformInterface::RectPoints::RectPoints(float left, float right, float top, float bottom)
{
    SetAxisAligned(left, right, top, bottom);
}

inline AZ::Vector2& UiTransformInterface::RectPoints::TopLeft()
{
    return pt[Corner_TopLeft];
}

inline const AZ::Vector2& UiTransformInterface::RectPoints::TopLeft() const
{
    return pt[Corner_TopLeft];
}

inline AZ::Vector2& UiTransformInterface::RectPoints::TopRight()
{
    return pt[Corner_TopRight];
}

inline const AZ::Vector2& UiTransformInterface::RectPoints::TopRight() const
{
    return pt[Corner_TopRight];
}

inline AZ::Vector2& UiTransformInterface::RectPoints::BottomRight()
{
    return pt[Corner_BottomRight];
}

inline const AZ::Vector2& UiTransformInterface::RectPoints::BottomRight() const
{
    return pt[Corner_BottomRight];
}

inline AZ::Vector2& UiTransformInterface::RectPoints::BottomLeft()
{
    return pt[Corner_BottomLeft];
}

inline const AZ::Vector2& UiTransformInterface::RectPoints::BottomLeft() const
{
    return pt[Corner_BottomLeft];
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetCenter() const
{
    return ((GetAxisAlignedTopLeft() + GetAxisAlignedBottomRight()) * 0.5f);
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetAxisAlignedSize() const
{
    return AZ::Vector2(BottomRight().GetX() - TopLeft().GetX(), BottomRight().GetY() - TopLeft().GetY());
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetAxisAlignedTopLeft() const
{
    return AZ::Vector2(min(min(min(TopLeft().GetX(), TopRight().GetX()), BottomRight().GetX()), BottomLeft().GetX()),
        min(min(min(TopLeft().GetY(), TopRight().GetY()), BottomRight().GetY()), BottomLeft().GetY()));
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetAxisAlignedTopRight() const
{
    return AZ::Vector2(max(max(max(TopLeft().GetX(), TopRight().GetX()), BottomRight().GetX()), BottomLeft().GetX()),
        min(min(min(TopLeft().GetY(), TopRight().GetY()), BottomRight().GetY()), BottomLeft().GetY()));
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetAxisAlignedBottomRight() const
{
    return AZ::Vector2(max(max(max(TopLeft().GetX(), TopRight().GetX()), BottomRight().GetX()), BottomLeft().GetX()),
        max(max(max(TopLeft().GetY(), TopRight().GetY()), BottomRight().GetY()), BottomLeft().GetY()));
}

inline AZ::Vector2 UiTransformInterface::RectPoints::GetAxisAlignedBottomLeft() const
{
    return AZ::Vector2(min(min(min(TopLeft().GetX(), TopRight().GetX()), BottomRight().GetX()), BottomLeft().GetX()),
        max(max(max(TopLeft().GetY(), TopRight().GetY()), BottomRight().GetY()), BottomLeft().GetY()));
}

inline void UiTransformInterface::RectPoints::SetAxisAligned(float left, float right, float top, float bottom)
{
    pt[Corner_TopLeft] = AZ::Vector2(left, top);
    pt[Corner_TopRight] = AZ::Vector2(right, top);
    pt[Corner_BottomRight] = AZ::Vector2(right, bottom);
    pt[Corner_BottomLeft] = AZ::Vector2(left, bottom);
}

inline UiTransformInterface::RectPoints UiTransformInterface::RectPoints::Transform(AZ::Matrix4x4& transform) const
{
    RectPoints result;
    for (int i = 0; i < Corner_Count; ++i)
    {
        AZ::Vector3 point3(pt[i].GetX(), pt[i].GetY(), 0.0f);
        point3 = transform * point3;
        result.pt[i] = AZ::Vector2(point3.GetX(), point3.GetY());
    }
    return result;
}

inline UiTransformInterface::RectPoints UiTransformInterface::RectPoints::operator-(const RectPoints& rhs) const
{
    RectPoints result(*this);
    for (int i = 0; i < Corner_Count; ++i)
    {
        result.pt[i] -= rhs.pt[i];
    }
    return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////
// Rect inline methods
////////////////////////////////////////////////////////////////////////////////////////////////////

inline void UiTransformInterface::Rect::Set(float l, float r, float t, float b)
{
    left = l;
    right = r;
    top = t;
    bottom = b;
}

inline float UiTransformInterface::Rect::GetWidth() const
{
    return right - left;
}

inline float UiTransformInterface::Rect::GetHeight() const
{
    return bottom - top;
}

inline float UiTransformInterface::Rect::GetCenterX()
{
    return (left + right) * 0.5f;
}

inline float UiTransformInterface::Rect::GetCenterY()
{
    return (top + bottom) * 0.5f;
}

inline AZ::Vector2 UiTransformInterface::Rect::GetSize() const
{
    return AZ::Vector2(GetWidth(), GetHeight());
}

inline AZ::Vector2 UiTransformInterface::Rect::GetCenter()
{
    return AZ::Vector2(GetCenterX(), GetCenterY());
}

inline void UiTransformInterface::Rect::MoveBy(AZ::Vector2 offset)
{
    left += offset.GetX();
    right += offset.GetX();
    top += offset.GetY();
    bottom += offset.GetY();
}

inline UiTransformInterface::RectPoints UiTransformInterface::Rect::GetPoints()
{
    return UiTransformInterface::RectPoints(left, right, top, bottom);
}

inline bool UiTransformInterface::Rect::operator==(const Rect& rhs) const
{
    return left == rhs.left
            && right == rhs.right
            && top == rhs.top
            && bottom == rhs.bottom;
}

inline bool UiTransformInterface::Rect::operator!=(const Rect& rhs) const
{
    return !(*this == rhs);
}