/*
* 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.
*
*/
// Original file Copyright Crytek GMBH or its affiliates, used under license.

#ifndef CRYINCLUDE_CRY3DENGINE_PARTICLE_H
#define CRYINCLUDE_CRY3DENGINE_PARTICLE_H
#pragma once

#include "IParticles.h"
#include "ParticleEffect.h"
#include "ParticleMemory.h"
#include "ParticleEnviron.h"
#include "ParticleUtils.h"
#include "CREParticle.h"

#if !PARTICLES_USE_CRY_PHYSICS
#include <CryPhysicsDeprecation.h>
#include <AzFramework/Physics/WorldBodyBus.h>
#endif

class CParticleContainer;
class CParticleSubEmitter;
class CParticleEmitter;
struct SParticleRenderData;
struct SParticleVertexContext;
struct SParticleUpdateContext;
struct SLocalRenderVertices;
struct STargetForces;

//////////////////////////////////////////////////////////////////////////
#define fMAX_COLLIDE_DEVIATION              0.1f
#define fMAX_DENSITY_ADJUST                     32.f

// dynamic particle data
// To do opt: subclass for geom particles.

//////////////////////////////////////////////////////////////////////////
struct STimeState
{
protected:
    float           m_fAge;                                     // Current age.
    float           m_fStopAge;                             // Age of death.
    float           m_fCollideAge;                      // Age of first collision (for SpawnOnCollision children).

public:
    STimeState()
        : m_fAge(0.f)
        , m_fStopAge(0.f)
        , m_fCollideAge(fHUGE) {}

    float GetAge() const
    { return m_fAge; }
    float GetStopAge() const
    { return m_fStopAge; }
    float GetCollideAge() const
    { return m_fCollideAge; }

    // should be called GetNormalizedAge() since it returns 0-1
    float GetRelativeAge(float fAgeAdjust = 0.f) const
    {
        float fRelativeAge = div_min(max(m_fAge + fAgeAdjust, 0.f), m_fStopAge, 1.f);

        // [LY-112058] Clamp to 1.f in case (m_fAge > m_fStopAge) due to a long frame step
        fRelativeAge = min(fRelativeAge, 1.f);

        assert(fRelativeAge >= 0.f && fRelativeAge <= 1.f);
        return fRelativeAge;
    }
    bool IsAlive(float fAgeAdjust = 0.f) const
    { return m_fAge + fAgeAdjust < m_fStopAge; }

    void Start(float fAgeAdjust = 0.f)
    { m_fAge = -fAgeAdjust; m_fStopAge = m_fCollideAge = fHUGE; }
    void Stop(float fAgeAdjust = 0.f)
    { m_fStopAge = min(m_fStopAge, m_fAge + fAgeAdjust); }
    void Collide(float fAgeAdjust = 0.f)
    { m_fCollideAge = m_fAge + fAgeAdjust; }
    void Kill()
    { m_fStopAge = -fHUGE; }
};

struct SMoveState
{
protected:
    QuatTS      m_Loc;                                      // Position, orientation, and size.
    float       m_fAngle;                                   // Scalar angle, for camera-facing rotation.
    Velocity3   m_Vel;                                      // Linear and rotational velocity.

public:
    SMoveState()
        : m_Loc(IDENTITY)
        , m_Vel(ZERO) {}

    QuatTS const& GetLocation() const
    { return m_Loc; }
    Velocity3 const& GetVelocity() const
    { return m_Vel; }
    Vec3 GetVelocityAt(Vec3 const& vWorldPos) const
    { return m_Vel.VelocityAt(vWorldPos - m_Loc.t); }

    void PreTransform(QuatTS const& qp)
    {
        m_Loc = m_Loc * qp;
        m_Vel.vLin = m_Vel.vLin * qp.q * qp.s;
        m_Vel.vRot = m_Vel.vRot * qp.q;
    }
    void Transform(QuatTS const& qp)
    {
        m_Loc = qp * m_Loc;
        m_Vel.vLin = qp.q * m_Vel.vLin * qp.s;
        m_Vel.vRot = qp.q * m_Vel.vRot;
    }
    void OffsetPosition(const Vec3& delta)
    {
        m_Loc.t += delta;
    }
};

    
struct SParticleState
    : STimeState
    , SMoveState
{
    friend class CParticle;
};

//////////////////////////////////////////////////////////////////////////
class CParticleSource
    : public Cry3DEngineBase
    , public _plain_reference_target<int>
    , public SParticleState
    , public SEmitGeom
{
public:

    CParticleSource()
        : m_pEmitter(0)
    {}

    const CParticleSubEmitter* GetEmitter() const
    { return m_pEmitter; }
    const SEmitGeom& GetEmitGeom() const
    { return *this; }

    using _plain_reference_target<int>::AddRef;
    using _plain_reference_target<int>::Release;

protected:

    CParticleSubEmitter*    m_pEmitter;                         // Parent emitter, if this is a child emitter.
} _ALIGN(16);

//////////////////////////////////////////////////////////////////////////

#if !PARTICLES_USE_CRY_PHYSICS
enum class CParticleCollision : uint8
{
    NONE = 0,
    SELF = 1,
    OTHER = 2,
    BOTH = SELF | OTHER
};

inline CParticleCollision& operator|=(CParticleCollision& self, CParticleCollision rhs)
{
    self = static_cast<CParticleCollision>(static_cast<uint8>(self) | static_cast<uint8>(rhs));
    return self;
}
#endif

class CParticle
    : public CParticleSource
{
public:

    CParticle() = default;
    CParticle(const CParticle& other);
    ~CParticle();

    void Init(SParticleUpdateContext const& context, float fAge, CParticleSubEmitter* pEmitter, const EmitParticleData& data);
    void Update(SParticleUpdateContext const& context, float fUpdateTime, bool bNew = false);
    void GetPhysicsState();

    float GetMinDist(Vec3 const& vP) const
    {
        static const float fSizeFactor = 1.0f;
        return (m_Loc.t - vP).GetLengthFast() - m_Loc.s * fSizeFactor;
    }

    float GetAlphaMod() const
    {
        return GetParams().fAlpha.GetValueFromBase(m_BaseMods.Alpha, GetRelativeAge(), m_BaseMods.ColorLerp);
    }
    float GetRandomColorLerp() const
    {
        return m_BaseMods.ColorLerp;
    }

    ILINE uint16 GetEmitterSequence() const
    {
        return m_nEmitterSequence;
    }

    void Hide()
    {
        // Disable rendering.
        m_BaseMods.Alpha = 0;
    }

    void UpdateBounds(AABB& bb, SParticleState const& state) const;
    void UpdateBounds(AABB& bb) const
    {
        UpdateBounds(bb, *this);
    }
    void OffsetPosition(const Vec3& delta);

#if !PARTICLES_USE_CRY_PHYSICS
    void OnCollided(AZ::EntityId collidedWith);
#endif

    // Associated structures.
    CParticleContainer& GetContainer() const
    { return *m_pContainer; }
    CParticleSource& GetSource() const;
    CParticleEmitter& GetMain() const;
    ResourceParticleParams const& GetParams() const;

    // Rendering functions.
    bool RenderGeometry(SRendParams& RenParamsShared, SParticleVertexContext& context, const SRenderingPassInfo& passInfo) const;
    void AddLight(const SRendParams& RenParams, const SRenderingPassInfo& passInfo) const;
    void SetVertices(SLocalRenderVertices& alloc, SParticleVertexContext& context, uint8 uAlpha) const;
    void GetTextureRect(RectF& rectTex, Vec3& vTexBlend) const;
    void ComputeRenderData(SParticleRenderData& RenderData, const SParticleVertexContext& context, float fObjectSize = 1.f) const;
    float ComputeRenderAlpha(const SParticleRenderData& RenderData, float fRelativeAge, SParticleVertexContext& context) const;
    void GetRenderMatrix(Vec3& vX, Vec3& vY, Vec3& vZ, Vec3& vT, const QuatTS& loc, const SParticleRenderData& RenderData, const SParticleVertexContext& context) const;
    void GetRenderMatrix(Matrix34& mat, const QuatTS& loc, const SParticleRenderData& RenderData, const SParticleVertexContext& context) const
    {
        Vec3 av[4];
        GetRenderMatrix(av[0], av[1], av[2], av[3], loc, RenderData, context);
        mat.SetFromVectors(av[0], av[1], av[2], av[3]);
    }
    void SetVertexLocation(SVF_Particle& Vert, const QuatTS& loc, const SParticleRenderData& RenderData, const SParticleVertexContext& context) const
    {
        Vec3 vZ;
        GetRenderMatrix(Vert.xaxis, vZ, Vert.yaxis, Vert.xyz, loc, RenderData, context);
    }
    
#ifdef PARTICLE_EDITOR_FUNCTIONS
    void UpdateAllocations(int nPrevHistorySteps);
#endif
    static size_t GetAllocationSize(const CParticleContainer* pCont);
    void GetMemoryUsage(ICrySizer* pSizer) const { /*nothing*/ }

    //this allows us to spawn a group of particles without adding additional overhead caused by other methods.
    void SetBeamInfo(unsigned int segmentCount, Vec3 segmentStep, const float uvScale, bool isEdgeParticle = false);
    bool IsSegmentEdge() const;
    inline void CreateBeamVertices(SLocalRenderVertices& alloc, SParticleVertexContext& context, SVF_Particle& baseVert) const;
    inline void InitBeam(SLocalRenderVertices& alloc, SParticleVertexContext& context, SVF_Particle& baseVert) const;
    inline void AddSegmentToBeam(SLocalRenderVertices& alloc, SParticleVertexContext& context, SVF_Particle& baseVert) const;
    inline void FinishBeam(SLocalRenderVertices& alloc, SParticleVertexContext& context, SVF_Particle& baseVert) const;

    inline void GatherVertexData(SParticleVertexContext& Context, uint8 uAlpha, SParticleRenderData& renderData, SVF_Particle& baseVert) const;
    inline Vec2 CalculateConnectedTextureCoords(SParticleVertexContext& context) const;

private:

    //////////////////////////////////////////////////////////////////////////
    // For particles with tail, keeps history of previous locations.
    struct SParticleHistory
    {
        float       fAge;
        QuatTS  Loc;

        bool IsUsed() const     { return fAge >= 0.f; }
        void SetUnused()            { fAge = -1.f; }
    } _ALIGN(16);

    // Track sliding state.
    struct SSlideInfo
    {
#if PARTICLES_USE_CRY_PHYSICS
        int                     physicalEntityId;   // Physical entity hit.
#else // AZPhysics
        AZ::EntityId            physicalEntityId;   // Physical entity hit.
#endif
        Vec3                    vNormal;            // Normal of sliding surface.
        float                   fFriction;          // Sliding friction, proportional to normal force.
        float                   fSlidingTime;       // Cumulative amount of time sliding.

        SSlideInfo()
        {
            Clear();
        }
        void Clear()
        {
            ClearSliding(Vec3(ZERO));
        }
        void ClearSliding(const Vec3& vNormal_)
        {
#if PARTICLES_USE_CRY_PHYSICS
            physicalEntityId = -1; // Defaulting to -1 to fallback to terrain collision (see GetPhysicalEntityById)
#else // AZPhysics
            physicalEntityId.SetInvalid(); // Defaulting to invalid to fallback to terrain collision (see GetPhysicalEntityById)
#endif
            fFriction = 0;
            fSlidingTime = -1.f;
            vNormal = vNormal_;
        }
        void SetSliding(CryParticleHitEntity* entitySlidingAgainst, const Vec3& vNormal_, float fFriction_)
        {
#if PARTICLES_USE_CRY_PHYSICS
            physicalEntityId = gEnv->pPhysicalWorld->GetPhysicalEntityId(entitySlidingAgainst);
#else // AZPhysics
            physicalEntityId = entitySlidingAgainst ? entitySlidingAgainst->GetEntityId() : AZ::EntityId();
#endif
            vNormal = vNormal_;
            fFriction = fFriction_;
            fSlidingTime = 0.f;
        }

        bool IsSliding() const
        {
            return fSlidingTime >= 0.f;
        }

        CryParticleHitEntity* GetPhysicalEntity() const
        {
#if PARTICLES_USE_CRY_PHYSICS
            return gEnv->pPhysicalWorld->GetPhysicalEntityById(physicalEntityId);
#else // AZPhysics
            return SPhysEnviron::GetPhysicalEntityFromEntityId(physicalEntityId);
#endif // PARTICLES_USE_CRY_PHYSICS
        }
    };

    // Track predicted collisions.
    struct SHitInfo
    {
        Vec3                vPos;                   // Hit position.
        Vec3                vPathDir;               // Direction of reverse path.
        float               fPathLength;            // Length of reverse path.
        Vec3                vNormal;                // Normal of hit surface.
#if PARTICLES_USE_CRY_PHYSICS
        int                 physicalEntityId;       // Physical entity hit.
        int                 nSurfaceIdx;            // Surface index of hit; -1 if no hit.
#else // AZPhysics
        AZ::EntityId        physicalEntityId;       // Physical entity hit.
        Physics::Material*        material;       // Material hit; nullptr if no material.
#endif

        SHitInfo()
        {
            Clear();
        }
        void Clear()
        {
            fPathLength = 0.f;
#if PARTICLES_USE_CRY_PHYSICS
            nSurfaceIdx = -1;
            physicalEntityId = -1; // Defaulting to -1 to fallback to terrain collision (see GetPhysicalEntityById)
#else // AZPhysics
            material = nullptr;
            physicalEntityId.SetInvalid(); // Defaulting to invalid to fallback to terrain collision (see GetPhysicalEntityById)
#endif
        }

        bool HasPath() const
        {
            return fPathLength > 0.f;
        }
        bool HasHit() const
        {
#if PARTICLES_USE_CRY_PHYSICS
            return nSurfaceIdx >= 0;
#else
            return material != nullptr;
#endif
        }
        void SetMiss(const Vec3& vStart_, const Vec3& vEnd_)
        {
            SetHit(vStart_, vEnd_, Vec3(0.f));
        }
#if PARTICLES_USE_CRY_PHYSICS
        void SetHit(const Vec3& vStart_, const Vec3& vEnd_, const Vec3& vNormal_, int nSurfaceIdx_ = -1, CryParticleHitEntity* pEntity_ = 0)
#else
        void SetHit(const Vec3& vStart_, const Vec3& vEnd_, const Vec3& vNormal_, Physics::Material* material_ = nullptr, CryParticleHitEntity* pEntity_ = 0)
#endif
        {
            vPos = vEnd_;
            vPathDir = vStart_ - vEnd_;
            fPathLength = vPathDir.GetLength();
            if (fPathLength > 0.f)
            {
                vPathDir /= fPathLength;
            }
            vNormal = vNormal_;
#if PARTICLES_USE_CRY_PHYSICS
            nSurfaceIdx = nSurfaceIdx_;
            physicalEntityId = gEnv->pPhysicalWorld->GetPhysicalEntityId(pEntity_);
#else // AZPhysics
            material = material_;
            physicalEntityId = pEntity_ ? pEntity_->GetEntityId() : AZ::EntityId();
#endif
        }

        // If path invalid, returns false.
        // If path valid returns true; if hit.dist < 1, then hit was matched.
        bool TestHit(CryParticleRayHit& hit, const Vec3& vPos0, const Vec3& vPos1, const Vec3& vVel0, const Vec3& vVel1, float fMaxDev, float fRadius = 0.f) const;

        CryParticleHitEntity* GetPhysicalEntity() const
        {
#if PARTICLES_USE_CRY_PHYSICS
            return gEnv->pPhysicalWorld->GetPhysicalEntityById(physicalEntityId);
#else // AZPhysics
            return SPhysEnviron::GetPhysicalEntityFromEntityId(physicalEntityId);
#endif
        }
    };

    struct SCollisionInfo
    {
        SSlideInfo  Sliding;
        SHitInfo        Hit;
        int32           nCollisionLeft;                     // Number of collisions this particle is allowed to have: max = unlimited; 0 = no collide, -1 = stop

        SCollisionInfo(int32 nMaxCollisions = 0)
            : nCollisionLeft(nMaxCollisions ? nMaxCollisions : 0x7FFFFFFF) {}

        void Clear()
        {
            Sliding.Clear();
            Hit.Clear();
        }

        int32 Collide()
        { return --nCollisionLeft; }
        void Stop()
        { nCollisionLeft = -1; }
        bool CanCollide() const
        { return nCollisionLeft > 0; }
        bool Stopped() const
        { return nCollisionLeft < 0; }
    };

    // Constant values.

    SParticleHistory*           m_aPosHistory;                      // History of positions, for tail. Allocated and maintained by particle.
    SCollisionInfo*             m_pCollisionInfo;                   // Predicted collision info.
    // Base modifications (random variation, emitter strength) for this particle of variable parameters.
    // Stored as compressed fraction from 0..1.
    struct SBaseMods
    {
        // Random modifiers for unsigned params.
        TFixed<uint8, 1>
            SizeX,
            SizeY,
            SizeZ,
            StretchOrTail,  // Used for either stretch or tail (exclusive features)

            AirResistance,
            Turbulence3DSpeed,
            TurbulenceSize,

            LightSourceIntensity,
            LightSourceRadius,
            Alpha;

        // Random modifiers for signed params.
        TFixed<int8, 1>
            PivotX,
            PivotY,
            PivotZ,
            GravityScale,
            TurbulenceSpeed,
            fTargetRadius,
            RotateRateX,
            RotateRateY,
            RotateRateZ,
            ColorLerp;

        Color3B Color;

        void Init()
        {
            memset(this, 0xFF, sizeof(*this));
        }
    } m_BaseMods;

    uint8                                   m_nTileVariant;                     // Selects texture tile.

    uint16                              m_nEmitterSequence : 15,     // Which sequence particle is part of (for connected rendering).
                                        m_bHorizontalFlippedTexture : 1,            // Reverse texture U.
                                        m_bVerticalFlippedTexture : 1;          // Reverse texture V.

    //! particle index in a connected particle sequence such as Trail or Beam
    uint16 m_indexInSequence;
            
    Vec3 m_originalEmitterLocation;

    // External references.
    CParticleContainer*     m_pContainer;           // Container particle lives in.

    // cache initial emitter orientation.  Could be the whole SParticleState if more stuff needs to be cached
    Quat_tpl<f32>           m_initialOrientation;
    // m_segmentCount is the number of particles that belong to a "burst" of particles.
    // this count is tracked so that different systems can use it to handle proper calculations, for instance cutting "tails" of particles in a line to create a beam.
    uint16 m_segmentCount;
    float m_initialEmissionStrength;
    Vec3 m_preEmissionRandomVelocity;
    float m_UVOffset; //this is 1 for all cases except when particle is a beam edge, at which point it will scale the UV to match the segmentStep
    TSmallBool m_isSegmentEdge; //used to mark the beginning and end of a beam segment
    Vec3 m_segmentStep; //vector from this particle to the next one.
    // Functions.

    //size scale from emitter's spawn parameter. save it in init since it has random element
    Vec3 m_sizeScale;

#if !PARTICLES_USE_CRY_PHYSICS
    CParticleCollision m_collided;
    bool m_physicsActive = false;
#endif // PARTICLES_USE_CRY_PHYSICS
private:

    void SetState(SParticleState const& state)          { static_cast<SParticleState&>(*this) = state; }
    float GetBaseRadius() const                                         { return GetParams().GetMaxObjectSize(GetStatObj()); }
    float GetVisibleRadius() const                                  { assert(m_Loc.s >= 0.f); return m_Loc.s * GetBaseRadius(); }
    float GetPhysicalRadius() const                                 { return GetVisibleRadius() * GetParams().fThickness; }
    inline Vec3 GetNormal() const                                       { return m_Loc.q.GetColumn1(); }

    void InitPos(SParticleUpdateContext const& context, QuatTS const& loc, float fEmissionRelAge);

    //! Returns an estimated world-space velocity of the final particle movement after
    //! all updates have been applied. The estimated velocity is from the current state
    //! to the nextState.
    //! \param nextState The presumed next/target state of the Particle
    //! \param timeStep Number of seconds between the current state and the nextState.
    Vec3 GetVisualVelocity(SParticleState const& nextState, float stepTime) const;

    void AddPosHistory(SParticleState const& stateNew);
    void AlignTo(SParticleState& state, const Vec3& vNormal, float fTime) const;
    float UpdateAlignment(SParticleState& state, SParticleUpdateContext const& context, Plane const& plWater, float fStepTime = 0.f) const;
    Vec3 VortexRotation(SParticleState const& state, bool bVelocity, float fTime = 0.f) const;
    void TargetMovement(ParticleTarget const& target, SParticleState& state, float fTime, float fRelativeAge) const;
    float TravelSlide(SParticleState& state, SSlideInfo& sliding, float fTime, const Vec3& vExtAccel, float fMaxSlide, float fMinStepTime) const;
    void Move(SParticleState& state, float fTime, STargetForces const& forces) const;
    float MoveLinear(SParticleState& state, SCollisionInfo& coll, float fTime, STargetForces const& forces, float fMaxLinearDev, float fMaxSlideDev, float fMinStepTime) const;
    bool CheckCollision(CryParticleRayHit& hit, float fTime, SParticleUpdateContext const& context, const STargetForces& forces, const SParticleState& stateNew, SCollisionInfo& collNew);

    void Physicalize();
#if PARTICLES_USE_CRY_PHYSICS
    int GetSurfaceIndex() const;
    void GetCollisionParams(int nCollSurfaceIdx, float& fElasticity, float& fDrag) const;
#else
    void GetCollisionParams(const Physics::Material* material, float& fElasticity, float& fDrag) const;
#endif

    void ApplyCameraNonFacingFade(const SParticleVertexContext& context, SVF_Particle& vertex) const;

    void SetTailVertices(const SVF_Particle& BaseVert, SParticleRenderData RenderData, SLocalRenderVertices& alloc, SParticleVertexContext const& context) const;

    void DebugBounds(SParticleState const& state) const
#if defined(_DEBUG)
    ;
#else
    {}
#endif
} _ALIGN(32);

#endif // CRYINCLUDE_CRY3DENGINE_PARTICLE_H