// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

#include "Collider.h"
#include "Engine/Core/Log.h"
#include "Engine/Serialization/Serialization.h"
#include "Engine/Physics/Utilities.h"
#include "Engine/Physics/PhysicsSettings.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Physics/PhysicalMaterial.h"
#include "Engine/Physics/Actors/RigidBody.h"
#include <ThirdParty/PhysX/geometry/PxGeometryQuery.h>
#include <ThirdParty/PhysX/PxShape.h>
#include <ThirdParty/PhysX/PxPhysics.h>
#include <ThirdParty/PhysX/PxFiltering.h>
#include <ThirdParty/PhysX/PxRigidDynamic.h>
#include <ThirdParty/PhysX/PxRigidStatic.h>
#include <ThirdParty/PhysX/PxScene.h>

Collider::Collider(const SpawnParams& params)
    : PhysicsColliderActor(params)
    , _center(Vector3::Zero)
    , _isTrigger(false)
    , _shape(nullptr)
    , _staticActor(nullptr)
    , _cachedScale(1.0f)
    , _contactOffset(10.0f)
{
    Material.Changed.Bind<Collider, &Collider::OnMaterialChanged>(this);
}

void Collider::SetIsTrigger(bool value)
{
    if (value == _isTrigger || !CanBeTrigger())
        return;

    _isTrigger = value;

    if (_shape)
    {
        const bool isTrigger = _isTrigger && CanBeTrigger();
        const PxShapeFlags shapeFlags = GetShapeFlags(isTrigger, IsActiveInHierarchy());
        _shape->setFlags(shapeFlags);
    }
}

void Collider::SetCenter(const Vector3& value)
{
    if (Vector3::NearEqual(value, _center))
        return;

    _center = value;

    if (_staticActor)
    {
        _shape->setLocalPose(PxTransform(C2P(_center)));
    }
    else if (const RigidBody* rigidBody = GetAttachedRigidBody())
    {
        _shape->setLocalPose(PxTransform(C2P((_localTransform.Translation + _localTransform.Orientation * _center) * rigidBody->GetScale()), C2P(_localTransform.Orientation)));
    }

    UpdateBounds();
}

void Collider::SetContactOffset(float value)
{
    value = Math::Clamp(value, 0.0f, 100.0f);
    if (Math::NearEqual(value, _contactOffset))
        return;

    _contactOffset = value;

    if (_shape)
    {
        _shape->setContactOffset(Math::Max(_shape->getRestOffset() + ZeroTolerance, _contactOffset));
    }
}

bool Collider::RayCast(const Vector3& origin, const Vector3& direction, float& resultHitDistance, float maxDistance) const
{
    resultHitDistance = MAX_float;
    if (_shape == nullptr)
        return false;

    // Prepare data
    const PxTransform trans(C2P(_transform.Translation), C2P(_transform.Orientation));
    const PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eNORMAL | PxHitFlag::eFACE_INDEX | PxHitFlag::eUV;

    // Perform raycast test
    PxRaycastHit hit;
    if (PxGeometryQuery::raycast(C2P(origin), C2P(direction), _shape->getGeometry().any(), trans, maxDistance, hitFlags, 1, &hit) != 0)
    {
        resultHitDistance = hit.distance;
        return true;
    }

    return false;
}

bool Collider::RayCast(const Vector3& origin, const Vector3& direction, RayCastHit& hitInfo, float maxDistance) const
{
    if (_shape == nullptr)
        return false;

    // Prepare data
    const PxTransform trans(C2P(_transform.Translation), C2P(_transform.Orientation));
    const PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eNORMAL | PxHitFlag::eUV;
    PxRaycastHit hit;

    // Perform raycast test
    if (PxGeometryQuery::raycast(C2P(origin), C2P(direction), _shape->getGeometry().any(), trans, maxDistance, hitFlags, 1, &hit) == 0)
        return false;

    // Gather results
    hitInfo.Gather(hit);
    return true;
}

void Collider::ClosestPoint(const Vector3& position, Vector3& result) const
{
    if (_shape == nullptr)
    {
        result = Vector3::Maximum;
        return;
    }

    // Prepare data
    const PxTransform trans(C2P(_transform.Translation), C2P(_transform.Orientation));
    PxVec3 closestPoint;

    // Compute distance between a point and a geometry object
    const float distanceSqr = PxGeometryQuery::pointDistance(C2P(position), _shape->getGeometry().any(), trans, &closestPoint);
    if (distanceSqr > 0.0f)
    {
        // Use calculated point
        result = P2C(closestPoint);
    }
    else
    {
        // Fallback to the input location
        result = position;
    }
}

bool Collider::ContainsPoint(const Vector3& point) const
{
    if (_shape)
    {
        const PxTransform trans(C2P(_transform.Translation), C2P(_transform.Orientation));
        const float distanceSqr = PxGeometryQuery::pointDistance(C2P(point), _shape->getGeometry().any(), trans);
        return distanceSqr <= 0.0f;
    }
    return false;
}

bool Collider::ComputePenetration(const Collider* colliderA, const Collider* colliderB, Vector3& direction, float& distance)
{
    direction = Vector3::Zero;
    distance = 0.0f;

    CHECK_RETURN(colliderA && colliderB, false);
    const PxShape* shapeA = colliderA->GetPxShape();
    const PxShape* shapeB = colliderB->GetPxShape();
    if (!shapeA || !shapeB)
        return false;

    const PxTransform poseA(C2P(colliderA->GetPosition()), C2P(colliderA->GetOrientation()));
    const PxTransform poseB(C2P(colliderB->GetPosition()), C2P(colliderB->GetOrientation()));

    return PxGeometryQuery::computePenetration(
        C2P(direction),
        distance,
        shapeA->getGeometry().any(),
        poseA,
        shapeB->getGeometry().any(),
        poseB
    );
}

bool Collider::IsAttached() const
{
    return _shape && _shape->getActor() != nullptr;
}

RigidBody* Collider::GetAttachedRigidBody() const
{
    if (_shape && _staticActor == nullptr)
    {
        auto actor = _shape->getActor();
        if (actor && actor->is<PxRigidDynamic>())
            return static_cast<RigidBody*>(actor->userData);
    }
    return nullptr;
}

void Collider::Attach(RigidBody* rigidBody)
{
    ASSERT(CanAttach(rigidBody));

    // Remove static body if used
    if (_staticActor)
        RemoveStaticActor();

    // Create shape if missing
    if (_shape == nullptr)
        CreateShape();

    // Attach
    rigidBody->GetPhysXRigidActor()->attachShape(*_shape);
        _shape->setLocalPose(PxTransform(C2P((_localTransform.Translation + _localTransform.Orientation * _center) * rigidBody->GetScale()), C2P(_localTransform.Orientation)));
    if (rigidBody->IsDuringPlay())
        rigidBody->UpdateBounds();
}

void Collider::UpdateScale()
{
    const Vector3 scale = GetScale();
    if (Vector3::NearEqual(_cachedScale, scale))
        return;

    // Recreate shape geometry
    UpdateGeometry();
}

void Collider::UpdateLayerBits()
{
    ASSERT(_shape);

    PxFilterData filterData;

    // Own layer ID
    filterData.word0 = GetLayerMask();

    // Own layer mask
    filterData.word1 = PhysicsSettings::Instance()->LayerMasks[GetLayer()];

    _shape->setSimulationFilterData(filterData);
    _shape->setQueryFilterData(filterData);
}

void Collider::CreateShapeBase(const PxGeometry& geometry)
{
    ASSERT(_shape == nullptr);

    // Prepare
    const bool isTrigger = _isTrigger && CanBeTrigger();
    const PxShapeFlags shapeFlags = GetShapeFlags(isTrigger, IsActiveInHierarchy());
    PxMaterial* material = Physics::GetDefaultMaterial();
    if (Material && !Material->WaitForLoaded())
    {
        material = ((PhysicalMaterial*)Material->Instance)->GetPhysXMaterial();
    }

    // Create shape
    _shape = CPhysX->createShape(geometry, *material, true, shapeFlags);
    ASSERT(_shape);
    _shape->userData = this;

    // Setup properties
    _shape->setContactOffset(Math::Max(_shape->getRestOffset() + ZeroTolerance, _contactOffset));
    UpdateLayerBits();
}

void Collider::CreateStaticActor()
{
    ASSERT(_staticActor == nullptr);

    const PxTransform trans(C2P(_transform.Translation), C2P(_transform.Orientation));
    _staticActor = CPhysX->createRigidStatic(trans);
    ASSERT(_staticActor);
    _staticActor->userData = this;

    // Reset local pos of the shape and link it to the actor
    _shape->setLocalPose(PxTransform(C2P(_center)));
    _staticActor->attachShape(*_shape);

    Physics::AddActor(_staticActor);
}

void Collider::RemoveStaticActor()
{
    ASSERT(_staticActor != nullptr);

    Physics::RemoveActor(_staticActor);
    _staticActor = nullptr;
}

void Collider::OnMaterialChanged()
{
    // Update the shape material
    if (_shape)
    {
        PxMaterial* material = Physics::GetDefaultMaterial();
        if (Material)
        {
            if (!Material->WaitForLoaded())
            {
                material = ((PhysicalMaterial*)Material->Instance)->GetPhysXMaterial();
            }
        }
        _shape->setMaterials(&material, 1);
    }
}

void Collider::Serialize(SerializeStream& stream, const void* otherObj)
{
    // Base
    PhysicsColliderActor::Serialize(stream, otherObj);

    SERIALIZE_GET_OTHER_OBJ(Collider);

    SERIALIZE_MEMBER(IsTrigger, _isTrigger);
    SERIALIZE_MEMBER(Center, _center);
    SERIALIZE_MEMBER(ContactOffset, _contactOffset);
    SERIALIZE(Material);
}

void Collider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
{
    // Base
    PhysicsColliderActor::Deserialize(stream, modifier);

    DESERIALIZE_MEMBER(IsTrigger, _isTrigger);
    DESERIALIZE_MEMBER(Center, _center);
    DESERIALIZE_MEMBER(ContactOffset, _contactOffset);
    DESERIALIZE(Material);
}

void Collider::BeginPlay(SceneBeginData* data)
{
    // Check if has no shape created (it means no rigidbody requested it but also collider may be spawned at runtime)
    if (_shape == nullptr)
    {
        CreateShape();

        // Check if parent is a rigidbody
        const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
        if (rigidBody && CanAttach(rigidBody))
        {
            // Attach to the rigidbody
            Attach(rigidBody);
        }
        else
        {
            // Be a static collider
            CreateStaticActor();
        }
    }

    // Base
    PhysicsColliderActor::BeginPlay(data);
}

void Collider::EndPlay()
{
    if (_shape)
    {
        // Detach from the actor
        auto actor = _shape->getActor();
        if (actor)
            actor->detachShape(*_shape);

        // Check if was using a static actor and cleanup it
        if (_staticActor)
        {
            RemoveStaticActor();
        }

        // Release shape
        Physics::RemoveCollider(this);
        _shape->release();
        _shape = nullptr;
    }

    // Base
    PhysicsColliderActor::EndPlay();
}

void Collider::OnActiveInTreeChanged()
{
    // Base
    PhysicsColliderActor::OnActiveInTreeChanged();

    if (_shape)
    {
        const bool isTrigger = _isTrigger && CanBeTrigger();
        const PxShapeFlags shapeFlags = GetShapeFlags(isTrigger, IsActiveInHierarchy());
        _shape->setFlags(shapeFlags);

        auto rigidBody = GetAttachedRigidBody();
        if (rigidBody)
        {
            rigidBody->UpdateMass();

            // TODO: maybe wake up only if one ore more shapes attached is active?
            //if (rigidBody->GetStartAwake())
            //	rigidBody->WakeUp();
        }
    }
}

void Collider::OnParentChanged()
{
    // Base
    PhysicsColliderActor::OnParentChanged();

    // Check reparenting collider case
    if (_shape)
    {
        // Detach from the actor
        auto actor = _shape->getActor();
        if (actor)
            actor->detachShape(*_shape);

        // Check if the new parent is a rigidbody
        const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
        if (rigidBody && CanAttach(rigidBody))
        {
            // Attach to the rigidbody (will remove static actor if it's n use)
            Attach(rigidBody);
        }
        else
        {
            // Use static actor (if not created yet)
            if (_staticActor == nullptr)
                CreateStaticActor();
        }
    }
}

void Collider::OnTransformChanged()
{
    // Base
    PhysicsColliderActor::OnTransformChanged();

    if (_staticActor)
    {
        _staticActor->setGlobalPose(PxTransform(C2P(_transform.Translation), C2P(_transform.Orientation)));
    }
    else if (const RigidBody* rigidBody = GetAttachedRigidBody())
    {
        _shape->setLocalPose(PxTransform(C2P((_localTransform.Translation + _localTransform.Orientation * _center) * rigidBody->GetScale()), C2P(_localTransform.Orientation)));
    }

    UpdateScale();
    UpdateBounds();
}

void Collider::OnLayerChanged()
{
    // Base
    PhysicsColliderActor::OnLayerChanged();

    if (_shape)
        UpdateLayerBits();
}
