//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#include <MaterialXView/RenderPipelineGL.h>
#include <MaterialXView/Viewer.h>
#include <MaterialXRenderGlsl/GLTextureHandler.h>

#include <MaterialXGenGlsl/GlslShaderGenerator.h>
#include <MaterialXRenderGlsl/TextureBaker.h>
#include <MaterialXRenderGlsl/GLFramebuffer.h>
#include <MaterialXRenderGlsl/GlslMaterial.h>

#include <nanogui/messagedialog.h>
#include <nanogui/opengl.h>

namespace
{

const float PI = std::acos(-1.0f);

}

GLRenderPipeline::GLRenderPipeline(Viewer* viewerPtr) :
    RenderPipeline(viewerPtr)
{
}

void GLRenderPipeline::initialize(void*, void*)
{
}

mx::ImageHandlerPtr GLRenderPipeline::createImageHandler()
{
    return mx::GLTextureHandler::create(mx::StbImageLoader::create());
}

mx::MaterialPtr GLRenderPipeline::createMaterial()
{
    return mx::GlslMaterial::create();
}

std::shared_ptr<void> GLRenderPipeline::createTextureBaker(unsigned int width,
                                                           unsigned int height,
                                                           mx::Image::BaseType baseType)
{
    return std::static_pointer_cast<void>(mx::TextureBakerGlsl::create(width, height, baseType));
}

void GLRenderPipeline::initFramebuffer(int, int, void*)
{
}

void GLRenderPipeline::resizeFramebuffer(int, int, void*)
{
}

void GLRenderPipeline::updateAlbedoTable(int tableSize)
{
    auto& genContext    = _viewer->_genContext;
    auto& stdLib        = _viewer->_stdLib;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;
    
    if (lightHandler->getAlbedoTable())
    {
        return;
    }

    // Create framebuffer.
    mx::GLFramebufferPtr framebuffer = mx::GLFramebuffer::create(tableSize, tableSize, 3, mx::Image::BaseType::FLOAT);
    framebuffer->bind();
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    // Create shader.
    mx::ShaderPtr hwShader = mx::createAlbedoTableShader(genContext, stdLib, "__ALBEDO_TABLE_SHADER__");
    mx::GlslMaterialPtr material = mx::GlslMaterial::create();
    try
    {
        material->generateShader(hwShader);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to generate albedo table shader", e.what());
        return;
    }

    // Render albedo table.
    material->bindShader();
    if (material->getProgram()->hasUniform(mx::HW::ALBEDO_TABLE_SIZE))
    {
        std::static_pointer_cast<mx::GlslMaterial>(material)->getProgram()
            ->bindUniform(mx::HW::ALBEDO_TABLE_SIZE, mx::Value::createValue(tableSize));
    }
    _viewer->renderScreenSpaceQuad(material);

    // Store albedo table image.
    imageHandler->releaseRenderResources(lightHandler->getAlbedoTable());
    lightHandler->setAlbedoTable(framebuffer->getColorImage());
    if (_viewer->_saveGeneratedLights)
    {
        imageHandler->saveImage("AlbedoTable.exr", lightHandler->getAlbedoTable());
    }

    // Restore state for scene rendering.
    glViewport(0, 0, _viewer->m_fbsize[0], _viewer->m_fbsize[1]);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glDrawBuffer(GL_BACK);
}

void GLRenderPipeline::updatePrefilteredMap()
{
    auto& genContext    = _viewer->_genContext;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;

    if (lightHandler->getEnvPrefilteredMap())
    {
        return;
    }

    // Create the prefilter shader.
    mx::GlslMaterialPtr material = nullptr;
    try
    {
        mx::ShaderPtr hwShader = mx::createEnvPrefilterShader(genContext, _viewer->_stdLib, "__ENV_PREFILTER__");
        material = mx::GlslMaterial::create();
        material->generateShader(hwShader);
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to generate prefilter shader", e.what());
    }

    mx::ImagePtr srcTex = lightHandler->getEnvRadianceMap();

    int w = srcTex->getWidth();
    int h = srcTex->getHeight();
    int numMips = srcTex->getMaxMipCount();

    // Create texture to hold the prefiltered environment.
    mx::GLTextureHandlerPtr glImageHandler = std::dynamic_pointer_cast<mx::GLTextureHandler>(imageHandler);
    mx::ImagePtr outTex = mx::Image::create(w, h, 3, mx::Image::BaseType::HALF);
    glImageHandler->createRenderResources(outTex, true, true);

	mx::GlslProgramPtr program = material->getProgram();

    try
    {
        int i = 0;
        while (w > 0 && h > 0)
        {
            // Create framebuffer
            unsigned int framebuffer;
            glGenFramebuffers(1, &framebuffer);
            glBindFramebuffer(GL_FRAMEBUFFER, framebuffer); 
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, outTex->getResourceId(), i);
            glViewport(0, 0, w, h);
            material->bindShader();

            // Bind the source texture
            mx::ImageSamplingProperties samplingProperties;
            samplingProperties.uaddressMode = mx::ImageSamplingProperties::AddressMode::PERIODIC;
            samplingProperties.vaddressMode = mx::ImageSamplingProperties::AddressMode::CLAMP;
            samplingProperties.filterType = mx::ImageSamplingProperties::FilterType::LINEAR;
            imageHandler->bindImage(srcTex, samplingProperties);
            int textureLocation = glImageHandler->getBoundTextureLocation(srcTex->getResourceId());
            assert(textureLocation >= 0);
            program->bindUniform(mx::HW::ENV_RADIANCE, mx::Value::createValue(textureLocation));
            // Bind other uniforms
            program->bindUniform(mx::HW::ENV_PREFILTER_MIP, mx::Value::createValue(i));
            const mx::Matrix44 yRotationPI = mx::Matrix44::createScale(mx::Vector3(-1, 1, -1));
            program->bindUniform(mx::HW::ENV_MATRIX, mx::Value::createValue(yRotationPI));
            program->bindUniform(mx::HW::ENV_RADIANCE_MIPS, mx::Value::createValue<int>(numMips));

            _viewer->renderScreenSpaceQuad(material);

            glDeleteFramebuffers(1, &framebuffer); 

            w /= 2;
            h /= 2;
            i++;
        }
    }
    catch (mx::ExceptionRenderError& e)
    {
        for (const std::string& error : e.errorLog())
        {
            std::cerr << error << std::endl;
        }
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to render prefiltered environment", e.what());
    }
    catch (std::exception& e)
    {
        new ng::MessageDialog(_viewer, ng::MessageDialog::Type::Warning, "Failed to render prefiltered environment", e.what());
    }

    // Clean up.
    glViewport(0, 0, _viewer->m_fbsize[0], _viewer->m_fbsize[1]);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

    lightHandler->setEnvPrefilteredMap(outTex);
}

mx::ImagePtr GLRenderPipeline::getShadowMap(int shadowMapSize)
{
    auto& genContext      = _viewer->_genContext;
    auto& imageHandler    = _viewer->_imageHandler;
    auto& shadowCamera    = _viewer->_shadowCamera;
    auto& stdLib          = _viewer->_stdLib;
    auto& geometryHandler = _viewer->_geometryHandler;
    
    if (!_viewer->_shadowMap)
    {
        // Generate shaders for shadow rendering.
        if (!_viewer->_shadowMaterial)
        {
            try
            {
                mx::ShaderPtr hwShader = mx::createDepthShader(genContext, stdLib, "__SHADOW_SHADER__");
                _viewer->_shadowMaterial = mx::GlslMaterial::create();
                _viewer->_shadowMaterial->generateShader(hwShader);
            }
            catch (std::exception& e)
            {
                std::cerr << "Failed to generate shadow shader: " << e.what() << std::endl;
                _viewer->_shadowMaterial = nullptr;
            }
        }
        if (!_viewer->_shadowBlurMaterial)
        {
            try
            {
                mx::ShaderPtr hwShader = mx::createBlurShader(genContext, stdLib, "__SHADOW_BLUR_SHADER__", "gaussian", 1.0f);
                _viewer->_shadowBlurMaterial = mx::GlslMaterial::create();
                _viewer->_shadowBlurMaterial->generateShader(hwShader);
            }
            catch (std::exception& e)
            {
                std::cerr << "Failed to generate shadow blur shader: " << e.what() << std::endl;
                _viewer->_shadowBlurMaterial = nullptr;
            }
        }

        if (_viewer->_shadowMaterial && _viewer->_shadowBlurMaterial)
        {
            // Create framebuffer.
            mx::GLFramebufferPtr framebuffer = mx::GLFramebuffer::create(shadowMapSize, shadowMapSize, 2, mx::Image::BaseType::FLOAT);
            framebuffer->bind();
            glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

            // Render shadow geometry.
            _viewer->_shadowMaterial->bindShader();
            for (auto mesh : geometryHandler->getMeshes())
            {
                _viewer->_shadowMaterial->bindMesh(mesh);
                _viewer->_shadowMaterial->bindViewInformation(shadowCamera);
                for (size_t i = 0; i < mesh->getPartitionCount(); i++)
                {
                    mx::MeshPartitionPtr geom = mesh->getPartition(i);
                    _viewer->_shadowMaterial->drawPartition(geom);
                }
            }
            _viewer->_shadowMap = framebuffer->getColorImage();

            // Apply Gaussian blurring.
            mx::ImageSamplingProperties blurSamplingProperties;
            blurSamplingProperties.uaddressMode = mx::ImageSamplingProperties::AddressMode::CLAMP;
            blurSamplingProperties.vaddressMode = mx::ImageSamplingProperties::AddressMode::CLAMP;
            blurSamplingProperties.filterType = mx::ImageSamplingProperties::FilterType::CLOSEST;
            for (unsigned int i = 0; i < _viewer->_shadowSoftness; i++)
            {
                framebuffer->bind();
                _viewer->_shadowBlurMaterial->bindShader();
                if (imageHandler->bindImage(_viewer->_shadowMap, blurSamplingProperties))
                {
                    mx::GLTextureHandlerPtr textureHandler = std::static_pointer_cast<mx::GLTextureHandler>(imageHandler);
                    int textureLocation = textureHandler->getBoundTextureLocation(_viewer->_shadowMap->getResourceId());
                    if (textureLocation >= 0)
                    {
                        std::static_pointer_cast<mx::GlslMaterial>(_viewer->_shadowBlurMaterial)
                            ->getProgram()->bindUniform("image_file", mx::Value::createValue(textureLocation));
                    }
                }
                glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
                _viewer->renderScreenSpaceQuad(_viewer->_shadowBlurMaterial);
                imageHandler->releaseRenderResources(_viewer->_shadowMap);
                _viewer->_shadowMap = framebuffer->getColorImage();
            }

            // Restore state for scene rendering.
            glViewport(0, 0, _viewer->m_fbsize[0], _viewer->m_fbsize[1]);
            glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
            glDrawBuffer(GL_BACK);
        }

        // Reset frame timing after shadow generation.
        _viewer->resetFrameTiming();
    }

    return _viewer->_shadowMap;
}


void GLRenderPipeline::renderFrame(void*, int shadowMapSize, const char* dirLightNodeCat)
{
    auto& genContext    = _viewer->_genContext;
    auto& lightHandler  = _viewer->_lightHandler;
    auto& imageHandler  = _viewer->_imageHandler;
    auto& viewCamera    = _viewer->_viewCamera;
    auto& envCamera     = _viewer->_envCamera;
    auto& shadowCamera  = _viewer->_shadowCamera;
    float lightRotation = _viewer->_lightRotation;
    auto& searchPath    = _viewer->_searchPath;
    auto& geometryHandler    = _viewer->_geometryHandler;

    // Update prefiltered environment.
    if (lightHandler->getUsePrefilteredMap() && !_viewer->_materialAssignments.empty())
    {
        updatePrefilteredMap();
    }

    // Initialize OpenGL state
    glDisable(GL_BLEND);
    glEnable(GL_DEPTH_TEST);
    glDepthMask(GL_TRUE);
    glDepthFunc(GL_LEQUAL);
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    glDisable(GL_CULL_FACE);
    glDisable(GL_FRAMEBUFFER_SRGB);

    // Update lighting state.
    lightHandler->setLightTransform(mx::Matrix44::createRotationY(lightRotation / 180.0f * PI));

    // Update shadow state.
    mx::ShadowState shadowState;
    shadowState.ambientOcclusionGain = _viewer->_ambientOcclusionGain;
    mx::NodePtr dirLight = lightHandler->getFirstLightOfCategory(dirLightNodeCat);
    if (genContext.getOptions().hwShadowMap && dirLight)
    {
        mx::ImagePtr shadowMap = getShadowMap(shadowMapSize);
        if (shadowMap)
        {
            shadowState.shadowMap = shadowMap;
            shadowState.shadowMatrix = viewCamera->getWorldMatrix().getInverse() *
                shadowCamera->getWorldViewProjMatrix();
        }
        else
        {
            genContext.getOptions().hwShadowMap = false;
        }
    }

    glEnable(GL_FRAMEBUFFER_SRGB);

    // Environment background
    if (_viewer->_drawEnvironment)
    {
        mx::MaterialPtr envMaterial = _viewer->getEnvironmentMaterial();
        if (envMaterial)
        {
            const mx::MeshList& meshes = _viewer->_envGeometryHandler->getMeshes();
            mx::MeshPartitionPtr envPart = !meshes.empty() ? meshes[0]->getPartition(0) : nullptr;
            if (envPart)
            {
                // Apply rotation to the environment shader.
                envMaterial->modifyUniform("envImage/rotation", mx::Value::createValue(lightRotation));

                // Apply light intensity to the environment shader.
                envMaterial->modifyUniform("envImageAdjusted/in2", mx::Value::createValue(lightHandler->getEnvLightIntensity()));

                // Render the environment mesh.
                glDepthMask(GL_FALSE);
                envMaterial->bindShader();
                envMaterial->bindMesh(meshes[0]);
                envMaterial->bindViewInformation(envCamera);
                envMaterial->bindImages(imageHandler, searchPath, false);
                envMaterial->drawPartition(envPart);
                glDepthMask(GL_TRUE);
            }
        }
        else
        {
            _viewer->_drawEnvironment = false;
        }
    }

    // Enable backface culling if requested.
    if (!_viewer->_renderDoubleSided)
    {
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
    }

    // Opaque pass
    for (const auto& assignment : _viewer->_materialAssignments)
    {
        mx::MeshPartitionPtr geom = assignment.first;
        mx::GlslMaterialPtr material = std::dynamic_pointer_cast<mx::GlslMaterial>(assignment.second);
        shadowState.ambientOcclusionMap = _viewer->getAmbientOcclusionImage(material);
        if (!material)
        {
            continue;
        }

        material->bindShader();
        material->bindMesh(geometryHandler->findParentMesh(geom));
        if (material->getProgram()->hasUniform(mx::HW::ALPHA_THRESHOLD))
        {
            material->getProgram()->bindUniform(mx::HW::ALPHA_THRESHOLD, mx::Value::createValue(0.99f));
        }
        material->getProgram()->bindTimeAndFrame((float) _timer.elapsedTime(), (float) _frame);
        material->bindViewInformation(viewCamera);
        material->bindLighting(lightHandler, imageHandler, shadowState);
        material->bindImages(imageHandler, searchPath);
        material->drawPartition(geom);
        material->unbindImages(imageHandler);
    }

    // Transparent pass
    if (_viewer->_renderTransparency)
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        for (const auto& assignment : _viewer->_materialAssignments)
        {
            mx::MeshPartitionPtr geom = assignment.first;
            mx::GlslMaterialPtr material = std::dynamic_pointer_cast<mx::GlslMaterial>(assignment.second);
            shadowState.ambientOcclusionMap = _viewer->getAmbientOcclusionImage(material);
            if (!material || !material->hasTransparency())
            {
                continue;
            }

            material->bindShader();
            material->bindMesh(geometryHandler->findParentMesh(geom));
            if (material->getProgram()->hasUniform(mx::HW::ALPHA_THRESHOLD))
            {
                material->getProgram()->bindUniform(mx::HW::ALPHA_THRESHOLD, mx::Value::createValue(0.001f));
            }
            material->getProgram()->bindTimeAndFrame((float) _timer.elapsedTime(), (float) _frame);
            material->bindViewInformation(viewCamera);
            material->bindLighting(lightHandler, imageHandler, shadowState);
            material->bindImages(imageHandler, searchPath);
            material->drawPartition(geom);
            material->unbindImages(imageHandler);
        }
        glDisable(GL_BLEND);
    }

    if (!_viewer->_renderDoubleSided)
    {
        glDisable(GL_CULL_FACE);
    }
    glDisable(GL_FRAMEBUFFER_SRGB);

    // Wireframe pass
    if (_viewer->_outlineSelection)
    {
        mx::MaterialPtr wireMaterial = _viewer->getWireframeMaterial();
        if (wireMaterial)
        {
            glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
            wireMaterial->bindShader();
            wireMaterial->bindMesh(geometryHandler->findParentMesh(_viewer->getSelectedGeometry()));
            wireMaterial->bindViewInformation(viewCamera);
            wireMaterial->drawPartition(_viewer->getSelectedGeometry());
            glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
        }
        else
        {
            _viewer->_outlineSelection = false;
        }
    }
}

void GLRenderPipeline::bakeTextures()
{
    auto& imageHandler = _viewer->_imageHandler;

    mx::MaterialPtr material = _viewer->getSelectedMaterial();
    mx::DocumentPtr doc = material ? material->getDocument() : nullptr;
    if (!doc)
    {
        return;
    }

    {
        // Construct a texture baker.
        mx::Image::BaseType baseType = _viewer->_bakeHdr ? mx::Image::BaseType::FLOAT : mx::Image::BaseType::UINT8;
        mx::UnsignedIntPair bakingRes = _viewer->computeBakingResolution(doc);
        mx::TextureBakerPtr baker = std::static_pointer_cast<mx::TextureBakerPtr::element_type>(createTextureBaker(bakingRes.first, bakingRes.second, baseType));
        baker->setupUnitSystem(_viewer->_stdLib);
        baker->setDistanceUnit(_viewer->_genContext.getOptions().targetDistanceUnit);
        baker->setAverageImages(_viewer->_bakeAverage);
        baker->setOptimizeConstants(_viewer->_bakeOptimize);
        baker->writeDocumentPerMaterial(_viewer->_bakeDocumentPerMaterial);

        // Assign our existing image handler, releasing any existing render resources for cached images.
        imageHandler->releaseRenderResources();
        baker->setImageHandler(imageHandler);

        // Extend the image search path to include material source folders.
        mx::FileSearchPath extendedSearchPath = _viewer->_searchPath;
        extendedSearchPath.append(_viewer->_materialSearchPath);

        // Bake all materials in the active document.
        try
        {
            baker->bakeAllMaterials(doc, extendedSearchPath, _viewer->_bakeFilename);
        }
        catch (std::exception& e)
        {
            std::cerr << "Error in texture baking: " << e.what() << std::endl;
        }

        // Release any render resources generated by the baking process.
        imageHandler->releaseRenderResources();
    }
    
    // After the baker has been destructed, restore state for scene rendering.
    glfwMakeContextCurrent(_viewer->m_glfw_window);
    glfwGetFramebufferSize(_viewer->m_glfw_window, &_viewer->m_fbsize[0], &_viewer->m_fbsize[1]);
    glViewport(0, 0, _viewer->m_fbsize[0], _viewer->m_fbsize[1]);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glDrawBuffer(GL_BACK);
}

mx::ImagePtr GLRenderPipeline::getFrameImage()
{
    glFlush();
    
    const auto& size = _viewer->m_size;
    const auto& pixel_ratio = _viewer->m_pixel_ratio;

    // Create an image with dimensions adjusted for device DPI.
    mx::ImagePtr image = mx::Image::create((unsigned int) (size.x() * pixel_ratio),
                                           (unsigned int) (size.y() * pixel_ratio), 3);
    image->createResourceBuffer();

    // Read pixels into the image buffer.
    glReadPixels(0, 0, image->getWidth(), image->getHeight(), GL_RGB, GL_UNSIGNED_BYTE, image->getResourceBuffer());

    return image;
}
