/*
* 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 <LyShine/Bus/UiAnimateEntityBus.h>
#include <LyShine/Bus/UiTransformBus.h>
#include <LyShine/Bus/UiTransform2dBus.h>
#include <LyShine/Bus/UiLayoutFitterBus.h>
#include <LyShine/UiComponentTypes.h>
#include <LyShine/UiSerializeHelpers.h>

#include <AzCore/Component/Component.h>

// Only needed for internal unit-testing
#include <LyShine.h>

class UiCanvasComponent;
class UiElementComponent;

////////////////////////////////////////////////////////////////////////////////////////////////////
class UiTransform2dComponent
    : public AZ::Component
    , public UiTransformBus::Handler
    , public UiTransform2dBus::Handler
    , public UiAnimateEntityBus::Handler
{
public: // member functions

    AZ_COMPONENT(UiTransform2dComponent, LyShine::UiTransform2dComponentUuid, AZ::Component);

    UiTransform2dComponent();
    ~UiTransform2dComponent() override;

    // UiTransformInterface
    float GetZRotation() override;
    void SetZRotation(float rotation) override;
    AZ::Vector2 GetScale() override;
    void SetScale(AZ::Vector2 scale) override;
    float GetScaleX() override;
    void SetScaleX(float scale) override;
    float GetScaleY() override;
    void SetScaleY(float scale) override;
    AZ::Vector2 GetPivot() override;
    void SetPivot(AZ::Vector2 pivot) override;
    float GetPivotX() override;
    void SetPivotX(float pivot) override;
    float GetPivotY() override;
    void SetPivotY(float pivot) override;
    ScaleToDeviceMode GetScaleToDeviceMode() override;
    void SetScaleToDeviceMode(ScaleToDeviceMode scaleToDeviceMode) override;

    void GetViewportSpacePoints(RectPoints& points) final;
    AZ::Vector2 GetViewportSpacePivot() final;
    void GetTransformToViewport(AZ::Matrix4x4& mat) final;
    void GetTransformFromViewport(AZ::Matrix4x4& mat) final;
    void RotateAndScalePoints(RectPoints& points) final;

    void GetCanvasSpacePoints(RectPoints& points) final;
    AZ::Vector2 GetCanvasSpacePivot() final;
    void GetTransformToCanvasSpace(AZ::Matrix4x4& mat) final;
    void GetTransformFromCanvasSpace(AZ::Matrix4x4& mat) final;

    void GetCanvasSpaceRectNoScaleRotate(Rect& rect) final;
    void GetCanvasSpacePointsNoScaleRotate(RectPoints& points) final;
    AZ::Vector2 GetCanvasSpaceSizeNoScaleRotate() final;
    AZ::Vector2 GetCanvasSpacePivotNoScaleRotate() final;

    void GetLocalTransform(AZ::Matrix4x4& mat) final;
    void GetLocalInverseTransform(AZ::Matrix4x4& mat) final;
    bool HasScaleOrRotation() final;

    AZ::Vector2 GetViewportPosition() final;
    void SetViewportPosition(const AZ::Vector2& position) final;
    AZ::Vector2 GetCanvasPosition() final;
    void SetCanvasPosition(const AZ::Vector2& position) final;
    AZ::Vector2 GetLocalPosition() final;
    void SetLocalPosition(const AZ::Vector2& position) final;
    float GetLocalPositionX() final;
    void SetLocalPositionX(float position) final;
    float GetLocalPositionY() final;
    void SetLocalPositionY(float position) final;
    void MoveViewportPositionBy(const AZ::Vector2& offset) final;
    void MoveCanvasPositionBy(const AZ::Vector2& offset) final;
    void MoveLocalPositionBy(const AZ::Vector2& offset) final;

    bool IsPointInRect(AZ::Vector2 point) final;
    bool BoundsAreOverlappingRect(const AZ::Vector2& bound0, const AZ::Vector2& bound1) final;

    void SetRecomputeFlags(Recompute recompute) final;

    bool HasCanvasSpaceRectChanged() final;
    bool HasCanvasSpaceSizeChanged() final;
    bool HasCanvasSpaceRectChangedByInitialization() final;
    void NotifyAndResetCanvasSpaceRectChange() final;
    // ~UiTransformInterface

    // UiTransform2dInterface
    Anchors GetAnchors() override;
    void SetAnchors(Anchors anchors, bool adjustOffsets, bool allowPush) override;
    Offsets GetOffsets() override;
    void SetOffsets(Offsets offsets) override;
    void SetPivotAndAdjustOffsets(AZ::Vector2 pivot) override;
    void SetLocalWidth(float width) override;
    float GetLocalWidth() override;
    void SetLocalHeight(float height) override;
    float GetLocalHeight() override;
    // ~UiTransform2dInterface

    // UiAninmateEntityInterface
    void PropertyValuesChanged() override;
    // ~UiAninmateEntityInterface

    //! This is called from the canvas component during the update if the element was scheduled for a transform recompute
    void RecomputeTransformsAndSendNotifications();

#if defined(LYSHINE_INTERNAL_UNIT_TEST)
    static void UnitTest(CLyShine* lyshine, IConsoleCmdArgs* cmdArgs);
#endif

public:  // static member functions

    static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
    {
        provided.push_back(AZ_CRC("UiTransformService", 0x3a838e34));
    }

    static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible)
    {
        incompatible.push_back(AZ_CRC("UiTransformService", 0x3a838e34));
    }

    static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
    {
    }

    static void Reflect(AZ::ReflectContext* context);

protected: // member functions

    // AZ::Component
    void Activate() override;
    void Deactivate() override;
    // ~AZ::Component

    //! Determine whether this element's transform is being overridden by a component on its parent
    virtual bool IsControlledByParent() const;

    //! Get the level of control of a layout fitter
    virtual UiLayoutFitterInterface::FitType GetLayoutFitterType() const;

    //! Determine whether this element's transform is not being overridden by a component on its parent
    //! This just exists to be called from the edit context setup
    bool IsNotControlledByParent() const;

    //! Get the first ancestor that has a scale to device mode affecting the same dimension as this
    //! element's scale to device mode
    AZ::EntityId GetAncestorWithSameDimensionScaleToDevice(ScaleToDeviceMode scaleToDeviceMode) const;

    //! Get a list of descendants that have a scale to device mode affecting the same dimension as this
    //! element's scale to device mode
    LyShine::EntityArray GetDescendantsWithSameDimensionScaleToDevice(ScaleToDeviceMode scaleToDeviceMode) const;

    //! Return whether there are anchors that are apart affecting the same dimension as this
    //! element's scale to device mode
    bool AreAnchorsApartInSameScaleToDeviceDimension(ScaleToDeviceMode scaleToDeviceMode) const;

    //! Return a short one line string that incudes a warning for the currently assigned scale to device mode.
    //! An empty string indicates no warnings
    AZStd::string GetScaleToDeviceModeWarningText() const;

    //! Return a tooltip string describing the warning for the currently assigned scale to device mode.
    //! An empty string indicates no warnings
    AZStd::string GetScaleToDeviceModeWarningTooltipText() const;

    //! This is used to dynamically change the label for the Anchor property in the properties pane
    //! as a way to display a "disabled" stated for this component when the transform is controlled by the
    //! parent.
    const char* GetAnchorPropertyLabel() const;

    //! Helper function to get the canvas entity ID for canvas containing this element
    AZ::EntityId GetCanvasEntityId();

    //! Helper function to get the canvas component for canvas containing this element
    UiCanvasComponent* GetCanvasComponent() const;

    //! ChangeNotify function for when a transform property is changed
    void OnTransformPropertyChanged();

    //! If m_recomputeTransformToViewport is true then recompute the transform and clear the flag
    void RecomputeTransformToViewportIfNeeded();

    //! If m_recomputeTransformToCanvasSpace is true then recompute the transform and clear the flag
    void RecomputeTransformToCanvasSpaceIfNeeded();

private: // member functions

    AZ_DISABLE_COPY_MOVE(UiTransform2dComponent);

    //! Get the scale with the uniform device scale factored in, if m_scaleToDevice is true
    AZ::Vector2 GetScaleAdjustedForDevice();

    //! Calculates the rect if m_recomputeCanvasSpaceRect dirty flag is set
    void CalculateCanvasSpaceRect();

    //! Get the position of the element's anchors in canvas space
    AZ::Vector2 GetCanvasSpaceAnchorsCenterNoScaleRotate();

    // Get a pointer to this entity's UiElementComponent.
    UiElementComponent* GetElementComponent() const;

    // Get a pointer to the parent element's transform component. Returns nullptr if no parent.
    UiTransform2dComponent* GetParentTransformComponent() const;

    // Get a pointer to the given child element's transform component. Returns nullptr if no parent.
    UiTransform2dComponent* GetChildTransformComponent(int index) const;

    // Used to check that FixupPostLoad has been called
    bool IsFullyInitialized() const;

    // Display a warning that the component is not yet fully initialized
    void EmitNotInitializedWarning() const;

    // Given a scale apply the canvases device scale to it according to the m_scaleToDeviceMode setting
    void ApplyDeviceScale(AZ::Vector2& scale);

private: // static member functions

    static bool VersionConverter(AZ::SerializeContext& context,
        AZ::SerializeContext::DataElementNode& classElement);

    //! Determine whether the specified scale to device mode affects horizontal scale
    static bool DoesScaleToDeviceModeAffectX(ScaleToDeviceMode scaleToDeviceMode);

    //! Determine whether the specified scale to device mode affects vertical scale
    static bool DoesScaleToDeviceModeAffectY(ScaleToDeviceMode scaleToDeviceMode);

private: // data

    Anchors m_anchors;  // initialized by Anchors constructor
    Offsets m_offsets;  // initialized by Offsets constructor
    AZ::Vector2 m_pivot;
    float m_rotation;
    AZ::Vector2 m_scale;

    //! Cached transform to viewport space. Gets recalculated if the m_recomputeTransformToViewport dirty flag is set
    AZ::Matrix4x4 m_transformToViewport;

    //! Cached transform to canvas space. Gets recalculated if the m_recomputeTransformToCanvasSpace dirty flag is set
    AZ::Matrix4x4 m_transformToCanvasSpace;

    //! Cached rect in CanvasNoScaleRotateSpace.
    //! Gets recalculated if the m_recomputeCanvasSpaceRect dirty flag is set
    Rect m_rect;

    //! The previously cached rect in CanvasNoScaleRotateSpace.
    //! Initialized when m_rect is calculated for the first time.
    //! Updated to m_rect when a rect change notification is sent
    Rect m_prevRect;

    //! True if m_rect has been calculated
    bool m_rectInitialized;

    //! True if the rect has changed due to it being calculated for the first time. In this
    //! case m_prevRect will equal m_rect
    bool m_rectChangedByInitialization;

    //! If this is not set to None then the canvas scale is applied, in addition to m_scale, according to this mode
    ScaleToDeviceMode m_scaleToDeviceMode;

    //! If this is true, then the transform stored in m_transformToViewport is dirty and needs to be recomputed
    bool m_recomputeTransformToViewport;

    //! If this is true, then the transform stored in m_transformToCanvasSpace is dirty and needs to be recomputed
    bool m_recomputeTransformToCanvasSpace;

    bool m_recomputeCanvasSpaceRect;

    //! Pointer directly to the UiElementComponent for this entity. Cached for performance after profiling.
    UiElementComponent* m_elementComponent = nullptr;
};