//
// Lol Engine
//
// Copyright: (c) 2010-2013 Sam Hocevar <sam@hocevar.net>
//   This program is free software; you can redistribute it and/or
//   modify it under the terms of the Do What The Fuck You Want To
//   Public License, Version 2, as published by Sam Hocevar. See
//   http://www.wtfpl.net/ for more details.
//

#include <lol/engine-internal.h>

#include "lolgl.h"

#if defined _WIN32 && defined USE_D3D9
#   define FAR
#   define NEAR
#   include <d3d9.h>
#elif defined _XBOX
#   include <xtl.h>
#   undef near /* Fuck Microsoft */
#   undef far /* Fuck Microsoft again */
#endif

namespace lol
{

//
// The FramebufferData class
// -------------------------
//

class FramebufferData
{
    friend class Framebuffer;

    ibox2 m_saved_viewport;
    ivec2 m_size;
    bool m_bound;

#if defined USE_D3D9
    IDirect3DDevice9 *m_dev;
    IDirect3DTexture9 *m_texture;
    IDirect3DSurface9 *m_surface, *m_back_surface;
#elif defined _XBOX
    D3DDevice *m_dev;
    D3DTexture *m_texture;
    D3DSurface *m_surface, *m_back_surface;
#else
    GLuint m_fbo, m_texture, m_depth;
#endif
};

//
// The FramebufferFormat struct
// ----------------------
//

uint32_t FramebufferFormat::GetFormat()
{
    switch (m_format)
    {
#if defined USE_D3D9
    case R_16_F:        return D3DFMT_R16F;
    case R_32_F:        return D3DFMT_R32F;
    case RG_16:
    case RG_16_I:
    case RG_16_UI:      return D3DFMT_G16R16;
    case RG_16_F:       return D3DFMT_G16R16F;
    case RG_32_F:       return D3DFMT_G32R32F;
    case RGB_8:
    case RGB_8_I:
    case RGB_8_UI:      return D3DFMT_R8G8B8;
    case RGBA_8:
    case RGBA_8_I:
    case RGBA_8_UI:     return D3DFMT_A8R8G8B8;
    case RGBA_16:
    case RGBA_16_I:
    case RGBA_16_UI:    return D3DFMT_A16B16G16R16;
    case RGBA_16_F:     return D3DFMT_A16B16G16R16F;
    case RGBA_32_F:     return D3DFMT_A32B32G32R32F;
#elif defined _XBOX
    case R_16_F:        return D3DFMT_R16F;
    case R_32_F:        return D3DFMT_R32F;
    case RG_16:
    case RG_16_I:
    case RG_16_UI:      return D3DFMT_G16R16;
    case RG_16_F:       return D3DFMT_G16R16F;
    case RG_32_F:       return D3DFMT_G32R32F;
    case RGB_8:
    case RGB_8_I:
    case RGB_8_UI:      return D3DFMT_X8R8G8B8;
    case RGBA_8:
    case RGBA_8_I:
    case RGBA_8_UI:     return D3DFMT_A8R8G8B8;
    case RGBA_16:
    case RGBA_16_I:
    case RGBA_16_UI:    return D3DFMT_A16B16G16R16;
    case RGBA_16_F:     return D3DFMT_A16B16G16R16F;
    case RGBA_32_F:     return D3DFMT_A32B32G32R32F;
#elif defined HAVE_GLES_2X
    /* FIXME: incomplete */
    case RGBA_8:
    case RGBA_8_I:
    case RGBA_8_UI:     return GL_RGBA;
#elif defined __APPLE__ && defined __MACH__
    case R_8:
    case R_8_I:
    case R_8_UI:
    case R_8_F:

    case R_16:
    case R_16_I:
    case R_16_UI:
    case R_16_F:

    case R_32_I:
    case R_32:
    case R_32_UI:
    case R_32_F:        return GL_RED;

    case RG_8:
    case RG_8_I:
    case RG_8_UI:
    case RG_8_F:

    case RG_16:
    case RG_16_I:
    case RG_16_UI:
    case RG_16_F:

    case RG_32:
    case RG_32_I:
    case RG_32_UI:
    case RG_32_F:       return GL_RG;

    case RGB_8:
    case RGB_8_I:
    case RGB_8_UI:
    case RGB_8_F:

    case RGB_16:
    case RGB_16_I:
    case RGB_16_UI:
    case RGB_16_F:

    case RGB_32:
    case RGB_32_I:
    case RGB_32_UI:
    case RGB_32_F:      return (m_invert_rgb)?(GL_BGR):(GL_RGB);

    case RGBA_8:
    case RGBA_8_I:
    case RGBA_8_UI:
    case RGBA_8_F:

    case RGBA_16:
    case RGBA_16_I:
    case RGBA_16_UI:
    case RGBA_16_F:

    case RGBA_32:
    case RGBA_32_I:
    case RGBA_32_UI:
#   if defined GL_BGRA
    case RGBA_32_F:     return (m_invert_rgb)?(GL_BGRA):(GL_RGBA);
#   else
    case RGBA_32_F:     return GL_RGBA;
#   endif
#else
    case R_8:           return GL_R8;
    case R_8_I:         return GL_R8I;
    case R_8_UI:        return GL_R8UI;

    case R_16:          return GL_R16;
    case R_16_I:        return GL_R16I;
    case R_16_UI:       return GL_R16UI;
    case R_16_F:        return GL_R16F;

    case R_32_I:        return GL_R32I;
    case R_32_UI:       return GL_R32UI;
    case R_32_F:        return GL_R32F;

    case RG_8:          return GL_RG8;
    case RG_8_I:        return GL_RG8I;
    case RG_8_UI:       return GL_RG8UI;

    case RG_16:         return GL_RG16;
    case RG_16_I:       return GL_RG16I;
    case RG_16_UI:      return GL_RG16UI;
    case RG_16_F:       return GL_RG16F;

    case RG_32_I:       return GL_RG32I;
    case RG_32_UI:      return GL_RG32UI;
    case RG_32_F:       return GL_RG32F;

    case RGB_8:         return GL_RGB8;
    case RGB_8_I:       return GL_RGB8I;
    case RGB_8_UI:      return GL_RGB8UI;

    case RGB_16:        return GL_RGB16;
    case RGB_16_I:      return GL_RGB16I;
    case RGB_16_UI:     return GL_RGB16UI;
    case RGB_16_F:      return GL_RGB16F;

    case RGB_32_I:      return GL_RGB32I;
    case RGB_32_UI:     return GL_RGB32UI;
    case RGB_32_F:      return GL_RGB32F;

    case RGBA_8:        return GL_RGBA8;
    case RGBA_8_I:      return GL_RGBA8I;
    case RGBA_8_UI:     return GL_RGBA8UI;

    case RGBA_16:       return GL_RGBA16;
    case RGBA_16_I:     return GL_RGBA16I;
    case RGBA_16_UI:    return GL_RGBA16UI;
    case RGBA_16_F:     return GL_RGBA16F;

    case RGBA_32_I:     return GL_RGBA32I;
    case RGBA_32_UI:    return GL_RGBA32UI;
    case RGBA_32_F:     return GL_RGBA32F;
#endif
    default:
        ASSERT(false, "unknown framebuffer format %d", m_format);
        return 0;
    }
}

uint32_t FramebufferFormat::GetFormatOrder()
{
    switch (m_format)
    {
#if defined USE_D3D9 || defined _XBOX
    /* FIXME: not implemented at all */
#elif defined HAVE_GLES_2X
    /* FIXME: incomplete */
    case R_8:   case RG_8:   case RGB_8:   case RGBA_8:
    case R_8_I: case RG_8_I: case RGB_8_I: case RGBA_8_I:
        return GL_BYTE;
    case R_8_UI: case RG_8_UI: case RGB_8_UI: case RGBA_8_UI:
        return GL_UNSIGNED_BYTE;
#elif defined __APPLE__ && defined __MACH__
    case R_8:   case RG_8:   case RGB_8:   case RGBA_8:
    case R_8_I: case RG_8_I: case RGB_8_I: case RGBA_8_I:
        return GL_BYTE;
    case R_8_UI: case RG_8_UI: case RGB_8_UI: case RGBA_8_UI:
        return GL_UNSIGNED_BYTE;

    case R_16:   case RG_16:   case RGB_16:   case RGBA_16:
    case R_16_I: case RG_16_I: case RGB_16_I: case RGBA_16_I:
        return GL_SHORT;
    case R_16_UI: case RG_16_UI: case RGB_16_UI: case RGBA_16_UI:
        return GL_UNSIGNED_SHORT;

    case R_16_F: case RG_16_F: case RGB_16_F: case RGBA_16_F:
        ASSERT(false, "unsupported framebuffer format order %d", m_format);
        return 0;

    case R_32_I: case RG_32_I: case RGB_32_I: case RGBA_32_I:
        return GL_INT;
    case R_32_UI: case RG_32_UI: case RGB_32_UI: case RGBA_32_UI:
        return GL_UNSIGNED_INT;
    case R_32_F: case RG_32_F: case RGB_32_F: case RGBA_32_F:
        return GL_FLOAT;
#else
    case R_8:  case R_8_I:  case R_8_UI:  case R_8_F:
    case R_16: case R_16_I: case R_16_UI: case R_16_F:
    case R_32: case R_32_I: case R_32_UI: case R_32_F:
        return GL_RED;

    case RG_8:  case RG_8_I:  case RG_8_UI:  case RG_8_F:
    case RG_16: case RG_16_I: case RG_16_UI: case RG_16_F:
    case RG_32: case RG_32_I: case RG_32_UI: case RG_32_F:
        return GL_RG;

    case RGB_8:  case RGB_8_I:  case RGB_8_UI:  case RGB_8_F:
    case RGB_16: case RGB_16_I: case RGB_16_UI: case RGB_16_F:
    case RGB_32: case RGB_32_I: case RGB_32_UI: case RGB_32_F:
        return m_invert_rgb ? GL_BGR : GL_RGB;

    case RGBA_8:  case RGBA_8_I:  case RGBA_8_UI:  case RGBA_8_F:
    case RGBA_16: case RGBA_16_I: case RGBA_16_UI: case RGBA_16_F:
    case RGBA_32: case RGBA_32_I: case RGBA_32_UI: case RGBA_32_F:
#   if defined GL_BGRA
        return (m_invert_rgb)?(GL_BGRA):(GL_RGBA);
#   else
        return GL_RGBA;
#   endif
#endif
    default:
        ASSERT(false, "unknown framebuffer format order %d", m_format);
        return 0;
    }
}

//
// The Framebuffer class
// ----------------------
//

Framebuffer::Framebuffer(ivec2 size, FramebufferFormat fbo_format)
  : m_data(new FramebufferData)
{
    m_data->m_size = size;
    m_data->m_bound = false;
#if defined USE_D3D9
    m_data->m_dev = (IDirect3DDevice9 *)g_renderer->GetDevice();

    if (FAILED(m_data->m_dev->CreateTexture(size.x, size.y, 1,
                                            D3DUSAGE_RENDERTARGET,
                                            (D3DFORMAT)fbo_format.GetFormat(),
                                            D3DPOOL_DEFAULT,
                                            &m_data->m_texture, nullptr)))
        Abort();
    if (FAILED(m_data->m_texture->GetSurfaceLevel(0, &m_data->m_surface)))
        Abort();
#elif defined _XBOX
    m_data->m_dev = (D3DDevice *)g_renderer->GetDevice();

    if (FAILED(m_data->m_dev->CreateTexture(size.x, size.y, 1, 0,
                                            (D3DFORMAT)fbo_format.GetFormat(),
                                            D3DPOOL_DEFAULT,
                                            &m_data->m_texture, nullptr)))
        Abort();
    if (FAILED(m_data->m_dev->CreateRenderTarget(size.x, size.y,
                                                 (D3DFORMAT)fbo_format.GetFormat(),
                                                 D3DMULTISAMPLE_NONE, 0, 0,
                                                 &m_data->m_surface, nullptr)))
        Abort();
#else
#   if GL_VERSION_1_1
    GLenum internal_format = fbo_format.GetFormat();
    GLenum format = fbo_format.GetFormatOrder();
    GLenum depth = GL_DEPTH_COMPONENT;
#   elif GL_ES_VERSION_2_0
    /* In OpenGL ES, internal format and format must match. */
    GLenum internal_format = fbo_format.GetFormat();
    GLenum format = fbo_format.GetFormat();
    GLenum depth = GL_DEPTH_COMPONENT16; /* for WebGL */
#   else
    /* In OpenGL ES, internal format and format must match. */
    GLenum internal_format = fbo_format.GetFormat();
    GLenum format = fbo_format.GetFormat();
#   endif
    GLenum wrapmode = GL_CLAMP_TO_EDGE;
    GLenum filtering = GL_NEAREST;

#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glGenFramebuffers(1, &m_data->m_fbo);
    glBindFramebuffer(GL_FRAMEBUFFER, m_data->m_fbo);
#   else
    glGenFramebuffersOES(1, &m_data->m_fbo);
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_data->m_fbo);
#   endif

    glGenTextures(1, &m_data->m_texture);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapmode);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapmode);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filtering);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filtering);
    glTexImage2D(GL_TEXTURE_2D, 0, internal_format, size.x, size.y, 0,
                 format, GL_UNSIGNED_BYTE, nullptr);

#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                           GL_TEXTURE_2D, m_data->m_texture, 0);
#   else
    glFramebufferTexture2DOES(GL_FRAMEBUFFER_OES, GL_COLOR_ATTACHMENT0_EXT,
                              GL_TEXTURE_2D, m_data->m_texture, 0);
#   endif

    m_data->m_depth = GL_INVALID_ENUM;
#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    if (depth != GL_INVALID_ENUM)
    {
        /* XXX: might not work on GL ES, see
         * http://stackoverflow.com/q/4041682/111461
         * See also http://qt-project.org/forums/viewthread/11734 */
        glGenRenderbuffers(1, &m_data->m_depth);
        glBindRenderbuffer(GL_RENDERBUFFER, m_data->m_depth);
        glRenderbufferStorage(GL_RENDERBUFFER, depth, size.x, size.y);
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                                  GL_RENDERBUFFER, m_data->m_depth);
    }
#   endif

    glBindTexture(GL_TEXTURE_2D, 0);

#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    ASSERT(status == GL_FRAMEBUFFER_COMPLETE,
           "invalid framebuffer status 0x%x", status);
#   endif

#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#   else
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, 0);
#   endif
#endif
}

Framebuffer::~Framebuffer()
{
#if defined USE_D3D9 || defined _XBOX
    m_data->m_surface->Release();
    m_data->m_texture->Release();
#else
#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glDeleteFramebuffers(1, &m_data->m_fbo);
#   else
    glDeleteFramebuffersOES(1, &m_data->m_fbo);
#   endif
    glDeleteTextures(1, &m_data->m_texture);
#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    if (m_data->m_depth != GL_INVALID_ENUM)
        glDeleteRenderbuffers(1, &m_data->m_depth);
#   endif
#endif
    delete m_data;
}

TextureUniform Framebuffer::GetTextureUniform() const
{
    TextureUniform ret;
#if defined USE_D3D9 || defined _XBOX
    ret.m_flags = (uint64_t)(uintptr_t)m_data->m_texture;
#else
    ret.m_flags = m_data->m_texture;
#endif
    return ret;
}

ivec2 Framebuffer::GetSize() const
{
    return m_data->m_size;
}

Image Framebuffer::GetImage() const
{
    Image ret(m_data->m_size);

#if defined USE_D3D9 || defined _XBOX
    /* TODO: implement D3D Framebuffer::GetImage() */
#else
    u8vec4 *buffer = ret.Lock<PixelFormat::RGBA_8>();
    glReadPixels(0, 0, m_data->m_size.x, m_data->m_size.y,
                 GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    ret.Unlock(buffer);
#endif

    return ret;
}

void Framebuffer::Bind()
{
    ASSERT(!m_data->m_bound, "trying to bind an already bound framebuffer");

#if defined USE_D3D9 || defined _XBOX
    if (FAILED(m_data->m_dev->GetRenderTarget(0, &m_data->m_back_surface)))
        Abort();
    if (FAILED(m_data->m_dev->SetRenderTarget(0, m_data->m_surface)))
        Abort();
#else
#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glBindFramebuffer(GL_FRAMEBUFFER, m_data->m_fbo);
#   else
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, m_data->m_fbo);
#   endif
#endif

    /* FIXME: this should be done in the RenderContext object
     * instead, maybe by getting rid of Framebuffer::Bind() and
     * creating RenderContext::SetFramebuffer() instead. */
    m_data->m_saved_viewport = g_renderer->GetViewport();
    g_renderer->SetViewport(ibox2(ivec2::zero, m_data->m_size));
    m_data->m_bound = true;
}

void Framebuffer::Unbind()
{
    ASSERT(m_data->m_bound, "trying to unbind an unbound framebuffer");

#if defined USE_D3D9 || defined _XBOX
#   if defined _XBOX
    if (FAILED(m_data->m_dev->Resolve(D3DRESOLVE_RENDERTARGET0, nullptr,
                                    m_data->m_texture, nullptr, 0, 0, nullptr,
                                    0, 0, nullptr)))
        Abort();
#   endif
    if (FAILED(m_data->m_dev->SetRenderTarget(0, m_data->m_back_surface)))
        Abort();
    m_data->m_back_surface->Release();
#else
#   if GL_VERSION_1_1 || GL_ES_VERSION_2_0
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
#   else
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, 0);
#   endif
#endif

    g_renderer->SetViewport(m_data->m_saved_viewport);
    m_data->m_bound = false;
}

} /* namespace lol */

