/**
 * @file gltfscenemanager.cpp
 * @brief Builds menus out of items.
 *
 * $LicenseInfo:firstyear=2024&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2024, 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 "llviewerprecompiledheaders.h"

#include "gltfscenemanager.h"
#include "llviewermenufile.h"
#include "llappviewer.h"
#include "lltinygltfhelper.h"
#include "llvertexbuffer.h"
#include "llselectmgr.h"
#include "llagent.h"
#include "llnotificationsutil.h"
#include "llvoavatarself.h"
#include "llvolumeoctree.h"
#include "gltf/asset.h"
#include "pipeline.h"
#include "llviewershadermgr.h"
#include "llviewertexturelist.h"
#include "llimagej2c.h"
#include "llfloaterperms.h"
#include "llfloaterreg.h"
#include "llagentbenefits.h"
#include "llfilesystem.h"
#include "boost/json.hpp"

#define GLTF_SIM_SUPPORT 1

using namespace LL;

// temporary location of LL GLTF Implementation
using namespace LL::GLTF;

void GLTFSceneManager::load()
{
    LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject();

    if (obj)
    {
        // Load a scene from disk
        LLFilePickerReplyThread::startPicker(
            [](const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter)
            {
                if (LLAppViewer::instance()->quitRequested())
                {
                    return;
                }
                if (filenames.size() > 0)
                {
                    GLTFSceneManager::instance().load(filenames[0]);
                }
            },
            LLFilePicker::FFLOAD_GLTF,
            false);
    }
    else
    {
        LLNotificationsUtil::add("GLTFOpenSelection");
    }
}

void GLTFSceneManager::saveAs()
{
    LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject();
    if (obj && obj->mGLTFAsset)
    {
        LLFilePickerReplyThread::startPicker(
            [](const std::vector<std::string>& filenames, LLFilePicker::ELoadFilter load_filter, LLFilePicker::ESaveFilter save_filter)
            {
                if (LLAppViewer::instance()->quitRequested())
                {
                    return;
                }
                if (filenames.size() > 0)
                {
                    GLTFSceneManager::instance().save(filenames[0]);
                }
            },
            LLFilePicker::FFSAVE_GLTF,
            "scene.gltf");
    }
    else
    {
        LLNotificationsUtil::add("GLTFSaveSelection");
    }
}

void GLTFSceneManager::uploadSelection()
{
    if (mUploadingAsset)
    { // upload already in progress
        LLNotificationsUtil::add("GLTFUploadInProgress");
        return;
    }

    LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject();
    if (obj && obj->mGLTFAsset)
    {
        // make a copy of the asset prior to uploading
        mUploadingAsset = std::make_shared<Asset>();
        mUploadingObject = obj;
        *mUploadingAsset = *obj->mGLTFAsset;

        GLTF::Asset& asset = *mUploadingAsset;

        for (auto& image : asset.mImages)
        {
            if (image.mTexture.notNull())
            {
                mPendingImageUploads++;

                LLPointer<LLImageRaw> raw;

                if (image.mBufferView != INVALID_INDEX)
                {
                    BufferView& view = asset.mBufferViews[image.mBufferView];
                    Buffer& buffer = asset.mBuffers[view.mBuffer];

                    raw = LLViewerTextureManager::getRawImageFromMemory(buffer.mData.data() + view.mByteOffset, view.mByteLength, image.mMimeType);

                    image.clearData(asset);
                }
                else
                {
                    raw = image.mTexture->getRawImage();
                }

                if (raw.isNull())
                {
                    raw = image.mTexture->getSavedRawImage();
                }

                if (raw.isNull())
                {
                    image.mTexture->readbackRawImage();
                }

                if (raw.notNull())
                {
                    LLPointer<LLImageJ2C> j2c = LLViewerTextureList::convertToUploadFile(raw);

                    std::string buffer;
                    buffer.assign((const char*)j2c->getData(), j2c->getDataSize());

                    LLUUID asset_id = LLUUID::generateNewID();

                    std::string name;
                    S32 idx = (S32)(&image - &asset.mImages[0]);

                    if (image.mName.empty())
                    {

                        name = llformat("Image_%d", idx);
                    }
                    else
                    {
                        name = image.mName;
                    }

                    LLNewBufferedResourceUploadInfo::uploadFailure_f failure = [this](LLUUID assetId, LLSD response, std::string reason)
                        {
                            // TODO: handle failure
                            mPendingImageUploads--;
                            return false;
                        };


                    LLNewBufferedResourceUploadInfo::uploadFinish_f finish = [this, idx, raw, j2c](LLUUID assetId, LLSD response)
                        {
                            if (mUploadingAsset && mUploadingAsset->mImages.size() > idx)
                            {
                                mUploadingAsset->mImages[idx].mUri = assetId.asString();
                                mPendingImageUploads--;
                            }
                        };

                    S32 expected_upload_cost = LLAgentBenefitsMgr::current().getTextureUploadCost(j2c);

                    LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLNewBufferedResourceUploadInfo>(
                        buffer,
                        asset_id,
                        name,
                        name,
                        0,
                        LLFolderType::FT_TEXTURE,
                        LLInventoryType::IT_TEXTURE,
                        LLAssetType::AT_TEXTURE,
                        LLFloaterPerms::getNextOwnerPerms("Uploads"),
                        LLFloaterPerms::getGroupPerms("Uploads"),
                        LLFloaterPerms::getEveryonePerms("Uploads"),
                        expected_upload_cost,
                        false,
                        finish,
                        failure));

                    upload_new_resource(uploadInfo);
                }
            }
        }

        // upload .bin
        for (auto& bin : asset.mBuffers)
        {
            mPendingBinaryUploads++;

            S32 idx = (S32)(&bin - &asset.mBuffers[0]);

            std::string buffer;
            buffer.assign((const char*)bin.mData.data(), bin.mData.size());

            LLUUID asset_id = LLUUID::generateNewID();

            LLNewBufferedResourceUploadInfo::uploadFailure_f failure = [this](LLUUID assetId, LLSD response, std::string reason)
                {
                    // TODO: handle failure
                    mPendingBinaryUploads--;
                    mUploadingAsset = nullptr;
                    mUploadingObject = nullptr;
                    LL_WARNS("GLTF") << "Failed to upload GLTF binary: " << reason << LL_ENDL;
                    LL_WARNS("GLTF") << response << LL_ENDL;
                    return false;
                };

            LLNewBufferedResourceUploadInfo::uploadFinish_f finish = [this, idx](LLUUID assetId, LLSD response)
                {
                    if (mUploadingAsset && mUploadingAsset->mBuffers.size() > idx)
                    {
                        mUploadingAsset->mBuffers[idx].mUri = assetId.asString();
                        mPendingBinaryUploads--;

                        // HACK: save buffer to cache to emulate a successful download
                        LLFileSystem cache(assetId, LLAssetType::AT_GLTF_BIN, LLFileSystem::WRITE);
                        auto& data = mUploadingAsset->mBuffers[idx].mData;

                        llassert(data.size() <= size_t(S32_MAX));
                        cache.write((const U8 *) data.data(), S32(data.size()));
                    }
                };
#if GLTF_SIM_SUPPORT
            S32 expected_upload_cost = 1;

            LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLNewBufferedResourceUploadInfo>(
                buffer,
                asset_id,
                "",
                "",
                0,
                LLFolderType::FT_NONE,
                LLInventoryType::IT_GLTF_BIN,
                LLAssetType::AT_GLTF_BIN,
                LLFloaterPerms::getNextOwnerPerms("Uploads"),
                LLFloaterPerms::getGroupPerms("Uploads"),
                LLFloaterPerms::getEveryonePerms("Uploads"),
                expected_upload_cost,
                false,
                finish,
                failure));

            upload_new_resource(uploadInfo);
#else
            // dummy finish
            finish(LLUUID::generateNewID(), LLSD());
#endif
        }
    }
    else
    {
        LLNotificationsUtil::add("GLTFUploadSelection");
    }
}

void GLTFSceneManager::save(const std::string& filename)
{
    LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject();
    if (obj && obj->mGLTFAsset)
    {
        Asset* asset = obj->mGLTFAsset.get();
        if (!asset->save(filename))
        {
            LLNotificationsUtil::add("GLTFSaveFailed");
        }
    }
}

void GLTFSceneManager::load(const std::string& filename)
{
    std::shared_ptr<Asset> asset = std::make_shared<Asset>();

    if (asset->load(filename))
    {
        gDebugProgram.bind(); // bind a shader to satisfy LLVertexBuffer assertions
        asset->updateTransforms();

        // hang the asset off the currently selected object, or off of the avatar if no object is selected
        LLViewerObject* obj = LLSelectMgr::instance().getSelection()->getFirstRootObject();

        if (obj)
        { // assign to self avatar
            obj->mGLTFAsset = asset;
            obj->markForUpdate();
            if (std::find(mObjects.begin(), mObjects.end(), obj) == mObjects.end())
            {
                mObjects.push_back(obj);
            }
            LLFloaterReg::showInstance("gltf_asset_editor");
        }
    }
    else
    {
        LLNotificationsUtil::add("GLTFLoadFailed");
    }
}

GLTFSceneManager::~GLTFSceneManager()
{
    mObjects.clear();
}

void GLTFSceneManager::renderOpaque()
{
    render(true);
}

void GLTFSceneManager::renderAlpha()
{
    render(false);
}

void GLTFSceneManager::addGLTFObject(LLViewerObject* obj, LLUUID gltf_id)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
    llassert(obj->getVolume()->getParams().getSculptID() == gltf_id);
    llassert(obj->getVolume()->getParams().getSculptType() == LL_SCULPT_TYPE_GLTF);

    if (obj->mGLTFAsset)
    { // object already has a GLTF asset, don't reload it

        // TODO: below assertion fails on dupliate requests for assets -- possibly need to touch up asset loading state machine
        // llassert(std::find(mObjects.begin(), mObjects.end(), obj) != mObjects.end());
        return;
    }

    obj->ref();
    gAssetStorage->getAssetData(gltf_id, LLAssetType::AT_GLTF, onGLTFLoadComplete, obj);
}

//static
void GLTFSceneManager::onGLTFBinLoadComplete(const LLUUID& id, LLAssetType::EType asset_type, void* user_data, S32 status, LLExtStat ext_status)
{
    LLViewerObject* obj = (LLViewerObject*)user_data;
    llassert(asset_type == LLAssetType::AT_GLTF_BIN);

    if (status == LL_ERR_NOERR)
    {
        if (obj)
        {
            // find the Buffer with the given id in the asset
            if (obj->mGLTFAsset)
            {
                obj->mGLTFAsset->mPendingBuffers--;


                if (obj->mGLTFAsset->mPendingBuffers == 0)
                {
                    if (obj->mGLTFAsset->prep())
                    {
                        GLTFSceneManager& mgr = GLTFSceneManager::instance();
                        if (std::find(mgr.mObjects.begin(), mgr.mObjects.end(), obj) == mgr.mObjects.end())
                        {
                            GLTFSceneManager::instance().mObjects.push_back(obj);
                        }
                    }
                    else
                    {
                        LL_WARNS("GLTF") << "Failed to prepare GLTF asset: " << id << LL_ENDL;
                        obj->mGLTFAsset = nullptr;
                    }
                }
            }
        }
    }
    else
    {
        LL_WARNS("GLTF") << "Failed to load GLTF asset: " << id << LL_ENDL;
        obj->unref();
    }
}

//static
void GLTFSceneManager::onGLTFLoadComplete(const LLUUID& id, LLAssetType::EType asset_type, void* user_data, S32 status, LLExtStat ext_status)
{
    LLViewerObject* obj = (LLViewerObject*)user_data;
    llassert(asset_type == LLAssetType::AT_GLTF);

    if (status == LL_ERR_NOERR)
    {
        if (obj)
        {
            LLFileSystem file(id, asset_type, LLFileSystem::READ);
            std::string data;
            S32 file_size = file.getSize();
            data.resize(file_size);
            file.read((U8*)data.data(), file_size);

            boost::json::value json = boost::json::parse(data);

            std::shared_ptr<Asset> asset = std::make_shared<Asset>(json);
            obj->mGLTFAsset = asset;

            for (auto& buffer : asset->mBuffers)
            {
                // for now just assume the buffer is already in the asset cache
                LLUUID buffer_id;
                if (LLUUID::parseUUID(buffer.mUri, &buffer_id))
                {
                    asset->mPendingBuffers++;

                    gAssetStorage->getAssetData(buffer_id, LLAssetType::AT_GLTF_BIN, onGLTFBinLoadComplete, obj);
                }
                else
                {
                    LL_WARNS("GLTF") << "Buffer URI is not a valid UUID: " << buffer.mUri << LL_ENDL;
                    obj->unref();
                    return;
                }
            }
        }
    }
    else
    {
        LL_WARNS("GLTF") << "Failed to load GLTF asset: " << id << LL_ENDL;
        obj->unref();
    }
}

void GLTFSceneManager::update()
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;

    for (U32 i = 0; i < mObjects.size(); ++i)
    {
        if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr)
        {
            mObjects.erase(mObjects.begin() + i);
            --i;
            continue;
        }

        mObjects[i]->mGLTFAsset->update();
    }

    // process pending uploads
    if (mUploadingAsset && !mGLTFUploadPending)
    {
        if (mPendingImageUploads == 0 && mPendingBinaryUploads == 0)
        {
            boost::json::object obj;
            mUploadingAsset->serialize(obj);
            std::string buffer = boost::json::serialize(obj, {});

            LLNewBufferedResourceUploadInfo::uploadFailure_f failure = [this](LLUUID assetId, LLSD response, std::string reason)
                {
                    // TODO: handle failure
                    LL_WARNS("GLTF") << "Failed to upload GLTF json: " << reason << LL_ENDL;
                    LL_WARNS("GLTF") << response << LL_ENDL;

                    mUploadingAsset = nullptr;
                    mUploadingObject = nullptr;
                    mGLTFUploadPending = false;
                    return false;
                };

            LLNewBufferedResourceUploadInfo::uploadFinish_f finish = [this, buffer](LLUUID assetId, LLSD response)
            {
                LLAppViewer::instance()->postToMainCoro(
                    [=]()
                    {
                        if (mUploadingAsset)
                        {
                            // HACK: save buffer to cache to emulate a successful upload
                            LLFileSystem cache(assetId, LLAssetType::AT_GLTF, LLFileSystem::WRITE);

                            LL_INFOS("GLTF") << "Uploaded GLTF json: " << assetId << LL_ENDL;
                            llassert(buffer.size() <= size_t(S32_MAX));
                            cache.write((const U8 *) buffer.c_str(), S32(buffer.size()));

                            mUploadingAsset = nullptr;
                        }

                        if (mUploadingObject)
                        {
                            mUploadingObject->mGLTFAsset = nullptr;
                            mUploadingObject->setGLTFAsset(assetId);
                            mUploadingObject->markForUpdate();
                            mUploadingObject = nullptr;
                        }

                        mGLTFUploadPending = false;
                    });
            };

#if GLTF_SIM_SUPPORT
            S32 expected_upload_cost = 1;
            LLUUID asset_id = LLUUID::generateNewID();

            mGLTFUploadPending = true;

            LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLNewBufferedResourceUploadInfo>(
                buffer,
                asset_id,
                "",
                "",
                0,
                LLFolderType::FT_NONE,
                LLInventoryType::IT_GLTF,
                LLAssetType::AT_GLTF,
                LLFloaterPerms::getNextOwnerPerms("Uploads"),
                LLFloaterPerms::getGroupPerms("Uploads"),
                LLFloaterPerms::getEveryonePerms("Uploads"),
                expected_upload_cost,
                false,
                finish,
                failure));

            upload_new_resource(uploadInfo);
#else
            // dummy finish
            finish(LLUUID::generateNewID(), LLSD());
#endif
        }
    }
}

void GLTFSceneManager::render(bool opaque, bool rigged, bool unlit)
{
    U8 variant = 0;
    if (rigged)
    {
        variant |= LLGLSLShader::GLTFVariant::RIGGED;
    }
    if (!opaque)
    {
        variant |= LLGLSLShader::GLTFVariant::ALPHA_BLEND;
    }
    if (unlit)
    {
        variant |= LLGLSLShader::GLTFVariant::UNLIT;
    }

    render(variant);
}

void GLTFSceneManager::render(U8 variant)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
    // just render the whole scene by traversing the whole scenegraph
    // Assumes camera transform is already set and appropriate shader is already bound.
    // Eventually we'll want a smarter render pipe that has pre-sorted the scene graph
    // into buckets by material and shader.

    // HACK -- implicitly render multi-uv variant
    if (!(variant & LLGLSLShader::GLTFVariant::MULTI_UV))
    {
        render((U8) (variant | LLGLSLShader::GLTFVariant::MULTI_UV));
    }

    bool rigged = variant & LLGLSLShader::GLTFVariant::RIGGED;

    for (U32 i = 0; i < mObjects.size(); ++i)
    {
        if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr)
        {
            mObjects.erase(mObjects.begin() + i);
            --i;
            continue;
        }

        Asset* asset = mObjects[i]->mGLTFAsset.get();
        gGL.pushMatrix();

        LLMatrix4a mat = mObjects[i]->getGLTFAssetToAgentTransform();

        // provide a modelview matrix that goes from asset to camera space
        // (matrix palettes are in asset space)
        gGL.loadMatrix(gGLModelView);
        gGL.multMatrix(mat.getF32ptr());

        render(*asset, variant);

        gGL.popMatrix();
    }
}

void GLTFSceneManager::render(Asset& asset, U8 variant)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;

    for (U32 ds = 0; ds < 2; ++ds)
    {
        RenderData& rd = asset.mRenderData[ds];
        auto& batches = rd.mBatches[variant];

        if (batches.empty())
        {
            return;
        }

        LLGLDisable cull_face(ds == 1 ? GL_CULL_FACE : 0);

        bool opaque = !(variant & LLGLSLShader::GLTFVariant::ALPHA_BLEND);
        bool rigged = variant & LLGLSLShader::GLTFVariant::RIGGED;

        bool shader_bound = false;

        for (U32 i = 0; i < batches.size(); ++i)
        {
            if (batches[i].mPrimitives.empty() || batches[i].mVertexBuffer.isNull())
            {
                continue;
            }

            if (!shader_bound)
            { // don't bind the shader until we know we have somthing to render
                if (opaque)
                {
                    gGLTFPBRMetallicRoughnessProgram.bind(variant);
                }
                else
                { // alpha shaders need all the shadow map setup etc
                    gPipeline.bindDeferredShader(gGLTFPBRMetallicRoughnessProgram.mGLTFVariants[variant]);
                }

                if (!rigged)
                {
                    glBindBufferBase(GL_UNIFORM_BUFFER, LLGLSLShader::UB_GLTF_NODES, asset.mNodesUBO);
                }

                glBindBufferBase(GL_UNIFORM_BUFFER, LLGLSLShader::UB_GLTF_MATERIALS, asset.mMaterialsUBO);

                for (U32 i = 0; i < TEXTURE_TYPE_COUNT; ++i)
                {
                    mLastTexture[i] = -2;
                }

                gGL.syncMatrices();
                shader_bound = true;
            }

            {
                LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfdc - set vb");
                batches[i].mVertexBuffer->setBuffer();
            }

            S32 mat_idx = i - 1;
            if (mat_idx != INVALID_INDEX)
            {
                Material& material = asset.mMaterials[mat_idx];
                bind(asset, material);
            }
            else
            {
                LLFetchedGLTFMaterial::sDefault.bind();
                LLGLSLShader::sCurBoundShaderPtr->uniform1i(LLShaderMgr::GLTF_MATERIAL_ID, -1);
            }

            for (auto& pdata : batches[i].mPrimitives)
            {
                LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("GLTF draw call");
                Node& node = asset.mNodes[pdata.mNodeIndex];
                Mesh& mesh = asset.mMeshes[node.mMesh];
                Primitive& primitive = mesh.mPrimitives[pdata.mPrimitiveIndex];

                if (rigged)
                {
                    LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfdc - bind skin");
                    llassert(node.mSkin != INVALID_INDEX);
                    Skin& skin = asset.mSkins[node.mSkin];
                    glBindBufferBase(GL_UNIFORM_BUFFER, LLGLSLShader::UB_GLTF_JOINTS, skin.mUBO);
                }
                else
                {
                    LLGLSLShader::sCurBoundShaderPtr->uniform1i(LLShaderMgr::GLTF_NODE_ID, pdata.mNodeIndex);
                }

                {
                    LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gltfdc - push vb");

                    primitive.mVertexBuffer->drawRangeFast(primitive.mGLMode, primitive.mVertexOffset, primitive.mVertexOffset + primitive.getVertexCount() - 1, primitive.getIndexCount(), primitive.mIndexOffset);
                }
            }
        }
    }
}

void GLTFSceneManager::bindTexture(Asset& asset, TextureType texture_type, TextureInfo& info, LLViewerTexture* fallback)
{
    U8 type_idx = (U8)texture_type;

    if (info.mIndex == mLastTexture[type_idx])
    { //already bound
        return;
    }

    S32 uniform[] =
    {
        LLShaderMgr::DIFFUSE_MAP,
        LLShaderMgr::NORMAL_MAP,
        LLShaderMgr::METALLIC_ROUGHNESS_MAP,
        LLShaderMgr::OCCLUSION_MAP,
        LLShaderMgr::EMISSIVE_MAP
    };

    S32 channel = LLGLSLShader::sCurBoundShaderPtr->getTextureChannel(uniform[(U8)type_idx]);

    if (channel > -1)
    {
        glActiveTexture(GL_TEXTURE0 + channel);

        if (info.mIndex != INVALID_INDEX)
        {
            Texture& texture = asset.mTextures[info.mIndex];

            LLViewerTexture* tex = asset.mImages[texture.mSource].mTexture;
            if (tex)
            {
                LL_PROFILE_ZONE_NAMED_CATEGORY_GLTF("gl bind texture");
                glBindTexture(GL_TEXTURE_2D, tex->getTexName());

                if (channel != -1 && texture.mSampler != -1)
                { // set sampler state
                    Sampler& sampler = asset.mSamplers[texture.mSampler];
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, sampler.mWrapS);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, sampler.mWrapT);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, sampler.mMagFilter);

                    // NOTE: do not set min filter.  Always respect client preference for min filter
                }
                else
                {
                    // set default sampler state
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
                    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
                }
            }
            else
            {
                glBindTexture(GL_TEXTURE_2D, fallback->getTexName());
            }
        }
        else
        {
            glBindTexture(GL_TEXTURE_2D, fallback->getTexName());
        }
    }
}


void GLTFSceneManager::bind(Asset& asset, Material& material)
{
    LL_PROFILE_ZONE_SCOPED_CATEGORY_GLTF;
    LLGLSLShader* shader = LLGLSLShader::sCurBoundShaderPtr;

    bindTexture(asset, TextureType::BASE_COLOR, material.mPbrMetallicRoughness.mBaseColorTexture, LLViewerFetchedTexture::sWhiteImagep);

    if (!LLPipeline::sShadowRender)
    {
        bindTexture(asset, TextureType::NORMAL, material.mNormalTexture, LLViewerFetchedTexture::sFlatNormalImagep);
        bindTexture(asset, TextureType::METALLIC_ROUGHNESS, material.mPbrMetallicRoughness.mMetallicRoughnessTexture, LLViewerFetchedTexture::sWhiteImagep);
        bindTexture(asset, TextureType::OCCLUSION, material.mOcclusionTexture, LLViewerFetchedTexture::sWhiteImagep);
        bindTexture(asset, TextureType::EMISSIVE, material.mEmissiveTexture, LLViewerFetchedTexture::sWhiteImagep);
    }

    shader->uniform1i(LLShaderMgr::GLTF_MATERIAL_ID, (GLint)(&material - &asset.mMaterials[0]));
}

LLMatrix4a inverse(const LLMatrix4a& mat)
{
    glh::matrix4f m((F32*)mat.mMatrix);
    m = m.inverse();
    LLMatrix4a ret;
    ret.loadu(m.m);
    return ret;
}

bool GLTFSceneManager::lineSegmentIntersect(LLVOVolume* obj, Asset* asset, const LLVector4a& start, const LLVector4a& end, S32 face, bool pick_transparent, bool pick_rigged, bool pick_unselectable, S32* node_hit, S32* primitive_hit,
    LLVector4a* intersection, LLVector2* tex_coord, LLVector4a* normal, LLVector4a* tangent)

{
    // line segment intersection test
    // start and end should be in agent space
    // volume space and asset space should be the same coordinate frame
    // results should be transformed back to agent space

    bool ret = false;

    LLVector4a local_start;
    LLVector4a local_end;

    LLMatrix4a asset_to_agent = obj->getGLTFAssetToAgentTransform();
    LLMatrix4a agent_to_asset = inverse(asset_to_agent);

    agent_to_asset.affineTransform(start, local_start);
    agent_to_asset.affineTransform(end, local_end);

    LLVector4a p;
    LLVector4a n;
    LLVector2 tc;
    LLVector4a tn;

    if (intersection != NULL)
    {
        p = *intersection;
    }

    if (tex_coord != NULL)
    {
        tc = *tex_coord;
    }

    if (normal != NULL)
    {
        n = *normal;
    }

    if (tangent != NULL)
    {
        tn = *tangent;
    }

    S32 hit_node_index = asset->lineSegmentIntersect(local_start, local_end, &p, &tc, &n, &tn, primitive_hit);

    if (hit_node_index >= 0)
    {
        local_end = p;
        if (node_hit != NULL)
        {
            *node_hit = hit_node_index;
        }

        if (intersection != NULL)
        {
            asset_to_agent.affineTransform(p, *intersection);
        }

        if (normal != NULL)
        {
            LLVector3 v_n(n.getF32ptr());
            normal->load3(obj->volumeDirectionToAgent(v_n).mV);
            (*normal).normalize3fast();
        }

        if (tangent != NULL)
        {
            LLVector3 v_tn(tn.getF32ptr());

            LLVector4a trans_tangent;
            trans_tangent.load3(obj->volumeDirectionToAgent(v_tn).mV);

            LLVector4Logical mask;
            mask.clear();
            mask.setElement<3>();

            tangent->setSelectWithMask(mask, tn, trans_tangent);
            (*tangent).normalize3fast();
        }

        if (tex_coord != NULL)
        {
            *tex_coord = tc;
        }

        ret = true;
    }

    return ret;
}

LLDrawable* GLTFSceneManager::lineSegmentIntersect(const LLVector4a& start, const LLVector4a& end,
    bool pick_transparent,
    bool pick_rigged,
    bool pick_unselectable,
    bool pick_reflection_probe,
    S32* node_hit,                   // return the index of the node that was hit
    S32* primitive_hit,               // return the index of the primitive that was hit
    LLVector4a* intersection,         // return the intersection point
    LLVector2* tex_coord,            // return the texture coordinates of the intersection point
    LLVector4a* normal,               // return the surface normal at the intersection point
    LLVector4a* tangent)            // return the surface tangent at the intersection point
{
    LLDrawable* drawable = nullptr;

    LLVector4a local_end = end;
    LLVector4a position;

    for (U32 i = 0; i < mObjects.size(); ++i)
    {
        if (mObjects[i]->isDead() || mObjects[i]->mGLTFAsset == nullptr || !mObjects[i]->getVolume())
        {
            mObjects.erase(mObjects.begin() + i);
            --i;
            continue;
        }

        // temporary debug -- always double check objects that have GLTF scenes hanging off of them even if the ray doesn't intersect the object bounds
        if (lineSegmentIntersect((LLVOVolume*) mObjects[i].get(), mObjects[i]->mGLTFAsset.get(), start, local_end, -1, pick_transparent, pick_rigged, pick_unselectable, node_hit, primitive_hit, &position, tex_coord, normal, tangent))
        {
            local_end = position;
            if (intersection)
            {
                *intersection = position;
            }
            drawable = mObjects[i]->mDrawable;
        }
    }

    return drawable;
}

void drawBoxOutline(const LLVector4a& pos, const LLVector4a& size);

extern LLVector4a       gDebugRaycastStart;
extern LLVector4a       gDebugRaycastEnd;

void renderOctreeRaycast(const LLVector4a& start, const LLVector4a& end, const LLVolumeOctree* octree);

void renderAssetDebug(LLViewerObject* obj, Asset* asset)
{
    // render debug
    // assumes appropriate shader is already bound
    // assumes modelview matrix is already set

    gGL.pushMatrix();
    // get raycast in asset space
    LLMatrix4a agent_to_asset = obj->getAgentToGLTFAssetTransform();

    gGL.multMatrix(agent_to_asset.getF32ptr());

    vec4 start;
    vec4 end;

    LLVector4a t;
    agent_to_asset.affineTransform(gDebugRaycastStart, t);
    start = glm::make_vec4(t.getF32ptr());
    agent_to_asset.affineTransform(gDebugRaycastEnd, t);
    end = glm::make_vec4(t.getF32ptr());

    start.w = end.w = 1.0;

    for (auto& node : asset->mNodes)
    {
        Mesh& mesh = asset->mMeshes[node.mMesh];

        if (node.mMesh != INVALID_INDEX)
        {
            gGL.pushMatrix();
            gGL.multMatrix((F32*)glm::value_ptr(node.mAssetMatrix));

            // draw bounding box of mesh primitives
            if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_BBOXES))
            {
                gGL.color3f(0.f, 1.f, 1.f);

                for (auto& primitive : mesh.mPrimitives)
                {
                    auto* listener = (LLVolumeOctreeListener*) primitive.mOctree->getListener(0);

                    LLVector4a center = listener->mBounds[0];
                    LLVector4a size = listener->mBounds[1];

                    drawBoxOutline(center, size);
                }
            }

#if 1
            if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_RAYCAST))
            {
                gGL.flush();
                glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

                // convert raycast to node local space
                vec4 local_start = node.mAssetMatrixInv * start;
                vec4 local_end = node.mAssetMatrixInv * end;

                for (auto& primitive : mesh.mPrimitives)
                {
                    if (primitive.mOctree.notNull())
                    {
                        LLVector4a s, e;
                        s.load3(glm::value_ptr(local_start));
                        e.load3(glm::value_ptr(local_end));
                        renderOctreeRaycast(s, e, primitive.mOctree);
                    }
                }

                gGL.flush();
                glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
            }
#endif
            gGL.popMatrix();
        }
    }

    gGL.popMatrix();
}

void GLTFSceneManager::renderDebug()
{
    if (!gPipeline.hasRenderDebugMask(
        LLPipeline::RENDER_DEBUG_BBOXES |
        LLPipeline::RENDER_DEBUG_RAYCAST |
        LLPipeline::RENDER_DEBUG_NODES))
    {
        return;
    }

    gDebugProgram.bind();

    gGL.pushMatrix();
    gGL.loadMatrix(gGLModelView);

    LLGLDisable cullface(GL_CULL_FACE);
    LLGLEnable blend(GL_BLEND);
    gGL.setSceneBlendType(LLRender::BT_ALPHA);
    gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE);
    gPipeline.disableLights();

    for (auto& obj : mObjects)
    {
        if (obj->isDead() || obj->mGLTFAsset == nullptr)
        {
            continue;
        }

        Asset* asset = obj->mGLTFAsset.get();

        renderAssetDebug(obj, asset);
    }

    if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_NODES))
    { //render node hierarchy

        for (U32 i = 0; i < 2; ++i)
        {
            LLGLDepthTest depth(GL_TRUE, i == 0 ? GL_FALSE : GL_TRUE, i == 0 ? GL_GREATER : GL_LEQUAL);
            LLGLState blend(GL_BLEND, i == 0 ? GL_TRUE : GL_FALSE);

            for (auto& obj : mObjects)
            {
                if (obj->isDead() || obj->mGLTFAsset == nullptr)
                {
                    continue;
                }

                gGL.pushMatrix();

                gGL.multMatrix(obj->getGLTFAssetToAgentTransform().getF32ptr());

                Asset* asset = obj->mGLTFAsset.get();

                for (auto& node : asset->mNodes)
                {
                    gGL.pushMatrix();
                    gGL.multMatrix(glm::value_ptr(node.mAssetMatrix));
                    // render x-axis red, y-axis green, z-axis blue
                    gGL.color4f(1.f, 0.f, 0.f, 0.5f);
                    gGL.begin(LLRender::LINES);
                    gGL.vertex3f(0.f, 0.f, 0.f);
                    gGL.vertex3f(1.f, 0.f, 0.f);
                    gGL.end();
                    gGL.flush();

                    gGL.color4f(0.f, 1.f, 0.f, 0.5f);
                    gGL.begin(LLRender::LINES);
                    gGL.vertex3f(0.f, 0.f, 0.f);
                    gGL.vertex3f(0.f, 1.f, 0.f);
                    gGL.end();
                    gGL.flush();

                    gGL.begin(LLRender::LINES);
                    gGL.color4f(0.f, 0.f, 1.f, 0.5f);
                    gGL.vertex3f(0.f, 0.f, 0.f);
                    gGL.vertex3f(0.f, 0.f, 1.f);
                    gGL.end();
                    gGL.flush();

                    // render path to child nodes cyan
                    gGL.color4f(0.f, 1.f, 1.f, 0.5f);
                    gGL.begin(LLRender::LINES);
                    for (auto& child_idx : node.mChildren)
                    {
                        Node& child = asset->mNodes[child_idx];
                        gGL.vertex3f(0.f, 0.f, 0.f);


                        gGL.vertex3fv(glm::value_ptr(child.mMatrix[3]));
                    }
                    gGL.end();
                    gGL.flush();
                    gGL.popMatrix();
                }

                gGL.popMatrix();
            }
        }
    }


    if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_RAYCAST))
    {
        S32 node_hit = -1;
        S32 primitive_hit = -1;
        LLVector4a intersection;

        LLDrawable* drawable = lineSegmentIntersect(gDebugRaycastStart, gDebugRaycastEnd, true, true, true, true, &node_hit, &primitive_hit, &intersection, nullptr, nullptr, nullptr);

        if (drawable)
        {

            LLViewerObject* obj = drawable->getVObj();
            if (obj)
            {
                gGL.pushMatrix();
                gGL.multMatrix(obj->getGLTFAssetToAgentTransform().getF32ptr());
                Asset* asset = obj->mGLTFAsset.get();
                Node* node = &asset->mNodes[node_hit];
                Primitive* primitive = &asset->mMeshes[node->mMesh].mPrimitives[primitive_hit];

                gGL.flush();
                glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
                gGL.color3f(1, 0, 1);
                drawBoxOutline(intersection, LLVector4a(0.1f, 0.1f, 0.1f, 0.f));

                gGL.multMatrix(glm::value_ptr(node->mAssetMatrix));

                auto* listener = (LLVolumeOctreeListener*)primitive->mOctree->getListener(0);
                drawBoxOutline(listener->mBounds[0], listener->mBounds[1]);

                gGL.flush();
                glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
                gGL.popMatrix();
            }
        }
    }

    gGL.popMatrix();
    gDebugProgram.unbind();

}