/**
 * Copyright (c) 2014-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

#ifndef __RAWMODEL_H__
#define __RAWMODEL_H__

#include <core/mathfu.h>
#include <unordered_map>
#include <functional>
#include <memory>
#include <set>

/**
 * The variuos situations in which the user may wish for us to (re-)compute normals for our vertices.
 */
enum class ComputeNormalsOption {
    NEVER,      // do not ever compute any normals (results in broken glTF for some sources)
    BROKEN,     // replace zero-length normals in any mesh that has a normal layer
    MISSING,    // if a mesh lacks normals, compute them all
    ALWAYS      // compute a new normal for every vertex, obliterating whatever may have been there before
};

enum class UseLongIndicesOptions {
    NEVER,      // only ever use 16-bit indices
    AUTO,       // use shorts or longs depending on vertex count
    ALWAYS,     // only ever use 32-bit indices
};

/**
 * User-supplied options that dictate the nature of the glTF being generated.
 */
struct GltfOptions
{
    /**
     * If negative, disabled. Otherwise, a bitfield of RawVertexAttributes that
     * specify the largest set of attributes that'll ever be kept for a vertex.
     * The special bit RAW_VERTEX_ATTRIBUTE_AUTO triggers smart mode, where the
     * attributes to keep are inferred from which textures are supplied.
     */
    int  keepAttribs { -1 };
    /** Whether to output a .glb file, the binary format of glTF. */
    bool outputBinary { false };
    /** If non-binary, whether to inline all resources, for a single (large) .glTF file. */
    bool embedResources { false };

    /** Whether and how to use KHR_draco_mesh_compression to minimize static geometry size. */
    struct {
        bool enabled = false;
        int compressionLevel = -1;
        int quantBitsPosition = -1;
        int quantBitsTexCoord = -1;
        int quantBitsNormal = -1;
        int quantBitsColor = -1;
        int quantBitsGeneric = -1;
    } draco;

    /** Whether to use KHR_materials_unlit to extend materials definitions. */
    bool useKHRMatUnlit { false };
    /** Whether to populate the pbrMetallicRoughness substruct in materials. */
    bool usePBRMetRough { false };
    /** Whether to include blend shape normals, if present according to the SDK. */
    bool useBlendShapeNormals { false };
    /** Whether to include blend shape tangents, if present according to the SDK. */
    bool useBlendShapeTangents { false };
    /** When to compute vertex normals from geometry. */
    ComputeNormalsOption computeNormals = ComputeNormalsOption::BROKEN;
    /** When to use 32-bit indices. */
    UseLongIndicesOptions useLongIndices = UseLongIndicesOptions::AUTO;
};

enum RawVertexAttribute
{
    RAW_VERTEX_ATTRIBUTE_POSITION      = 1 << 0,
    RAW_VERTEX_ATTRIBUTE_NORMAL        = 1 << 1,
    RAW_VERTEX_ATTRIBUTE_TANGENT       = 1 << 2,
    RAW_VERTEX_ATTRIBUTE_BINORMAL      = 1 << 3,
    RAW_VERTEX_ATTRIBUTE_COLOR         = 1 << 4,
    RAW_VERTEX_ATTRIBUTE_UV0           = 1 << 5,
    RAW_VERTEX_ATTRIBUTE_UV1           = 1 << 6,
    RAW_VERTEX_ATTRIBUTE_JOINT_INDICES = 1 << 7,
    RAW_VERTEX_ATTRIBUTE_JOINT_WEIGHTS = 1 << 8,

    RAW_VERTEX_ATTRIBUTE_AUTO = 1 << 31
};

struct RawBlendVertex
{
    Vec3f position {};
    Vec3f normal {};
    Vec4f tangent {};

    bool operator==(const RawBlendVertex &other) const {
        return position == other.position &&
               normal == other.normal &&
               tangent == other.tangent;
    }
};

struct RawVertex
{
    RawVertex() :
        polarityUv0(false),
        pad1(false),
        pad2(false),
        pad3(false) {}

    Vec3f position = VEC3F_ZERO;
    Vec3f normal = VEC3F_ZERO;
    Vec3f binormal = VEC3F_ZERO;
    Vec4f tangent = VEC4F_ZERO;
    Vec4f color = VEC4F_ZERO;
	Vec2f uv0 = VEC2F_ZERO;
	Vec2f uv1 = VEC2F_ZERO;
    Vec4i jointIndices = Vec4i( 0, 0, 0, 0 );
    Vec4f jointWeights = VEC4F_ZERO;
    // end of members that directly correspond to vertex attributes

    // if this vertex participates in a blend shape setup, the surfaceIx of its dedicated mesh; otherwise, -1
    int blendSurfaceIx = -1;
    // the size of this vector is always identical to the size of the corresponding RawSurface.blendChannels
    std::vector<RawBlendVertex> blends { };

    bool polarityUv0;
    bool pad1;
    bool pad2;
    bool pad3;

    bool operator==(const RawVertex &other) const;
    size_t Difference(const RawVertex &other) const;
};

class VertexHasher
{
public:
    size_t operator()(const RawVertex &v) const
    {
        size_t seed = 5381;
        const auto hasher = std::hash<float>{};
        seed ^= hasher(v.position[0]) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        seed ^= hasher(v.position[1]) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        seed ^= hasher(v.position[2]) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        return seed;
    }
};

struct RawTriangle
{
    int verts[3];
    int materialIndex;
    int surfaceIndex;
};

enum RawShadingModel
{
    RAW_SHADING_MODEL_UNKNOWN = -1,
    RAW_SHADING_MODEL_CONSTANT,
    RAW_SHADING_MODEL_LAMBERT,
    RAW_SHADING_MODEL_BLINN,
    RAW_SHADING_MODEL_PHONG,
    RAW_SHADING_MODEL_PBR_MET_ROUGH,
    RAW_SHADING_MODEL_MAX
};

static inline std::string Describe(RawShadingModel model) {
    switch(model) {
        case RAW_SHADING_MODEL_UNKNOWN:         return "<unknown>";
        case RAW_SHADING_MODEL_CONSTANT:        return "Constant";
        case RAW_SHADING_MODEL_LAMBERT:         return "Lambert";
        case RAW_SHADING_MODEL_BLINN:           return "Blinn";
        case RAW_SHADING_MODEL_PHONG:           return "Phong";
        case RAW_SHADING_MODEL_PBR_MET_ROUGH:   return "Metallic/Roughness";
        case RAW_SHADING_MODEL_MAX: default:    return "<unknown>";
    }
}

enum RawTextureUsage
{
    RAW_TEXTURE_USAGE_NONE = -1,
    RAW_TEXTURE_USAGE_AMBIENT,
    RAW_TEXTURE_USAGE_DIFFUSE,
    RAW_TEXTURE_USAGE_NORMAL,
    RAW_TEXTURE_USAGE_SPECULAR,
    RAW_TEXTURE_USAGE_SHININESS,
    RAW_TEXTURE_USAGE_EMISSIVE,
    RAW_TEXTURE_USAGE_REFLECTION,
    RAW_TEXTURE_USAGE_ALBEDO,
    RAW_TEXTURE_USAGE_OCCLUSION,
    RAW_TEXTURE_USAGE_ROUGHNESS,
    RAW_TEXTURE_USAGE_METALLIC,
    RAW_TEXTURE_USAGE_MAX
};

static inline std::string Describe(RawTextureUsage usage)
{
    switch (usage) {
        case RAW_TEXTURE_USAGE_NONE:        return "<none>";
        case RAW_TEXTURE_USAGE_AMBIENT:     return "ambient";
        case RAW_TEXTURE_USAGE_DIFFUSE:     return "diffuse";
        case RAW_TEXTURE_USAGE_NORMAL:      return "normal";
        case RAW_TEXTURE_USAGE_SPECULAR:    return "specular";
        case RAW_TEXTURE_USAGE_SHININESS:   return "shininess";
        case RAW_TEXTURE_USAGE_EMISSIVE:    return "emissive";
        case RAW_TEXTURE_USAGE_REFLECTION:  return "reflection";
        case RAW_TEXTURE_USAGE_OCCLUSION:   return "occlusion";
        case RAW_TEXTURE_USAGE_ROUGHNESS:   return "roughness";
        case RAW_TEXTURE_USAGE_METALLIC:    return "metallic";
        case RAW_TEXTURE_USAGE_MAX:default: return "unknown";
    }
};

enum RawTextureOcclusion
{
    RAW_TEXTURE_OCCLUSION_OPAQUE,
    RAW_TEXTURE_OCCLUSION_TRANSPARENT
};

struct RawTexture
{
    std::string         name;           // logical name in FBX file
    int                 width;
    int                 height;
    int                 mipLevels;
    RawTextureUsage     usage;
    RawTextureOcclusion occlusion;
    std::string         fileName;       // original filename in FBX file
    std::string         fileLocation;   // inferred path in local filesystem, or ""
};

enum RawMaterialType
{
    RAW_MATERIAL_TYPE_OPAQUE,
    RAW_MATERIAL_TYPE_TRANSPARENT,
    RAW_MATERIAL_TYPE_SKINNED_OPAQUE,
    RAW_MATERIAL_TYPE_SKINNED_TRANSPARENT,
};

struct RawMatProps {
    explicit RawMatProps(RawShadingModel shadingModel)
        : shadingModel(shadingModel)
    {}
    const RawShadingModel shadingModel;

    virtual bool operator!=(const RawMatProps &other) const { return !(*this == other); }
    virtual bool operator==(const RawMatProps &other) const { return shadingModel == other.shadingModel; };
};

struct RawTraditionalMatProps : RawMatProps {
    RawTraditionalMatProps(
        RawShadingModel shadingModel,
        const Vec3f &&ambientFactor,
        const Vec4f &&diffuseFactor,
        const Vec3f &&emissiveFactor,
        const Vec3f &&specularFactor,
        const float shininess
    ) : RawMatProps(shadingModel),
          ambientFactor(ambientFactor),
          diffuseFactor(diffuseFactor),
          emissiveFactor(emissiveFactor),
          specularFactor(specularFactor),
          shininess(shininess)
    {}

    const Vec3f ambientFactor;
    const Vec4f diffuseFactor;
    const Vec3f emissiveFactor;
    const Vec3f specularFactor;
    const float shininess;

    bool operator==(const RawMatProps &other) const override {
        if (RawMatProps::operator==(other)) {
            const auto &typed = (RawTraditionalMatProps &) other;
            return ambientFactor == typed.ambientFactor &&
                diffuseFactor == typed.diffuseFactor &&
                specularFactor == typed.specularFactor &&
                emissiveFactor == typed.emissiveFactor &&
                shininess == typed.shininess;
        }
        return false;
    }
};

struct RawMetRoughMatProps : RawMatProps {
    RawMetRoughMatProps(
        RawShadingModel shadingModel,
        const Vec4f &&diffuseFactor,
        const Vec3f &&emissiveFactor,
        float emissiveIntensity,
        float metallic,
        float roughness
    ) : RawMatProps(shadingModel),
      diffuseFactor(diffuseFactor),
      emissiveFactor(emissiveFactor),
      emissiveIntensity(emissiveIntensity),
      metallic(metallic),
      roughness(roughness)
    {}
    const Vec4f diffuseFactor;
    const Vec3f emissiveFactor;
    const float emissiveIntensity;
    const float metallic;
    const float roughness;

    bool operator==(const RawMatProps &other) const override {
        if (RawMatProps::operator==(other)) {
            const auto &typed = (RawMetRoughMatProps &) other;
            return diffuseFactor == typed.diffuseFactor &&
            emissiveFactor == typed.emissiveFactor &&
            emissiveIntensity == typed.emissiveIntensity &&
            metallic == typed.metallic &&
            roughness == typed.roughness;
        }
        return false;
    }
};

struct RawMaterial
{
    std::string                  name;
    RawMaterialType              type;
    std::shared_ptr<RawMatProps> info;
    int                          textures[RAW_TEXTURE_USAGE_MAX];
};

struct RawBlendChannel
{
    float defaultDeform;
    bool hasNormals;
    bool hasTangents;
};

struct RawSurface
{
    long                         id;
    std::string                  name;                            // The name of this surface
    long                         skeletonRootId;                  // The id of the root node of the skeleton.
    Bounds<float, 3>             bounds;
    std::vector<long>            jointIds;
    std::vector<Vec3f>           jointGeometryMins;
    std::vector<Vec3f>           jointGeometryMaxs;
    std::vector<Mat4f>           inverseBindMatrices;
    std::vector<RawBlendChannel> blendChannels;
    bool                         discrete;
};

struct RawChannel
{
    int                nodeIndex;
    std::vector<Vec3f> translations;
    std::vector<Quatf> rotations;
    std::vector<Vec3f> scales;
    std::vector<float> weights;
};

struct RawAnimation
{
    std::string             name;
    std::vector<float>      times;
    std::vector<RawChannel> channels;
};

struct RawCamera
{
    std::string name;
    long        nodeId;

    enum
    {
        CAMERA_MODE_PERSPECTIVE,
        CAMERA_MODE_ORTHOGRAPHIC
    } mode;

    struct
    {
        float aspectRatio;
        float fovDegreesX;
        float fovDegreesY;
        float nearZ;
        float farZ;
    } perspective;

    struct
    {
        float magX;
        float magY;
        float nearZ;
        float farZ;
    } orthographic;
};

struct RawNode
{
    bool                     isJoint;
    long                     id;
    std::string              name;
    long                     parentId;
    std::vector<long>        childIds;
    Vec3f                    translation;
    Quatf                    rotation;
    Vec3f                    scale;
    long                     surfaceId;
};

class RawModel
{
public:
    RawModel();

    // Add geometry.
    void AddVertexAttribute(const RawVertexAttribute attrib);
    int AddVertex(const RawVertex &vertex);
    int AddTriangle(const int v0, const int v1, const int v2, const int materialIndex, const int surfaceIndex);
    int AddTexture(const std::string &name, const std::string &fileName, const std::string &fileLocation, RawTextureUsage usage);
    int AddMaterial(const RawMaterial &material);
    int AddMaterial(
        const char *name, const RawMaterialType materialType, const int textures[RAW_TEXTURE_USAGE_MAX],
        std::shared_ptr<RawMatProps> materialInfo);
    int AddSurface(const RawSurface &suface);
    int AddSurface(const char *name, long surfaceId);
    int AddAnimation(const RawAnimation &animation);
    int AddCameraPerspective(
        const char *name, const long nodeId, const float aspectRatio, const float fovDegreesX, const float fovDegreesY,
        const float nearZ, const float farZ);
    int
    AddCameraOrthographic(const char *name, const long nodeId, const float magX, const float magY, const float nearZ, const float farZ);
    int AddNode(const RawNode &node);
    int AddNode(const long id, const char *name, const long parentId);
    void SetRootNode(const long nodeId) { rootNodeId = nodeId; }
    const long GetRootNode() const { return rootNodeId; }

    // Remove unused vertices, textures or materials after removing vertex attributes, textures, materials or surfaces.
    void Condense();

    void TransformGeometry(ComputeNormalsOption);

    void TransformTextures(const std::vector<std::function<Vec2f(Vec2f)>> &transforms);

    size_t CalculateNormals(bool);

    // Get the attributes stored per vertex.
    int GetVertexAttributes() const { return vertexAttributes; }

    // Iterate over the vertices.
    int GetVertexCount() const { return (int) vertices.size(); }
    const RawVertex &GetVertex(const int index) const { return vertices[index]; }

    // Iterate over the triangles.
    int GetTriangleCount() const { return (int) triangles.size(); }
    const RawTriangle &GetTriangle(const int index) const { return triangles[index]; }

    // Iterate over the textures.
    int GetTextureCount() const { return (int) textures.size(); }
    const RawTexture &GetTexture(const int index) const { return textures[index]; }

    // Iterate over the materials.
    int GetMaterialCount() const { return (int) materials.size(); }
    const RawMaterial &GetMaterial(const int index) const { return materials[index]; }

    // Iterate over the surfaces.
    int GetSurfaceCount() const { return (int) surfaces.size(); }
    const RawSurface &GetSurface(const int index) const { return surfaces[index]; }
    RawSurface &GetSurface(const int index) { return surfaces[index]; }
    int GetSurfaceById(const long id) const;

    // Iterate over the animations.
    int GetAnimationCount() const { return (int) animations.size(); }
    const RawAnimation &GetAnimation(const int index) const { return animations[index]; }

    // Iterate over the cameras.
    int GetCameraCount() const { return (int) cameras.size(); }
    const RawCamera &GetCamera(const int index) const { return cameras[index]; }

    // Iterate over the nodes.
    int GetNodeCount() const { return (int) nodes.size(); }
    const RawNode &GetNode(const int index) const { return nodes[index]; }
    RawNode &GetNode(const int index) { return nodes[index]; }
    int GetNodeById(const long nodeId) const;

    // Create individual attribute arrays.
    // Returns true if the vertices store the particular attribute.
    template<typename _attrib_type_>
    void GetAttributeArray(std::vector<_attrib_type_> &out, const _attrib_type_ RawVertex::* ptr) const;

    // Create an array with a raw model for each material.
    // Multiple surfaces with the same material will turn into a single model.
    // However, surfaces that are marked as 'discrete' will turn into separate models.
    void CreateMaterialModels(
        std::vector<RawModel> &materialModels, bool shortIndices, const int keepAttribs, const bool forceDiscrete) const;

private:
    Vec3f getFaceNormal(int verts[3]) const;

    long                                             rootNodeId;
    int                                              vertexAttributes;
    std::unordered_map<RawVertex, int, VertexHasher> vertexHash;
    std::vector<RawVertex>                           vertices;
    std::vector<RawTriangle>                         triangles;
    std::vector<RawTexture>                          textures;
    std::vector<RawMaterial>                         materials;
    std::vector<RawSurface>                          surfaces;
    std::vector<RawAnimation>                        animations;
    std::vector<RawCamera>                           cameras;
    std::vector<RawNode>                             nodes;
};

template<typename _attrib_type_>
void RawModel::GetAttributeArray(std::vector<_attrib_type_> &out, const _attrib_type_ RawVertex::* ptr) const
{
    out.resize(vertices.size());
    for (size_t i = 0; i < vertices.size(); i++) {
        out[i] = vertices[i].*ptr;
    }
}

#endif // !__RAWMODEL_H__
