/*
* 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 <PhysXCharacters_precompiled.h>
#include <API/CharacterControllerCosmeticReplica.h>
#include <PhysX/SystemComponentBus.h>
#include <AzFramework/Physics/SystemBus.h>
#include <AzFramework/Physics/RigidBody.h>
#include <AzFramework/Physics/ShapeConfiguration.h>
#include <API/CharacterController.h>
#include <PhysXCharacters/NativeTypeIdentifiers.h>

namespace PhysXCharacters
{
    void CharacterControllerCosmeticReplica::Reflect(AZ::ReflectContext* context)
    {
        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serializeContext->Class<CharacterControllerCosmeticReplica>()
                ->Version(1)
                ;
        }
    }

    AZ::Quaternion CalculateOrientation(const Physics::ShapeConfiguration& shapeConfig, const AZ::Vector3& upDirection)
    {
        if (shapeConfig.GetShapeType() == Physics::ShapeType::Box || upDirection.IsZero())
        {
            return AZ::Quaternion::CreateIdentity();
        }

        return AZ::Quaternion::CreateShortestArc(AZ::Vector3::CreateAxisZ(), upDirection.GetNormalized());
    }

    float GetHeight(const Physics::ShapeConfiguration& shapeConfig)
    {
        const Physics::ShapeType& shapeType = shapeConfig.GetShapeType();
        if (shapeType == Physics::ShapeType::Box)
        {
            return static_cast<const Physics::BoxShapeConfiguration&>(shapeConfig).m_dimensions.GetZ();
        }
        if (shapeType == Physics::ShapeType::Capsule)
        {
            return static_cast<const Physics::CapsuleShapeConfiguration&>(shapeConfig).m_height;
        }

        AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Only box and capsule shapes are supported.  Shape type was %i", shapeType);
        return 0.0f;
    }

    AZStd::unique_ptr<Physics::ShapeConfiguration> GetScaledShapeConfiguration(float scale, const Physics::ShapeConfiguration& shapeConfig)
    {
        const Physics::ShapeType& shapeType = shapeConfig.GetShapeType();
        if (shapeType == Physics::ShapeType::Box)
        {
            auto scaledBoxConfig = AZStd::make_unique<Physics::BoxShapeConfiguration>();
            scaledBoxConfig->m_dimensions = scale * static_cast<const Physics::BoxShapeConfiguration&>(shapeConfig).m_dimensions;
            return AZStd::move(scaledBoxConfig);
        }
        else if (shapeType == Physics::ShapeType::Capsule)
        {
            auto scaledCapsuleConfig = AZStd::make_unique<Physics::CapsuleShapeConfiguration>();
            const auto& capsuleConfig = static_cast<const Physics::CapsuleShapeConfiguration&>(shapeConfig);
            scaledCapsuleConfig->m_height = scale * capsuleConfig.m_height;
            scaledCapsuleConfig->m_radius = scale * capsuleConfig.m_radius;
            return AZStd::move(scaledCapsuleConfig);
        }
        else
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Only box and capsule shapes are supported.  Shape type was %i", shapeType);
            return AZStd::make_unique<Physics::CapsuleShapeConfiguration>();
        }
    }

    CharacterControllerCosmeticReplica::CharacterControllerCosmeticReplica(const Physics::CharacterConfiguration& characterConfig,
        const Physics::ShapeConfiguration& shapeConfig, Physics::World& world)
    {
        const Physics::ShapeType& shapeType = shapeConfig.GetShapeType();
        if (shapeType != Physics::ShapeType::Box && shapeType != Physics::ShapeType::Capsule)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Could not create character controller replica.  "
                "Only box and capsule shapes are supported.  Shape type was %i", shapeType);
            return;
        }

        // rigid body
        Physics::RigidBodyConfiguration rigidBodyConfig;
        rigidBodyConfig.m_debugName = "Character Controller Cosmetic Replica";
        rigidBodyConfig.m_entityId = characterConfig.m_entityId;
        rigidBodyConfig.m_kinematic = true;
        rigidBodyConfig.m_orientation = CalculateOrientation(shapeConfig, characterConfig.m_upDirection);
        m_transform.SetRotationPartFromQuaternion(rigidBodyConfig.m_orientation);
        rigidBodyConfig.m_position = characterConfig.m_position;
        m_rigidBody = AZ::Interface<Physics::System>::Get()->CreateRigidBody(rigidBodyConfig);

        if (!m_rigidBody)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Failed to create replica actor.");
            return;
        }

        // up direction
        if (shapeType == Physics::ShapeType::Box || characterConfig.m_upDirection.IsZero())
        {
            m_upDirection = AZ::Vector3::CreateAxisZ();
        }
        else
        {
            m_upDirection = characterConfig.m_upDirection.GetNormalized();
        }

        // shape
        Physics::ColliderConfiguration colliderConfig;
        colliderConfig.m_collisionGroupId = characterConfig.m_collisionGroupId;
        colliderConfig.m_collisionLayer = characterConfig.m_collisionLayer;
        colliderConfig.m_materialSelection = characterConfig.m_materialSelection;
        float height = GetHeight(shapeConfig);
        colliderConfig.m_position = 0.5f * height * AZ::Vector3::CreateAxisZ();
        m_centreOffset = 0.5f * height * m_upDirection;

        // PhysX specific character config settings
        if (characterConfig.RTTI_GetType() == CharacterControllerConfiguration::RTTI_Type())
        {
            const auto& extendedConfig = static_cast<const CharacterControllerConfiguration&>(characterConfig);
            auto scaledShapeConfig = GetScaledShapeConfiguration(extendedConfig.m_scaleCoefficient, shapeConfig);
            m_shape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, *scaledShapeConfig);
            static_cast<physx::PxShape*>(m_shape->GetNativePointer())->setContactOffset(extendedConfig.m_contactOffset);
        }
        else
        {
            m_shape = AZ::Interface<Physics::System>::Get()->CreateShape(colliderConfig, shapeConfig);
        }

        m_rigidBody->AddShape(m_shape);

        // misc settings
        m_stepHeight = characterConfig.m_stepHeight;
        m_maximumSlopeAngle = characterConfig.m_maximumSlopeAngle;
        m_actorUserData = PhysX::ActorData(static_cast<physx::PxRigidActor*>(m_rigidBody->GetNativePointer()));
        m_actorUserData.SetCharacter(this);
        m_actorUserData.SetEntityId(characterConfig.m_entityId);

        world.AddBody(*m_rigidBody);
        CreateShadowBody(characterConfig, world);
    }

    CharacterControllerCosmeticReplica::~CharacterControllerCosmeticReplica()
    {
        if (m_rigidBody)
        {
            m_rigidBody->RemoveShape(m_shape);
            m_rigidBody = nullptr;
        }
        m_shape = nullptr;
    }

    void CharacterControllerCosmeticReplica::Move(const AZ::Vector3& worldPosition)
    {
        m_transform.SetPosition(worldPosition);
        if (m_rigidBody)
        {
            m_rigidBody->SetKinematicTarget(m_transform);
        }
        if (m_shadowBody)
        {
            m_shadowBody->SetKinematicTarget(m_transform);
        }
    }

    void CharacterControllerCosmeticReplica::CreateShadowBody(const Physics::CharacterConfiguration& characterConfig, Physics::World& world)
    {
        Physics::RigidBodyConfiguration rigidBodyConfig;
        rigidBodyConfig.m_kinematic = true;
        rigidBodyConfig.m_debugName = characterConfig.m_debugName + " (Shadow)";
        rigidBodyConfig.m_entityId = characterConfig.m_entityId;
        m_shadowBody = AZ::Interface<Physics::System>::Get()->CreateRigidBody(rigidBodyConfig);
        world.AddBody(*m_shadowBody);
    }

    // Physics::Character
    AZ::Vector3 CharacterControllerCosmeticReplica::GetBasePosition() const
    {
        return m_transform.GetPosition();
    }

    void CharacterControllerCosmeticReplica::SetBasePosition(const AZ::Vector3& position)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "SetBasePosition - the replica cannot be directly controlled.");
    }

    void CharacterControllerCosmeticReplica::SetRotation(const AZ::Quaternion& rotation)
    {
        if (m_shadowBody)
        {
            AZ::Transform transform = AZ::Transform::CreateFromQuaternionAndTranslation(rotation, GetBasePosition());
            m_shadowBody->SetTransform(transform);
        }
    }

    AZ::Vector3 CharacterControllerCosmeticReplica::GetCenterPosition() const
    {
        return m_transform.GetPosition() + m_centreOffset;
    }

    float CharacterControllerCosmeticReplica::GetStepHeight() const
    {
        return m_stepHeight;
    }

    void CharacterControllerCosmeticReplica::SetStepHeight(float stepHeight)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "SetStepHeight - the replica cannot be directly controlled.");
    }

    AZ::Vector3 CharacterControllerCosmeticReplica::GetUpDirection() const
    {
        return m_upDirection;
    }

    void CharacterControllerCosmeticReplica::SetUpDirection(const AZ::Vector3& upDirection)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "SetUpDirection - the replica cannot be directly controlled.");
    }

    float CharacterControllerCosmeticReplica::GetSlopeLimitDegrees() const
    {
        return m_maximumSlopeAngle;
    }

    void CharacterControllerCosmeticReplica::SetSlopeLimitDegrees(float slopeLimitDegrees)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "SetSlopeLimitDegrees - the replica cannot be directly controlled.");
    }

    AZ::Vector3 CharacterControllerCosmeticReplica::GetVelocity() const
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "GetVelocity - not yet supported.");
        return AZ::Vector3::CreateZero();
    }

    void CharacterControllerCosmeticReplica::SetCollisionLayer(const Physics::CollisionLayer& layer)
    {
        if (!m_shape)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Attempting to access null shape on character controller.");
            return;
        }

        m_shape->SetCollisionLayer(layer);
    }

    void CharacterControllerCosmeticReplica::SetCollisionGroup(const Physics::CollisionGroup& group)
    {
        if (!m_shape)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Attempting to access null shape on character controller.");
            return;
        }

        m_shape->SetCollisionGroup(group);
    }

    Physics::CollisionLayer CharacterControllerCosmeticReplica::GetCollisionLayer() const
    {
        if (!m_shape)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Attempting to access null shape on character controller.");
            return Physics::CollisionLayer::Default;
        }

        return m_shape->GetCollisionLayer();
    }

    Physics::CollisionGroup CharacterControllerCosmeticReplica::GetCollisionGroup() const
    {
        if (!m_shape)
        {
            AZ_Error("PhysX Character Controller Cosmetic Replica", false, "Attempting to access null shape on character controller.");
            return Physics::CollisionGroup::All;
        }

        return m_shape->GetCollisionGroup();
    }
    
    AZ::Crc32 CharacterControllerCosmeticReplica::GetColliderTag() const
    {
        AZ_Assert(m_shape != nullptr, "Attempting to access null shape on character controller");
        return m_shape->GetTag();
    }

    AZ::Vector3 CharacterControllerCosmeticReplica::TryRelativeMove(const AZ::Vector3& deltaPosition, float deltaTime)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "TryRelativeMove - the replica cannot be directly controlled.");
        return GetBasePosition();
    }

    void CharacterControllerCosmeticReplica::CheckSupport(const AZ::Vector3& direction, float distance,
        const Physics::CharacterSupportInfo& supportInfo)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "CheckSupport - not yet supported.");
    }

    // Physics::WorldBody
    AZ::EntityId CharacterControllerCosmeticReplica::GetEntityId() const
    {
        return m_rigidBody ? m_rigidBody->GetEntityId() : AZ::EntityId();
    }

    Physics::World* CharacterControllerCosmeticReplica::GetWorld() const
    {
        return m_rigidBody ? m_rigidBody->GetWorld() : nullptr;
    }

    AZ::Transform CharacterControllerCosmeticReplica::GetTransform() const
    {
        return AZ::Transform::CreateTranslation(GetBasePosition());
    }

    void CharacterControllerCosmeticReplica::SetTransform(const AZ::Transform& transform)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "SetTransform - the replica cannot be directly controlled.");
    }

    AZ::Vector3 CharacterControllerCosmeticReplica::GetPosition() const
    {
        return GetBasePosition();
    }

    AZ::Quaternion CharacterControllerCosmeticReplica::GetOrientation() const
    {
        return AZ::Quaternion::CreateIdentity();
    }

    AZ::Aabb CharacterControllerCosmeticReplica::GetAabb() const
    {
        return m_rigidBody ? m_rigidBody->GetAabb() : AZ::Aabb::CreateNull();
    }

    Physics::RayCastHit CharacterControllerCosmeticReplica::RayCast(const Physics::RayCastRequest& request)
    {
        AZ_WarningOnce("PhysX Character Controller Cosmetic Replica", false, "RayCast - not yet supported.");
        return Physics::RayCastHit();
    }

    AZ::Crc32 CharacterControllerCosmeticReplica::GetNativeType() const
    {
        return NativeTypeIdentifiers::CharacterControllerReplica;
    }

    void* CharacterControllerCosmeticReplica::GetNativePointer() const
    {
        if (m_rigidBody)
        {
            return m_rigidBody->GetNativePointer();
        }

        return nullptr;
    }

    void CharacterControllerCosmeticReplica::AddToWorld(Physics::World& world)
    {
        if (m_rigidBody)
        {
            m_rigidBody->AddToWorld(world);
        }

        if (m_shadowBody)
        {
            m_shadowBody->AddToWorld(world);
        }
    }

    void CharacterControllerCosmeticReplica::RemoveFromWorld(Physics::World& world)
    {
        if (m_rigidBody)
        {
            m_rigidBody->RemoveFromWorld(world);
        }

        if (m_shadowBody)
        {
            m_shadowBody->RemoveFromWorld(world);
        }
    }

    void CharacterControllerCosmeticReplica::AttachShape(AZStd::shared_ptr<Physics::Shape> shape)
    {
        if (m_shadowBody)
        {
            m_shadowBody->AddShape(shape);
        }
    }
} // namespace PhysXCharacters