 //  SuperTuxKart - a fun racing game with go-kart
//  Copyright (C) 2015 SuperTuxKart-Team
//
//  This program is free software; you can redistribute it and/or
//  modify it under the terms of the GNU General Public License
//  as published by the Free Software Foundation; either version 3
//  of the License, or (at your option) any later version.
//
//  This program 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 General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

#ifndef SERVER_ONLY

#include "graphics/skybox.hpp"
#include "config/stk_config.hpp"
#include "graphics/central_settings.hpp"
#include "graphics/irr_driver.hpp"
#include "graphics/sp/sp_base.hpp"
#include "graphics/texture_shader.hpp"

#include <ge_main.hpp>

#include <algorithm>
#include <cassert>

#include <IImage.h>

#include <squish.h>
extern "C"
{
    #include <mipmap/img.h>
    #include <mipmap/imgresize.h>
}

using namespace irr;


class SkyboxShader : public TextureShader<SkyboxShader,1>
{
private:
    GLuint m_vao;

public:
    SkyboxShader()
    {
        loadProgram(OBJECT, GL_VERTEX_SHADER, "sky.vert",
                            GL_FRAGMENT_SHADER, "sky.frag");
        assignUniforms();
        assignSamplerNames(0, "tex", ST_TRILINEAR_CUBEMAP);

        glGenVertexArrays(1, &m_vao);
        glBindVertexArray(m_vao);
        glBindBuffer(GL_ARRAY_BUFFER, SharedGPUObjects::getSkyTriVBO());
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), 0);
        glBindVertexArray(0);
    }   // SkyboxShader
    // ------------------------------------------------------------------------
    void bindVertexArray()
    {
        glBindVertexArray(m_vao);
    }   // bindVertexArray
};   // SkyboxShader

#if !defined(USE_GLES2)
class SpecularIBLGenerator : public TextureShader<SpecularIBLGenerator, 2,
                                                  core::matrix4, float >
{
public:
    SpecularIBLGenerator()
    {
        loadProgram(OBJECT, GL_VERTEX_SHADER,   "screenquad.vert",
                            GL_FRAGMENT_SHADER, "importance_sampling_specular.frag");
        assignUniforms("PermutationMatrix", "ViewportSize");
        assignSamplerNames(0, "tex", ST_TRILINEAR_CUBEMAP,
                           1, "samples", ST_TEXTURE_BUFFER);
    }
};   // SpecularIBLGenerator
#endif

namespace {
    // ----------------------------------------------------------------------------
    void swapPixels(char *old_img, char *new_img, unsigned stride, unsigned old_i,
                    unsigned old_j, unsigned new_i, unsigned new_j)
    {
        new_img[4 * (stride * new_i + new_j)] = old_img[4 * (stride * old_i + old_j)];
        new_img[4 * (stride * new_i + new_j) + 1] = old_img[4 * (stride * old_i + old_j) + 1];
        new_img[4 * (stride * new_i + new_j) + 2] = old_img[4 * (stride * old_i + old_j) + 2];
        new_img[4 * (stride * new_i + new_j) + 3] = old_img[4 * (stride * old_i + old_j) + 3];
    }   // swapPixels

    // ----------------------------------------------------------------------------
    // From http://http.developer.nvidia.com/GPUGems3/gpugems3_ch20.html
    /** Returns the index-th pair from Hammersley set of pseudo random set.
        Hammersley set is a uniform distribution between 0 and 1 for 2 components.
        We use the natural indexation on the set to avoid storing the whole set.
        \param index of the pair
        \param size of the set. */
    std::pair<float, float> getHammersleySequence(int index, int samples)
    {
        float InvertedBinaryRepresentation = 0.;
        for (size_t i = 0; i < 32; i++)
        {
            InvertedBinaryRepresentation += ((index >> i) & 0x1)
                                          * powf(.5, (float) (i + 1.));
        }
        return std::make_pair(float(index) / float(samples),
                              InvertedBinaryRepresentation);
    }   // HammersleySequence


    // ----------------------------------------------------------------------------
    /** Returns a pseudo random (theta, phi) generated from a probability density
    *  function modeled after Phong function.
    *  \param a pseudo random float pair from a uniform density function between
    *         0 and 1.
    *   \param exponent from the Phong formula.
    */
    std::pair<float, float> getImportanceSamplingPhong(std::pair<float, float> Seeds,
                                                       float exponent)
    {
        return std::make_pair(acosf(powf(Seeds.first, 1.f / (exponent + 1.f))),
                              2.f * 3.14f * Seeds.second);
    }   // getImportanceSamplingPhong

    // ----------------------------------------------------------------------------
    static core::matrix4 getPermutationMatrix(size_t indexX, float valX,
                                              size_t indexY, float valY,
                                              size_t indexZ, float valZ)
    {
        core::matrix4 result_mat;
        float *M = result_mat.pointer();
        memset(M, 0, 16 * sizeof(float));
        assert(indexX < 4);
        assert(indexY < 4);
        assert(indexZ < 4);
        M[indexX] = valX;
        M[4 + indexY] = valY;
        M[8 + indexZ] = valZ;
        return result_mat;
    }   // getPermutationMatrix

}  //namespace

// ============================================================================
extern "C" void squishCompressImage(uint8_t* rgba, int width, int height,
                                    int pitch, void* blocks, unsigned flags);
// ----------------------------------------------------------------------------
/** Generate an opengl cubemap texture from 6 2d textures */
void Skybox::generateCubeMapFromTextures()
{
    assert(m_skybox_textures.size() == 6);

    glGenTextures(1, &m_cube_map);

    unsigned size = 0;
    for (unsigned i = 0; i < 6; i++)
    {
        size = std::max(size, m_skybox_textures[i]->getDimension().Width);
        size = std::max(size, m_skybox_textures[i]->getDimension().Height);
    }

    const unsigned texture_permutation[] = { 2, 3, 0, 1, 5, 4 };
    char *rgba[6];
    for (unsigned i = 0; i < 6; i++)
        rgba[i] = new char[size * size * 4];
    for (unsigned i = 0; i < 6; i++)
    {
        unsigned idx = texture_permutation[i];
        video::IImage* img = m_skybox_textures[idx];

        bool is_gles = false;
#ifdef USE_GLES2
        is_gles = true;
#endif
        if (CVS->isTextureCompressionEnabled() || is_gles)
        {
            uint8_t* data = (uint8_t*)img->lock();
            for (unsigned int j = 0; j <
                img->getDimension().Width * img->getDimension().Height; j++)
            {
                uint8_t tmp_val = data[j * 4];
                data[j * 4] = data[j * 4 + 2];
                data[j * 4 + 2] = tmp_val;
            }
        }

        img->copyToScaling(rgba[i], size, size);

        if (i == 2 || i == 3)
        {
            char *tmp = new char[size * size * 4];
            memcpy(tmp, rgba[i], size * size * 4);
            for (unsigned x = 0; x < size; x++)
            {
                for (unsigned y = 0; y < size; y++)
                {
                    swapPixels(tmp, rgba[i], size, x, y, (size - y - 1), x);
                }
            }
            delete[] tmp;
        }
        img->drop();

        bool needs_srgb_format = CVS->isDeferredEnabled();
        glBindTexture(GL_TEXTURE_CUBE_MAP, m_cube_map);

        const unsigned tc_flag = squish::kDxt5 | stk_config->m_tc_quality;
        if (CVS->isTextureCompressionEnabled())
        {
            unsigned tex_size = GE::get4x4CompressedTextureSize(size, size);
            uint8_t* compressed = new uint8_t[tex_size];
            squishCompressImage((uint8_t*)rgba[i], size, size, size * 4,
                compressed, tc_flag);
            GLint internal_format = needs_srgb_format ?
                GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT :
                GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
            glCompressedTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
                internal_format, size, size, 0, tex_size, compressed);
            delete[] compressed;
        }
        else
        {
            GLint format = GL_BGRA;
            if (is_gles)
                format = GL_RGBA;
            GLint internal_format = needs_srgb_format ? GL_SRGB8_ALPHA8 : GL_RGBA8;
            glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0,
                internal_format, size, size, 0, format,
                GL_UNSIGNED_BYTE, (GLvoid*)rgba[i]);
        }

        if (CVS->isTextureCompressionEnabled())
        {
            imMipmapCascade cascade;
            imReduceOptions options;
            imReduceSetOptions(&options, IM_REDUCE_FILTER_LINEAR/*filter*/,
                2/*hopcount*/, 2.0f/*alpha*/, 1.0f/*amplifynormal*/,
                0.0f/*normalsustainfactor*/);
#ifdef DEBUG
            int ret = imBuildMipmapCascade(&cascade, rgba[i], size, size,
                1/*layercount*/, 4, size * 4, &options, 0);
            assert(ret == 1);
#else
            imBuildMipmapCascade(&cascade, rgba[i], size, size,
                1/*layercount*/, 4, size * 4, &options, 0);
#endif
            std::vector<unsigned> mipmap_sizes;
            unsigned width = size;
            while (true)
            {
                width = width < 2 ? 1 : width >> 1;
                mipmap_sizes.push_back(width);
                if (width == 1)
                    break;
            }
            for (unsigned mip = 0; mip < mipmap_sizes.size(); mip++)
            {
                unsigned cur_size = mipmap_sizes[mip];
                unsigned tex_size = GE::get4x4CompressedTextureSize(cur_size,
                    cur_size);
                uint8_t* compressed = new uint8_t[tex_size];
                squishCompressImage((uint8_t*)cascade.mipmap[mip + 1],
                    cur_size, cur_size, cur_size * 4, compressed, tc_flag);
                GLint internal_format = needs_srgb_format ?
                    GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT :
                    GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
                glCompressedTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
                    mip + 1, internal_format, cur_size, cur_size, 0, tex_size,
                    compressed);
                delete[] compressed;
            }
            imFreeMipmapCascade(&cascade);
        }
    }

    if (!CVS->isTextureCompressionEnabled())
        glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
    for (unsigned i = 0; i < 6; i++)
        delete[] rgba[i];
}   // generateCubeMapFromTextures


// ----------------------------------------------------------------------------
void Skybox::generateSpecularCubemap()
{
    glGenTextures(1, &m_specular_probe);
    glBindTexture(GL_TEXTURE_CUBE_MAP, m_specular_probe);
    unsigned int cubemap_size = 256;
    for (int i = 0; i < 6; i++)
    {
#if !defined(USE_GLES2)
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA16F,
                     cubemap_size, cubemap_size, 0, GL_BGRA, GL_FLOAT, 0);
#else
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGBA8,
                     cubemap_size, cubemap_size, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
#endif
    }
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);

    if (!CVS->isDeferredEnabled() || !CVS->isARBTextureBufferObjectUsable())
        return;

#if !defined(USE_GLES2)

    GLuint fbo;
    glGenFramebuffers(1, &fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glViewport(0, 0, cubemap_size, cubemap_size);
    GLenum bufs[] = { GL_COLOR_ATTACHMENT0 };
    glDrawBuffers(1, bufs);
    SpecularIBLGenerator::getInstance()->use();

    glDisable(GL_BLEND);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    core::matrix4 M[6] = {
        getPermutationMatrix(2, -1., 1, -1., 0, 1.),
        getPermutationMatrix(2, 1., 1, -1., 0, -1.),
        getPermutationMatrix(0, 1., 2, 1., 1, 1.),
        getPermutationMatrix(0, 1., 2, -1., 1, -1.),
        getPermutationMatrix(0, 1., 1, -1., 2, 1.),
        getPermutationMatrix(0, -1., 1, -1., 2, -1.),
    };

    for (unsigned level = 0; level < 8; level++)
    {
        // Blinn Phong can be approximated by Phong with 4x the specular
        // coefficient
        // See http://seblagarde.wordpress.com/2012/03/29/relationship-between-phong-and-blinn-lighting-model/
        // NOTE : Removed because it makes too sharp reflexion
        float roughness = (8 - level) * pow(2.f, 10.f) / 8.f;
        float viewportSize = float(1 << (8 - level));

        float *tmp = new float[2048];
        for (unsigned i = 0; i < 1024; i++)
        {
            std::pair<float, float> sample =
                getImportanceSamplingPhong(getHammersleySequence(i, 1024),
                                        roughness);
            tmp[2 * i] = sample.first;
            tmp[2 * i + 1] = sample.second;
        }

        glBindVertexArray(0);
        GLuint sample_texture, sample_buffer;
        glGenBuffers(1, &sample_buffer);
        glBindBuffer(GL_TEXTURE_BUFFER, sample_buffer);
        glBufferData(GL_TEXTURE_BUFFER, 2048 * sizeof(float), tmp,
                     GL_STATIC_DRAW);
        glGenTextures(1, &sample_texture);
        glBindTexture(GL_TEXTURE_BUFFER, sample_texture);
        glTexBuffer(GL_TEXTURE_BUFFER, GL_RG32F, sample_buffer);
        glBindTexture(GL_TEXTURE_BUFFER, 0);
        glBindVertexArray(SharedGPUObjects::getFullScreenQuadVAO());

        for (unsigned face = 0; face < 6; face++)
        {

            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                                   GL_TEXTURE_CUBE_MAP_POSITIVE_X + face,
                                   m_specular_probe, level);
            assert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);

            SpecularIBLGenerator::getInstance()
                ->setTextureUnits(m_cube_map, sample_texture);
            SpecularIBLGenerator::getInstance()->setUniforms(M[face],
                                                             viewportSize);

            glDrawArrays(GL_TRIANGLES, 0, 3);
        }
        glBindBuffer(GL_TEXTURE_BUFFER, 0);
        glBindTexture(GL_TEXTURE_BUFFER, 0);

        delete[] tmp;
        glDeleteTextures(1, &sample_texture);
        glDeleteBuffers(1, &sample_buffer);
    }
    glBindFramebuffer(GL_FRAMEBUFFER, irr_driver->getDefaultFramebuffer());
    glDeleteFramebuffers(1, &fbo);
    glActiveTexture(GL_TEXTURE0);
#endif
}   // generateSpecularCubemap


// ----------------------------------------------------------------------------
/** Generate a skybox from 6 2d textures.
Out of legacy the sequence of textures maps to:
- 1st texture maps to GL_TEXTURE_CUBE_MAP_POSITIVE_Y
- 2nd texture maps to GL_TEXTURE_CUBE_MAP_NEGATIVE_Y
- 3rd texture maps to GL_TEXTURE_CUBE_MAP_POSITIVE_X
- 4th texture maps to GL_TEXTURE_CUBE_MAP_NEGATIVE_X
- 5th texture maps to GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
- 6th texture maps to GL_TEXTURE_CUBE_MAP_POSITIVE_Z
*  \param skybox_textures sequence of 6 textures.
*/
Skybox::Skybox(const std::vector<video::IImage *> &skybox_textures)
{
    m_cube_map = 0;
    m_skybox_textures = skybox_textures;

#if !defined(USE_GLES2)
    glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);
#endif

    if (!skybox_textures.empty())
    {
        generateCubeMapFromTextures();
        if(CVS->isGLSL())
            generateSpecularCubemap();
    }
}

Skybox::~Skybox()
{
    glDeleteTextures(1, &m_cube_map);
    glDeleteTextures(1, &m_specular_probe);
}


// ----------------------------------------------------------------------------
void Skybox::render(const scene::ICameraSceneNode *camera) const
{
    if (m_cube_map == 0)
        return;
    glEnable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);

    if (CVS->isDeferredEnabled())
    {
        glEnable(GL_BLEND);
        glBlendFunc(GL_ONE, GL_ONE);
    }

    SkyboxShader::getInstance()->use();
    SkyboxShader::getInstance()->bindVertexArray();
    SkyboxShader::getInstance()->setUniforms();

    SkyboxShader::getInstance()->setTextureUnits(m_cube_map);

    glDrawArrays(GL_TRIANGLES, 0, 3);
    glBindVertexArray(0);
    glDisable(GL_BLEND);
}   // renderSkybox

#endif   // !SERVER_ONLY

