/**
   @file
   @author Shin'ichiro Nakaoka
*/

#include "ParticlesProgram.h"
#include <cnoid/EigenUtil>
#include <cnoid/SceneCameras>
#include <cnoid/GLSLProgram>
#include <cnoid/GLSLSceneRenderer>
#include <QImage>

using namespace std;
using namespace cnoid;


ParticlesProgramBase::ParticlesProgramBase(GLSLSceneRenderer* renderer)
    : renderer_(renderer)
{
    
    initializationState = NOT_INITIALIZED;
}


bool ParticlesProgramBase::initializeRendering(SceneParticles* particles)
{
    auto program = shaderProgram();

    program->initialize();

    auto& glsl = program->glslProgram();
    modelViewMatrixLocation = glsl.getUniformLocation("modelViewMatrix");
    projectionMatrixLocation = glsl.getUniformLocation("projectionMatrix");
    pointSizeLocation = glsl.getUniformLocation("pointSize");
    angle2pixelsLocation = glsl.getUniformLocation("angle2pixels");
    
    timeLocation = glsl.getUniformLocation("time");
    particleTexLocation = glsl.getUniformLocation("particleTex");

    if(!particles->texture().empty()){
        QImage image(particles->texture().c_str());
        QImage texture = image.rgbSwapped();
        glActiveTexture(GL_TEXTURE0);
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture.width(), texture.height(),
                     0, GL_RGBA, GL_UNSIGNED_BYTE, texture.constBits());
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }

    return true;
}


void ParticlesProgramBase::requestRendering
(SceneParticles* particles, const std::function<void()>& renderingFunction)
{
    if(renderer_->isRenderingPickingImage()){
        return;
    }

    if(initializationState != INITIALIZED){
        if(initializationState == FAILED){
            return;
        } else {
            if(initializeRendering(particles)){
                initializationState = INITIALIZED;
            } else {
                initializationState = FAILED;
                return;
            }
        }
    }

    renderer_->dispatchToTransparentPhase(
        particles, 0,
        [this, renderingFunction](Referenced* object, const Affine3& position, int /* id */){
            render(static_cast<SceneParticles*>(object), position, renderingFunction); });
}

float ParticlesProgramBase::frandom(float max)
{
    floatDistribution.param(FloatDistribution::param_type(0.0f, max));
    return floatDistribution(randomNumberGenerator);
}

void ParticlesProgramBase::render
(SceneParticles* particles, const Affine3& position, const std::function<void()>& renderingFunction)
{
    ShaderProgram* program = shaderProgram();
    
    renderer_->pushShaderProgram(program);

    auto lightingProgram = dynamic_cast<BasicLightingProgram*>(program);
    if(lightingProgram){
        renderer_->renderLights(lightingProgram);
        renderer_->renderFog(lightingProgram);
    }

    const Matrix4f MV = (renderer_->viewTransform() * position).cast<float>().matrix();
    glUniformMatrix4fv(modelViewMatrixLocation, 1, GL_FALSE, MV.data());
    const Matrix4f P = renderer_->projectionMatrix().cast<float>();
    glUniformMatrix4fv(projectionMatrixLocation, 1, GL_FALSE, P.data());

    auto& vp = renderer_->viewport();
    SgCamera* camera = renderer_->currentCamera();
    auto ps = particles->getParticleSystem();
    
    if(SgPerspectiveCamera* persCamera = dynamic_cast<SgPerspectiveCamera*>(camera)){
        glUniform1f(angle2pixelsLocation, vp.h / renderer_->getEffectiveFovy(persCamera));
        glUniform1f(pointSizeLocation, ps->particleSize());
    } else if(SgOrthographicCamera* orthoCamera = dynamic_cast<SgOrthographicCamera*>(camera)){
        //float size = 0.08f * height / orthoCamera->height();
        glUniform1f(pointSizeLocation, -ps->particleSize());
    }

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, textureId);
    glBindSampler(0, 0);
    
    glUniform1i(particleTexLocation, 0);

    globalAttitude_ = position.linear().cast<float>();

    // If particle size varies with MSAA level, try enabling the following code.
    // Note: This may not work for 16x MSAA due to driver limitations.
    //glDisable(GL_MULTISAMPLE);
    renderingFunction();
    //glEnable(GL_MULTISAMPLE);

    renderer_->popShaderProgram();
}


ParticlesProgram::ParticlesProgram
(GLSLSceneRenderer* renderer, const char* vertexShader, const char* fragmentShader)
    : BasicLightingProgram(
        { { vertexShader, GL_VERTEX_SHADER },
          { fragmentShader, GL_FRAGMENT_SHADER } }),
      ParticlesProgramBase(renderer)
{

}


LuminousParticlesProgram::LuminousParticlesProgram
(GLSLSceneRenderer* renderer, const char* vertexShader, const char* fragmentShader)
    : ShaderProgram(
        { { vertexShader, GL_VERTEX_SHADER },
          { fragmentShader, GL_FRAGMENT_SHADER } }),
      ParticlesProgramBase(renderer)
{

}
