/**
 * @file LLGLTFLoader.cpp
 * @brief LLGLTFLoader class implementation
 *
 * $LicenseInfo:firstyear=2022&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2022, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llgltfloader.h"

// Import & define single-header gltf import/export lib
#define TINYGLTF_IMPLEMENTATION
#define TINYGLTF_USE_CPP14  // default is C++ 11

// tinygltf by default loads image files using STB
#define STB_IMAGE_IMPLEMENTATION
// to use our own image loading:
// 1. replace this definition with TINYGLTF_NO_STB_IMAGE
// 2. provide image loader callback with TinyGLTF::SetImageLoader(LoadimageDataFunction LoadImageData, void *user_data)

// tinygltf saves image files using STB
#define STB_IMAGE_WRITE_IMPLEMENTATION
// similarly, can override with TINYGLTF_NO_STB_IMAGE_WRITE and TinyGLTF::SetImageWriter(fxn, data)

// Additionally, disable inclusion of STB header files entirely with
// TINYGLTF_NO_INCLUDE_STB_IMAGE
// TINYGLTF_NO_INCLUDE_STB_IMAGE_WRITE
#include "tinygltf/tiny_gltf.h"


// TODO: includes inherited from dae loader.  Validate / prune

#include <boost/lexical_cast.hpp>

#include "llsdserialize.h"
#include "lljoint.h"

#include "glh/glh_linear.h"
#include "llmatrix4a.h"

#include <boost/regex.hpp>
#include <boost/algorithm/string/replace.hpp>

static const std::string lod_suffix[LLModel::NUM_LODS] =
{
	"_LOD0",
	"_LOD1",
	"_LOD2",
	"",
	"_PHYS",
};


LLGLTFLoader::LLGLTFLoader(std::string filename,
    S32                                 lod,
    LLModelLoader::load_callback_t      load_cb,
    LLModelLoader::joint_lookup_func_t  joint_lookup_func,
    LLModelLoader::texture_load_func_t  texture_load_func,
    LLModelLoader::state_callback_t     state_cb,
    void *                              opaque_userdata,
    JointTransformMap &                 jointTransformMap,
    JointNameSet &                      jointsFromNodes,
    std::map<std::string, std::string> &jointAliasMap,
    U32                                 maxJointsPerMesh,
    U32                                 modelLimit) //,
    //bool                                preprocess)
    : LLModelLoader( filename,
                     lod,
                     load_cb,
                     joint_lookup_func,
                     texture_load_func,
                     state_cb,
                     opaque_userdata,
                     jointTransformMap,
                     jointsFromNodes,
                     jointAliasMap,
                     maxJointsPerMesh ),
    //mPreprocessGLTF(preprocess),
    mMeshesLoaded(false),
    mMaterialsLoaded(false)
{
}

LLGLTFLoader::~LLGLTFLoader() {}

bool LLGLTFLoader::OpenFile(const std::string &filename)
{
    tinygltf::TinyGLTF loader;
    std::string        error_msg;
    std::string        warn_msg;

    // Load a tinygltf model fom a file. Assumes that the input filename has already been
    // been sanitized to one of (.gltf , .glb) extensions, so does a simple find to distinguish.
    if (std::string::npos == filename.rfind(".gltf"))
    {  // file is binary
        mGltfLoaded = loader.LoadBinaryFromFile(&mGltfModel, &error_msg, &warn_msg, filename);
    }
    else
    {  // file is ascii
        mGltfLoaded = loader.LoadASCIIFromFile(&mGltfModel, &error_msg, &warn_msg, filename);
    }

    if (!mGltfLoaded)
    {
        if (!warn_msg.empty())
            LL_WARNS("GLTF_IMPORT") << "gltf load warning: " << warn_msg.c_str() << LL_ENDL;
        if (!error_msg.empty())
            LL_WARNS("GLTF_IMPORT") << "gltf load error: " << error_msg.c_str() << LL_ENDL;
        return false;
    }

    mMeshesLoaded = parseMeshes();
    if (mMeshesLoaded) uploadMeshes();

    mMaterialsLoaded = parseMaterials();
    if (mMaterialsLoaded) uploadMaterials();

    return (mMeshesLoaded || mMaterialsLoaded);
}

bool LLGLTFLoader::parseMeshes()
{
    if (!mGltfLoaded) return false;

    // 2022-04 DJH Volume params from dae example. TODO understand PCODE
    LLVolumeParams volume_params;
    volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);    
    
    for (tinygltf::Mesh mesh : mGltfModel.meshes)
    {
        LLModel *pModel = new LLModel(volume_params, 0.f);

        if (populateModelFromMesh(pModel, mesh)         && 
            (LLModel::NO_ERRORS == pModel->getStatus()) &&
            validate_model(pModel))
        {
            mModelList.push_back(pModel);
        }
        else
        {
            setLoadState(ERROR_MODEL + pModel->getStatus());
            delete(pModel);
            return false;
        }
    }
    return true;
}

bool LLGLTFLoader::populateModelFromMesh(LLModel* pModel, const tinygltf::Mesh &mesh)
{
    pModel->mLabel = mesh.name;
    int pos_idx, norm_idx, tan_idx, uv0_idx, uv1_idx, color0_idx, color1_idx;
    tinygltf::Accessor indices_a, positions_a, normals_a, uv0_a, color0_a;

    auto prims = mesh.primitives;
    for (auto prim : prims)
    {
        if (prim.indices >= 0) indices_a = mGltfModel.accessors[prim.indices];

        pos_idx = (prim.attributes.count("POSITION") > 0) ? prim.attributes.at("POSITION") : -1;
        if (pos_idx >= 0)
        {
            positions_a = mGltfModel.accessors[pos_idx];
            if (TINYGLTF_COMPONENT_TYPE_FLOAT != positions_a.componentType) 
                continue;
            auto positions_bv = mGltfModel.bufferViews[positions_a.bufferView];
            auto positions_buf = mGltfModel.buffers[positions_bv.buffer];
            //auto type = positions_vb.
            //if (positions_buf.name
        }

        norm_idx = (prim.attributes.count("NORMAL") > 0) ? prim.attributes.at("NORMAL") : -1;
        tan_idx = (prim.attributes.count("TANGENT") > 0) ? prim.attributes.at("TANGENT") : -1;
        uv0_idx = (prim.attributes.count("TEXCOORDS_0") > 0) ? prim.attributes.at("TEXCOORDS_0") : -1;
        uv1_idx = (prim.attributes.count("TEXCOORDS_1") > 0) ? prim.attributes.at("TEXCOORDS_1") : -1;
        color0_idx = (prim.attributes.count("COLOR_0") > 0) ? prim.attributes.at("COLOR_0") : -1;
        color1_idx = (prim.attributes.count("COLOR_1") > 0) ? prim.attributes.at("COLOR_1") : -1;

        if (prim.mode == TINYGLTF_MODE_TRIANGLES)
        {
            //auto pos = mesh.    TODO resume here DJH 2022-04
        }
    }
    
    //pModel->addFace()
    return false;
}

bool LLGLTFLoader::parseMaterials() 
{
    if (!mGltfLoaded) return false;

    // fill local texture data structures
    mSamplers.clear();
    for (auto in_sampler : mGltfModel.samplers)
    {
        gltf_sampler sampler;
        sampler.magFilter = in_sampler.magFilter > 0 ? in_sampler.magFilter : GL_LINEAR;
        sampler.minFilter = in_sampler.minFilter > 0 ? in_sampler.minFilter : GL_LINEAR;;
        sampler.wrapS     = in_sampler.wrapS;
        sampler.wrapT     = in_sampler.wrapT;
        sampler.name      = in_sampler.name; // unused
        mSamplers.push_back(sampler);
    }

    mImages.clear();
    for (auto in_image : mGltfModel.images)
    {
        gltf_image image;
        image.numChannels     = in_image.component;
        image.bytesPerChannel = in_image.bits >> 3;     // Convert bits to bytes
        image.pixelType       = in_image.pixel_type;    // Maps exactly, i.e. TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE == GL_UNSIGNED_BYTE, etc
        image.size            = in_image.image.size();
        image.height          = in_image.height;
        image.width           = in_image.width;
        image.data            = in_image.image.data();

        if (in_image.as_is)
        {
            LL_WARNS("GLTF_IMPORT") << "Unsupported image encoding" << LL_ENDL;
            return false;
        }
        
        if (image.size != image.height * image.width * image.numChannels * image.bytesPerChannel)
        {
            LL_WARNS("GLTF_IMPORT") << "Image size error" << LL_ENDL;
            return false;
        }

        mImages.push_back(image);
    }

    mTextures.clear();
    for (auto in_tex : mGltfModel.textures)
    {
        gltf_texture tex;
        tex.imageIdx   = in_tex.source;
        tex.samplerIdx = in_tex.sampler;
        tex.imageUuid.setNull();

        if (tex.imageIdx >= mImages.size() || tex.samplerIdx >= mSamplers.size())
        {
            LL_WARNS("GLTF_IMPORT") << "Texture sampler/image index error" << LL_ENDL;
            return false;
        }

        mTextures.push_back(tex);
    }

    // parse each material
    for (tinygltf::Material gltf_material : mGltfModel.materials)
    {
        gltf_render_material mat;
        mat.name = gltf_material.name;

        tinygltf::PbrMetallicRoughness& pbr = gltf_material.pbrMetallicRoughness;
        mat.hasPBR = true;  // Always true, for now

        mat.baseColor.set(pbr.baseColorFactor.data());
        mat.hasBaseTex = pbr.baseColorTexture.index >= 0;
        mat.baseColorTexIdx = pbr.baseColorTexture.index;
        mat.baseColorTexCoords = pbr.baseColorTexture.texCoord;

        mat.metalness = pbr.metallicFactor;
        mat.roughness = pbr.roughnessFactor;
        mat.hasMRTex = pbr.metallicRoughnessTexture.index >= 0;
        mat.metalRoughTexIdx = pbr.metallicRoughnessTexture.index;
        mat.metalRoughTexCoords = pbr.metallicRoughnessTexture.texCoord;

        mat.normalScale = gltf_material.normalTexture.scale;
        mat.hasNormalTex = gltf_material.normalTexture.index >= 0;
        mat.normalTexIdx = gltf_material.normalTexture.index;
        mat.normalTexCoords = gltf_material.normalTexture.texCoord;

        mat.occlusionScale = gltf_material.occlusionTexture.strength;
        mat.hasOcclusionTex = gltf_material.occlusionTexture.index >= 0;
        mat.occlusionTexIdx = gltf_material.occlusionTexture.index;
        mat.occlusionTexCoords = gltf_material.occlusionTexture.texCoord;

        mat.emissiveColor.set(gltf_material.emissiveFactor.data());
        mat.hasEmissiveTex = gltf_material.emissiveTexture.index >= 0;
        mat.emissiveTexIdx = gltf_material.emissiveTexture.index;
        mat.emissiveTexCoords = gltf_material.emissiveTexture.texCoord;

        mat.alphaMode = gltf_material.alphaMode;
        mat.alphaMask = gltf_material.alphaCutoff;

        if ((mat.hasNormalTex    && (mat.normalTexIdx     >= mTextures.size())) ||
            (mat.hasOcclusionTex && (mat.occlusionTexIdx  >= mTextures.size())) ||
            (mat.hasEmissiveTex  && (mat.emissiveTexIdx   >= mTextures.size())) ||
            (mat.hasBaseTex      && (mat.baseColorTexIdx  >= mTextures.size())) ||
            (mat.hasMRTex        && (mat.metalRoughTexIdx >= mTextures.size())))
        {
            LL_WARNS("GLTF_IMPORT") << "Texture resource index error" << LL_ENDL;
            return false;
        }

        if ((mat.hasNormalTex    && (mat.normalTexCoords      > 2)) ||    // mesh can have up to 3 sets of UV
            (mat.hasOcclusionTex && (mat.occlusionTexCoords   > 2)) ||
            (mat.hasEmissiveTex  && (mat.emissiveTexCoords    > 2)) ||
            (mat.hasBaseTex      && (mat.baseColorTexCoords   > 2)) ||
            (mat.hasMRTex        && (mat.metalRoughTexCoords  > 2)))
        {
            LL_WARNS("GLTF_IMPORT") << "Image texcoord index error" << LL_ENDL;
            return false;
        }

        mMaterials.push_back(mat);
    }

    return true; 
}

// TODO: convert raw vertex buffers to UUIDs
void LLGLTFLoader::uploadMeshes()
{
    llassert(0);
}

// convert raw image buffers to texture UUIDs & assemble into a render material
void LLGLTFLoader::uploadMaterials()
{
    for (gltf_render_material mat : mMaterials) // Initially 1 material per gltf file, but design for multiple
    {
        if (mat.hasBaseTex)
        {
            gltf_texture& gtex = mTextures[mat.baseColorTexIdx];
            if (gtex.imageUuid.isNull()) 
            {
                gtex.imageUuid = imageBufferToTextureUUID(gtex);
            }
        }

        if (mat.hasMRTex)
        {
            gltf_texture& gtex = mTextures[mat.metalRoughTexIdx];
            if (gtex.imageUuid.isNull())
            {
                gtex.imageUuid = imageBufferToTextureUUID(gtex);
            }
        }

        if (mat.hasNormalTex)
        {
            gltf_texture& gtex = mTextures[mat.normalTexIdx];
            if (gtex.imageUuid.isNull())
            {
                gtex.imageUuid = imageBufferToTextureUUID(gtex);
            }
        }

        if (mat.hasOcclusionTex)
        {
            gltf_texture& gtex = mTextures[mat.occlusionTexIdx];
            if (gtex.imageUuid.isNull())
            {
                gtex.imageUuid = imageBufferToTextureUUID(gtex);
            }
        }

        if (mat.hasEmissiveTex)
        {
            gltf_texture& gtex = mTextures[mat.emissiveTexIdx];
            if (gtex.imageUuid.isNull())
            {
                gtex.imageUuid = imageBufferToTextureUUID(gtex);
            }
        }
    }
}

LLUUID LLGLTFLoader::imageBufferToTextureUUID(const gltf_texture& tex)
{
    //gltf_image& image = mImages[tex.imageIdx];
    //gltf_sampler& sampler = mSamplers[tex.samplerIdx];

    // fill an LLSD container with image+sampler data

    // upload texture

    // retrieve UUID

    return LLUUID::null;
}
