/**
 * @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"


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,
            true);
    }
    else
    {
        LLNotificationsUtil::add("GLTFPreviewSelection");
    }
}

void GLTFSceneManager::load(const std::string& filename)
{
    tinygltf::Model model;
    LLTinyGLTFHelper::loadModel(filename, model);

    LLPointer<Asset> asset = new Asset();
    *asset = model;

    gDebugProgram.bind(); // bind a shader to satisfy LLVertexBuffer assertions
    asset->allocateGLResources(filename, model);
    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;

        if (std::find(mObjects.begin(), mObjects.end(), obj) == mObjects.end())
        {
            mObjects.push_back(obj);
        }
    }
}

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

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

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

void GLTFSceneManager::update()
{
    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;

        asset->update();

    }
}

void GLTFSceneManager::render(bool opaque, bool rigged)
{
    // for debugging, just render the whole scene as opaque
    // by traversing the whole scenegraph
    // Assumes camera transform is already set and
    // appropriate shader is already bound

    gGL.matrixMode(LLRender::MM_MODELVIEW);

    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;

        gGL.pushMatrix();

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

        LLMatrix4a modelview;
        modelview.loadu(gGLModelView);

        matMul(mat, modelview, modelview);

        asset->updateRenderTransforms(modelview);
        asset->render(opaque, rigged);

        gGL.popMatrix();
    }
}

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, 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();

    LLVector4a start;
    LLVector4a end;

    agent_to_asset.affineTransform(gDebugRaycastStart, start);
    agent_to_asset.affineTransform(gDebugRaycastEnd, end);


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

        if (node.mMesh != INVALID_INDEX)
        {
            gGL.loadMatrix((F32*)node.mRenderMatrix.mMatrix);

            // 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 0
            if (gPipeline.hasRenderDebugMask(LLPipeline::RENDER_DEBUG_RAYCAST))
            {
                gGL.flush();
                glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

                // convert raycast to node local space
                LLVector4a local_start;
                LLVector4a local_end;

                node.mAssetMatrixInv.affineTransform(start, local_start);
                node.mAssetMatrixInv.affineTransform(end, local_end);

                for (auto& primitive : mesh.mPrimitives)
                {
                    if (primitive.mOctree.notNull())
                    {
                        renderOctreeRaycast(local_start, local_end, primitive.mOctree);
                    }
                }

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

    gGL.popMatrix();
}

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

    gDebugProgram.bind();

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

    // force update all mRenderMatrix, not just nodes with meshes
    for (auto& obj : mObjects)
    {
        if (obj->isDead() || obj->mGLTFAsset == nullptr)
        {
            continue;
        }

        LLMatrix4a mat = obj->getGLTFAssetToAgentTransform();

        LLMatrix4a modelview;
        modelview.loadu(gGLModelView);

        matMul(mat, modelview, modelview);

        Asset* asset = obj->mGLTFAsset;

        for (auto& node : asset->mNodes)
        {
            matMul(node.mAssetMatrix, modelview, node.mRenderMatrix);
        }
    }

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

        Asset* asset = obj->mGLTFAsset;

        LLMatrix4a mat = obj->getGLTFAssetToAgentTransform();

        LLMatrix4a modelview;
        modelview.loadu(gGLModelView);

        matMul(mat, modelview, modelview);

        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 ? TRUE : FALSE);


            gGL.pushMatrix();

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

                LLMatrix4a mat = obj->getGLTFAssetToAgentTransform();

                LLMatrix4a modelview;
                modelview.loadu(gGLModelView);

                matMul(mat, modelview, modelview);

                Asset* asset = obj->mGLTFAsset;

                for (auto& node : asset->mNodes)
                {
                    // force update all mRenderMatrix, not just nodes with meshes
                    matMul(node.mAssetMatrix, modelview, node.mRenderMatrix);

                    gGL.loadMatrix(node.mRenderMatrix.getF32ptr());
                    // 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(child.mMatrix.getTranslation().getF32ptr());
                    }
                    gGL.end();
                    gGL.flush();
                }
            }

            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)
        {
            gGL.pushMatrix();
            Asset* asset = drawable->getVObj()->mGLTFAsset;
            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.loadMatrix((F32*) node->mRenderMatrix.mMatrix);



            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();
        }
    }
    gDebugProgram.unbind();
}