// mesh.h
//
// Copyright (C) 2004-2010, Celestia Development Team
// Original version by Chris Laurel <claurel@gmail.com>
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.

#pragma once

#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

#include <Eigen/Core>
#include <Eigen/Geometry>

#include <celutil/array_view.h>
#include <celutil/classops.h>
#include "material.h"

namespace cmod
{
// 32-bit index type
using Index32 = std::uint32_t;

using VWord = std::uint32_t;

enum class VertexAttributeSemantic : std::int16_t
{
    Position     = 0,
    Color0       = 1,
    Color1       = 2,
    Normal       = 3,
    Tangent      = 4,
    Texture0     = 5,
    Texture1     = 6,
    Texture2     = 7,
    Texture3     = 8,
    PointSize    = 9,
    SemanticMax  = 10,
    InvalidSemantic  = -1,
};

enum class VertexAttributeFormat : std::int16_t
{
    Float1    = 0,
    Float2    = 1,
    Float3    = 2,
    Float4    = 3,
    UByte4    = 4,
    FormatMax = 5,
    InvalidFormat = -1,
};

enum class PrimitiveGroupType : std::int16_t
{
    TriList    = 0,
    TriStrip   = 1,
    TriFan     = 2,
    LineList   = 3,
    LineStrip  = 4,
    PointList  = 5,
    SpriteList = 6,
    PrimitiveTypeMax = 7,
    InvalidPrimitiveGroupType = -1
};

struct VertexAttribute
{
    constexpr VertexAttribute() = default;

    constexpr VertexAttribute(VertexAttributeSemantic _semantic,
                              VertexAttributeFormat _format,
                              unsigned int _offsetWords) :
        semantic(_semantic),
        format(_format),
        offsetWords(_offsetWords)
    {
    }

    static constexpr unsigned int getFormatSizeWords(VertexAttributeFormat fmt)
    {
        switch (fmt)
        {
        case VertexAttributeFormat::Float1:
        case VertexAttributeFormat::UByte4:
            return 1;
        case VertexAttributeFormat::Float2:
            return 2;
        case VertexAttributeFormat::Float3:
            return 3;
        case VertexAttributeFormat::Float4:
            return 4;
        default:
            return 0;
        }
    }

    VertexAttributeSemantic semantic{ VertexAttributeSemantic::InvalidSemantic };
    VertexAttributeFormat   format{ VertexAttributeFormat::InvalidFormat };
    unsigned int            offsetWords{ 0 };
};

bool operator==(const VertexAttribute& a, const VertexAttribute& b);
bool operator<(const VertexAttribute& a, const VertexAttribute& b);

class VertexDescription : private celestia::util::NoCopy
{
public:
    VertexDescription();
    explicit VertexDescription(std::vector<VertexAttribute>&& attributes);

    VertexDescription clone() const;

    const VertexAttribute& getAttribute(VertexAttributeSemantic semantic) const;

    unsigned int strideBytes() const noexcept { return m_strideBytes; }
    celestia::util::array_view<VertexAttribute> attributes() const { return m_attributes; }

    bool validate() const;

    VertexDescription augment(VertexAttributeSemantic, VertexAttributeFormat) const;

private:
    unsigned int m_strideBytes{ 0 };
    std::vector<VertexAttribute> m_attributes;
    // Vertex attributes indexed by semantic
    std::array<int, static_cast<std::size_t>(VertexAttributeSemantic::SemanticMax)> m_semanticMap;

    friend bool operator==(const VertexDescription&, const VertexDescription&);
    friend bool operator<(const VertexDescription&, const VertexDescription&);
};

bool operator==(const VertexDescription&, const VertexDescription&);
bool operator<(const VertexDescription&, const VertexDescription&);

struct PrimitiveGroup : private celestia::util::NoCopy
{
    PrimitiveGroup clone() const;
    unsigned int getPrimitiveCount() const;

    PrimitiveGroupType prim{ PrimitiveGroupType::InvalidPrimitiveGroupType };
    unsigned int materialIndex{ 0 };
    int indicesCount{ 0 };
    int indicesOffset{ 0 };
    std::vector<Index32> indices;
};

class Mesh : private celestia::util::NoCopy
{
public:
    class PickResult
    {
    public:
        PickResult() = default;

        const Mesh* mesh{ nullptr };
        const PrimitiveGroup* group{ nullptr };
        unsigned int primitiveIndex{ 0 };
        double distance{ -1.0 };
    };

    Mesh clone() const;

    void setVertices(unsigned int _nVertices, std::vector<VWord>&& vertexData);
    bool setVertexDescription(VertexDescription&& desc);
    const VertexDescription& getVertexDescription() const;

    const PrimitiveGroup* getGroup(unsigned int index) const;
    PrimitiveGroup* getGroup(unsigned int index);
    unsigned int addGroup(PrimitiveGroup&& group);
    unsigned int addGroup(PrimitiveGroupType prim,
                          unsigned int materialIndex,
                          std::vector<Index32>&& indices);
    unsigned int getGroupCount() const;
    void remapIndices(const std::vector<Index32>& indexMap);
    void clearGroups();

    void remapMaterials(const std::vector<unsigned int>& materialMap);

    /*! Reorder primitive groups so that groups with identical materials
     *  appear sequentially in the primitive group list. This will reduce
     *  the number of graphics state changes at render time.
     */
    void aggregateByMaterial();

    const std::string& getName() const;
    void setName(std::string&&);

    bool pick(const Eigen::Vector3d& origin, const Eigen::Vector3d& direction, PickResult* result) const;
    bool pick(const Eigen::Vector3d& origin, const Eigen::Vector3d& direction, double& distance) const;

    Eigen::AlignedBox<float, 3> getBoundingBox() const;
    void transform(const Eigen::Vector3f& translation, float scale);

    const VWord* getVertexData() const { return vertices.data(); }
    unsigned int getVertexCount() const { return nVertices; }
    unsigned int getVertexStrideWords() const { return vertexDesc.strideBytes() / sizeof(cmod::VWord); }
    unsigned int getPrimitiveCount() const;

    unsigned int getIndexCount() const { return nTotalIndices; }

    void merge(const Mesh&);
    bool canMerge(const Mesh&, const std::vector<Material> &materials) const;
    void optimize();

    void rebuildIndexMetadata();

private:
    void mergePrimitiveGroups();

    VertexDescription vertexDesc;

    unsigned int nVertices{ 0 };
    std::vector<VWord> vertices;

    unsigned int nTotalIndices{ 0 };

    std::vector<PrimitiveGroup> groups;

    std::string name;
};

Mesh GenerateTangents(const Mesh& mesh);

} // namespace cmod
