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

#pragma once

#include "Engine/Core/Common.h"
#include "Engine/Core/Math/BoundingSphere.h"
#include "Engine/Core/Math/BoundingBox.h"
#include "Engine/Core/Math/VectorInt.h"
#include "Engine/Serialization/Stream.h"
#include "Engine/Graphics/Enums.h"
#include "Types.h"
#include "Config.h"
#include "SkeletonData.h"
#include "BlendShape.h"
#include "Engine/Animations/AnimationData.h"

/// <summary>
/// Data container for the common model meshes data. Supports holding all types of data related to the models pipeline.
/// </summary>
class FLAXENGINE_API MeshData
{
public:

    /// <summary>
    /// The slot index in the model materials to use during rendering.
    /// </summary>
    int32 MaterialSlotIndex = 0;

    /// <summary>
    /// The model skeleton node index. Used during importing and by the animated models.
    /// </summary>
    int32 NodeIndex = 0;

    /// <summary>
    /// The name of the mesh.
    /// </summary>
    String Name;

    /// <summary>
    /// Mesh positions buffer
    /// </summary>
    Array<Vector3> Positions;

    /// <summary>
    /// Texture coordinates
    /// </summary>
    Array<Vector2> UVs;

    /// <summary>
    /// Normals vector
    /// </summary>
    Array<Vector3> Normals;

    /// <summary>
    /// Tangents vectors
    /// </summary>
    Array<Vector3> Tangents;

    /// <summary>
    /// Mesh index buffer
    /// </summary>
    Array<uint32> Indices;

    /// <summary>
    /// Lightmap UVs
    /// </summary>
    Array<Vector2> LightmapUVs;

    /// <summary>
    /// Vertex colors
    /// </summary>
    Array<Color> Colors;

    /// <summary>
    /// Skinned mesh blend indices (max 4 per bone)
    /// </summary>
    Array<Int4> BlendIndices;

    /// <summary>
    /// Skinned mesh index buffer (max 4 per bone)
    /// </summary>
    Array<Vector4> BlendWeights;

    /// <summary>
    /// Blend shapes used by this mesh
    /// </summary>
    Array<BlendShape> BlendShapes;

public:

    /// <summary>
    /// Determines whether this instance has any mesh data.
    /// </summary>
    FORCE_INLINE bool HasData() const
    {
        return Indices.HasItems();
    }

public:

    /// <summary>
    /// Clear arrays
    /// </summary>
    void Clear();

    /// <summary>
    /// Ensure that buffers will have given space for data
    /// </summary>
    /// <param name="vertices">Amount of vertices.</param>
    /// <param name="indices">Amount of indices.</param>
    /// <param name="preserveContents">Failed if clear data otherwise will try to preserve the buffers contents.</param>
    /// <param name="withColors">True if use vertex colors buffer.</param>
    /// <param name="withSkin">True if use vertex blend indices and weights buffer.</param>
    void EnsureCapacity(int32 vertices, int32 indices, bool preserveContents = false, bool withColors = true, bool withSkin = true);

    /// <summary>
    /// Swaps the vertex and index buffers contents (without a data copy) with the other mesh.
    /// </summary>
    /// <param name="other">The other mesh to swap data with.</param>
    void SwapBuffers(MeshData& other);

    /// <summary>
    /// Clean data
    /// </summary>
    void Release();

public:

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vertices">Array of vertices</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(ModelVertex19* vertices, uint32 verticesCount);

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vertices">Array of vertices</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(ModelVertex18* vertices, uint32 verticesCount);

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vertices">Array of vertices</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(ModelVertex15* vertices, uint32 verticesCount);

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vb0">Array of data for vertex buffer 0</param>
    /// <param name="vb1">Array of data for vertex buffer 1</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(VB0ElementType18* vb0, VB1ElementType18* vb1, uint32 verticesCount);

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vb0">Array of data for vertex buffer 0</param>
    /// <param name="vb1">Array of data for vertex buffer 1</param>
    /// <param name="vb2">Array of data for vertex buffer 2</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(VB0ElementType18* vb0, VB1ElementType18* vb1, VB2ElementType18* vb2, uint32 verticesCount);

    /// <summary>
    /// Init from model vertices array
    /// </summary>
    /// <param name="vb0">Array of data for vertex buffer 0</param>
    /// <param name="vb1">Array of data for vertex buffer 1</param>
    /// <param name="verticesCount">Amount of vertices</param>
    void InitFromModelVertices(VB0ElementType15* vb0, VB1ElementType15* vb1, uint32 verticesCount);

    /// <summary>
    /// Sets the index buffer data.
    /// </summary>
    /// <param name="data">The data.</param>
    /// <param name="indicesCount">The indices count.</param>
    void SetIndexBuffer(void* data, uint32 indicesCount);

public:

    /// <summary>
    /// Pack mesh data to the stream
    /// </summary>
    /// <param name="stream">Output stream</param>
    /// <returns>True if cannot save data, otherwise false</returns>
    bool Pack2Model(WriteStream* stream) const;

    /// <summary>
    /// Pack skinned mesh data to the stream
    /// </summary>
    /// <param name="stream">Output stream</param>
    /// <returns>True if cannot save data, otherwise false</returns>
    bool Pack2SkinnedModel(WriteStream* stream) const;

    /// <summary>
    /// Calculate bounding box for the mesh
    /// </summary>
    /// <param name="result">Output box</param>
    void CalculateBox(BoundingBox& result) const
    {
        if (Positions.HasItems())
            BoundingBox::FromPoints(Positions.Get(), Positions.Count(), result);
    }

    /// <summary>
    /// Calculate bounding sphere for the mesh
    /// </summary>
    /// <param name="result">Output sphere</param>
    void CalculateSphere(BoundingSphere& result) const
    {
        if (Positions.HasItems())
            BoundingSphere::FromPoints(Positions.Get(), Positions.Count(), result);
    }

public:

#if COMPILE_WITH_MODEL_TOOL

    /// <summary>
    /// Generate lightmap uvs for the mesh entry
    /// </summary>
    /// <returns>True if generating lightmap uvs failed, otherwise false</returns>
    bool GenerateLightmapUVs();

    /// <summary>
    /// Iterates over the vertex buffers to remove the duplicated vertices and generate the optimized index buffer.
    /// </summary>
    void BuildIndexBuffer();

    /// <summary>
    /// Generate lightmap uvs for the mesh entry
    /// </summary>
    /// <param name="position">The target position to check.</param>
    /// <param name="epsilon">The position comparision epsilon.</param>
    /// <param name="result">The output vertices indices array.</param>
    void FindPositions(const Vector3& position, float epsilon, Array<int32>& result);

    /// <summary>
    /// Generates the normal vectors for the mesh geometry.
    /// </summary>
    /// <param name="smoothingAngle">Specifies the maximum angle (in degrees) that may be between two face normals at the same vertex position that their are smoothed together.</param>
    /// <returns>True if generating failed, otherwise false</returns>
    bool GenerateNormals(float smoothingAngle = 175.0f);

    /// <summary>
    /// Generates the tangent vectors for the mesh geometry. Requires normal vector and texture coords channel to be valid.
    /// </summary>
    /// <param name="smoothingAngle">Specifies the maximum angle (in degrees) that may be between two vertex tangents that their tangents and bi-tangents are smoothed.</param>
    /// <returns>True if generating failed, otherwise false.</returns>
    bool GenerateTangents(float smoothingAngle = 45.0f);

    /// <summary>
    /// Reorders all triangles for improved vertex cache locality. It tries to arrange all triangles to fans and to render triangles which share vertices directly one after the other.
    /// </summary>
    void ImproveCacheLocality();

    /// <summary>
    /// Sums the area of all triangles in the mesh.
    /// </summary>
    /// <returns>The area sum of all mesh triangles.</returns>
    float CalculateTrianglesArea() const;

#endif

    /// <summary>
    /// Transform a vertex buffer positions, normals, tangents and bitangents using the given matrix.
    /// </summary>
    /// <param name="matrix">The matrix to use for the transformation.</param>
    void TransformBuffer(const Matrix& matrix);

    /// <summary>
    /// Normalizes the blend weights. Requires to have vertices with positions and blend weights setup.
    /// </summary>
    void NormalizeBlendWeights()
    {
        ASSERT(Positions.Count() == BlendWeights.Count());
        for (int32 i = 0; i < Positions.Count(); i++)
        {
            const float sum = BlendWeights[i].SumValues();
            const float invSum = sum > ZeroTolerance ? 1.0f / sum : 0.0f;
            BlendWeights[i] *= invSum;
        }
    }

    /// <summary>
    /// Merges this mesh data with the specified other mesh.
    /// </summary>
    /// <param name="other">The other mesh to merge with.</param>
    void Merge(MeshData& other);
};

/// <summary>
/// Model texture resource descriptor.
/// </summary>
struct FLAXENGINE_API TextureEntry
{
    enum class TypeHint
    {
        ColorRGB,
        ColorRGBA,
        Normals,
    };

    /// <summary>
    /// The absolute path to the file.
    /// </summary>
    String FilePath;

    /// <summary>
    /// The texture contents hint based on the usage/context.
    /// </summary>
    TypeHint Type;

    /// <summary>
    /// The texture asset identifier.
    /// </summary>
    Guid AssetID;
};

/// <summary>
/// Model material slot entry that describes model mesh appearance.
/// </summary>
struct FLAXENGINE_API MaterialSlotEntry
{
    /// <summary>
    /// The slot name.
    /// </summary>
    String Name;

    /// <summary>
    /// Gets or sets shadows casting mode by this visual element
    /// </summary>
    ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All;

    /// <summary>
    /// The material asset identifier (material or material instance).
    /// </summary>
    Guid AssetID;

    struct
    {
        Color Color = Color::White;
        int32 TextureIndex = -1;
        bool HasAlphaMask = false;
    } Diffuse;

    struct
    {
        Color Color = Color::Transparent;
        int32 TextureIndex = -1;
    } Emissive;

    struct
    {
        float Value = 1.0f;
        int32 TextureIndex = -1;
    } Opacity;

    struct
    {
        int32 TextureIndex = -1;
    } Normals;

    bool TwoSided = false;

    bool UsesProperties() const
    {
        return Diffuse.Color != Color::White ||
                Diffuse.TextureIndex != -1 ||
                Emissive.Color != Color::Transparent ||
                Emissive.TextureIndex != -1 ||
                !Math::IsOne(Opacity.Value) ||
                Opacity.TextureIndex != -1 ||
                Normals.TextureIndex != -1;
    }
};

/// <summary>
/// Data container for LOD metadata and sub meshes.
/// </summary>
class FLAXENGINE_API ModelLodData
{
public:

    /// <summary>
    /// The screen size to switch LODs. Bottom limit of the model screen size to render this LOD.
    /// </summary>
    float ScreenSize = 1.0f;

    /// <summary>
    /// The meshes array.
    /// </summary>
    Array<MeshData*> Meshes;

public:

    /// <summary>
    /// Initializes a new instance of the <see cref="ModelLodData"/> class.
    /// </summary>
    ModelLodData()
    {
    }

    /// <summary>
    /// Finalizes an instance of the <see cref="ModelLodData"/> class.
    /// </summary>
    ~ModelLodData()
    {
        Meshes.ClearDelete();
    }
};

/// <summary>
/// Data container for model metadata and LODs.
/// </summary>
class FLAXENGINE_API ModelData
{
public:

    /// <summary>
    /// The minimum screen size to draw model (the bottom limit).
    /// </summary>
    float MinScreenSize = 0.0f;

    /// <summary>
    /// The texture slots.
    /// </summary>
    Array<TextureEntry> Textures;

    /// <summary>
    /// The material slots.
    /// </summary>
    Array<MaterialSlotEntry> Materials;

    /// <summary>
    /// Array with all LODs. The first element is the top most LOD0 followed by the LOD1, LOD2, etc.
    /// </summary>
    Array<ModelLodData> LODs;

    /// <summary>
    /// The skeleton bones hierarchy.
    /// </summary>
    SkeletonData Skeleton;

    /// <summary>
    /// The node animations.
    /// </summary>
    AnimationData Animation;

public:

    /// <summary>
    /// Initializes a new instance of the <see cref="ModelData"/> class.
    /// </summary>
    ModelData()
    {
    }

public:

    /// <summary>
    /// Gets the valid level of details count.
    /// </summary>
    /// <returns>The LOD count.</returns>
    FORCE_INLINE int32 GetLODsCount() const
    {
        return LODs.Count();
    }

    /// <summary>
    /// Determines whether this instance has valid skeleton structure.
    /// </summary>
    /// <returns>True if has skeleton, otherwise false.</returns>
    FORCE_INLINE bool HasSkeleton() const
    {
        return Skeleton.Bones.HasItems();
    }

public:

    /// <summary>
    /// Automatically calculates the screen size for every model LOD for a proper transitions.
    /// </summary>
    void CalculateLODsScreenSizes();

    /// <summary>
    /// Transform a vertex buffer positions, normals, tangents and bitangents using the given matrix. Applies to all the LODs and meshes.
    /// </summary>
    /// <param name="matrix">The matrix to use for the transformation.</param>
    void TransformBuffer(const Matrix& matrix);

public:

    /// <summary>
    /// Pack mesh data to the header stream
    /// </summary>
    /// <param name="stream">Output stream</param>
    /// <returns>True if cannot save data, otherwise false</returns>
    bool Pack2ModelHeader(WriteStream* stream) const;

    /// <summary>
    /// Pack skinned mesh data to the header stream
    /// </summary>
    /// <param name="stream">Output stream</param>
    /// <returns>True if cannot save data, otherwise false</returns>
    bool Pack2SkinnedModelHeader(WriteStream* stream) const;

    /// <summary>
    /// Pack animation data to the header stream
    /// </summary>
    /// <param name="stream">Output stream</param>
    /// <returns>True if cannot save data, otherwise false</returns>
    bool Pack2AnimationHeader(WriteStream* stream) const;
};
