//
// 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>
#endif

namespace lol
{

//
// The TextureData class
// ---------------------
//

class TextureData
{
    friend class Texture;

    ivec2 m_size;
    PixelFormat m_format;

#if defined USE_D3D9
    IDirect3DDevice9 *m_dev;
    IDirect3DTexture9 *m_texture;
    D3DTEXTUREFILTERTYPE m_mag_filter;
    D3DTEXTUREFILTERTYPE m_min_filter;
    D3DTEXTUREFILTERTYPE m_mip_filter;
#elif defined _XBOX
    D3DDevice *m_dev;
    D3DTexture *m_texture;
    D3DTEXTUREFILTERTYPE m_mag_filter;
    D3DTEXTUREFILTERTYPE m_min_filter;
    D3DTEXTUREFILTERTYPE m_mip_filter;
#else
    GLuint m_texture;
    GLint m_internal_format;
    GLenum m_gl_format, m_gl_type;
#endif
    int m_bytes_per_elem;
};

//
// The Texture class
// -----------------
//

#define GET_CLAMPED(array, index) \
    array[std::max(0, std::min((int)(index), \
                   (int)sizeof(array) / (int)sizeof(*array)))]

Texture::Texture(ivec2 size, PixelFormat format)
  : m_data(new TextureData)
{
    m_data->m_size = size;
    m_data->m_format = format;

#if defined USE_D3D9 || defined _XBOX
#   if defined USE_D3D9
    m_data->m_dev = (IDirect3DDevice9 *)g_renderer->GetDevice();
#   elif defined _XBOX
    m_data->m_dev = (D3DDevice *)g_renderer->GetDevice();
#   endif

    static struct
    {
        D3DFORMAT format;
        int bytes;
    }
    const d3d_formats[] =
    {
        /* Unknown */
        { D3DFMT_UNKNOWN, 0 },

        /* FIXME: this is all mixed up for the RGBA/ARGB combinations */
#   if defined USE_D3D9
        { D3DFMT_L8, 1 },       /* Y_8 */
        { D3DFMT_R8G8B8, 3 },   /* RGB_8 */
        { D3DFMT_A8R8G8B8, 4 }, /* RGBA_8 */
        { D3DFMT_UNKNOWN, 0 },  /* Y_F32 */
        { D3DFMT_UNKNOWN, 0 },  /* RGB_F32 */
        { D3DFMT_UNKNOWN, 0 },  /* RGBA_F32 */
#   else
        { D3DFMT_LIN_L8, 1 },
        { D3DFMT_UNKNOWN, 0 },
        /* By default the X360 will swizzle the texture. Ask for linear. */
        { D3DFMT_LIN_A8R8G8B8, 4 },
        { D3DFMT_UNKNOWN, 0 },  /* Y_F32 */
        { D3DFMT_UNKNOWN, 0 },  /* RGB_F32 */
        { D3DFMT_UNKNOWN, 0 },  /* RGBA_F32 */
#   endif
    };

    D3DFORMAT d3d_format = GET_CLAMPED(d3d_formats, format).format;
    ASSERT(d3d_format != D3DFMT_UNKNOWN,
           "unsupported texture format %d\n", format);
#   if defined USE_D3D9
    int d3d_usage = D3DUSAGE_DYNAMIC;
#   else
    int d3d_usage = D3DUSAGE_WRITEONLY;
#   endif

    m_data->m_dev->CreateTexture(m_data->m_size.x, m_data->m_size.y, 1,
                                 d3d_usage, d3d_format,
                                 D3DPOOL_DEFAULT, &m_data->m_texture, nullptr);
    m_data->m_bytes_per_elem = GET_CLAMPED(d3d_formats, format).bytes;
#else
    static struct
    {
        GLint internal_format;
        GLenum format, type;
        int bytes;
    }
    const gl_formats[] =
    {
        { 0, 0, 0, 0 }, /* Unknown */

        /* FIXME: this is all mixed up for the RGBA/ARGB combinations */
#if defined __native_client__ || defined HAVE_GLES_2X
        { GL_LUMINANCE, GL_LUMINANCE, GL_UNSIGNED_BYTE, 1 },
        { GL_RGB, GL_RGB, GL_UNSIGNED_BYTE, 3 },
        { GL_RGBA, GL_RGBA, GL_UNSIGNED_BYTE, 4 },
        /* FIXME: if GL_RGBA is not available, we should advertise
         * this format as "not available" on this platform. */
#else
        { GL_R8, GL_RED, GL_UNSIGNED_BYTE, 1 }, /* A8 */
        { GL_RGB8, GL_RGB, GL_UNSIGNED_BYTE, 3 }, /* RGB_8 */
        /* Seems efficient for little endian textures */
        { GL_RGBA8, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, 4 }, /* ARGB_8 */
#endif
        { 0, 0, 0, 0 }, /* Y_F32 */
        { 0, 0, 0, 0 }, /* RGB_F32 */
        { 0, 0, 0, 0 }, /* RGBA_F32 */
    };

    m_data->m_internal_format = GET_CLAMPED(gl_formats, format).internal_format;
    m_data->m_gl_format = GET_CLAMPED(gl_formats, format).format;
    m_data->m_gl_type = GET_CLAMPED(gl_formats, format).type;
    m_data->m_bytes_per_elem = GET_CLAMPED(gl_formats, format).bytes;

    glGenTextures(1, &m_data->m_texture);
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
#endif
}

TextureUniform Texture::GetTextureUniform() const
{
    TextureUniform ret;
#if defined USE_D3D9 || defined _XBOX
    ret.m_flags = (uint64_t)(uintptr_t)m_data->m_texture;
    ret.m_attrib = m_data->m_mag_filter;
    ret.m_attrib |= m_data->m_min_filter << 8;
    ret.m_attrib |= m_data->m_mip_filter << 16;
#else
    ret.m_flags = m_data->m_texture;
#endif
    return ret;
}

void Texture::Bind()
{
#if defined _XBOX || defined USE_D3D9
    m_data->m_dev->SetTexture(0, m_data->m_texture);
#else
#   if !defined HAVE_GLES_2X
    glEnable(GL_TEXTURE_2D);
#   endif
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);
#endif
}

void Texture::SetData(void *data)
{
#if defined _XBOX || defined USE_D3D9
    D3DLOCKED_RECT rect;
#   if defined USE_D3D9
    m_data->m_texture->LockRect(0, &rect, nullptr, D3DLOCK_DISCARD);
#   else
    m_data->m_texture->LockRect(0, &rect, nullptr, 0);
#   endif

    memcpy(rect.pBits, data, rect.Pitch * m_data->m_size.y);

    m_data->m_texture->UnlockRect(0);

#else
    glTexImage2D(GL_TEXTURE_2D, 0, m_data->m_internal_format,
                 m_data->m_size.x, m_data->m_size.y, 0,
                 m_data->m_gl_format, m_data->m_gl_type, data);
#endif
}

void Texture::SetSubData(ivec2 origin, ivec2 size, void *data)
{
#if defined _XBOX || defined USE_D3D9
    D3DLOCKED_RECT rect;
    m_data->m_texture->LockRect(0, &rect, nullptr, 0);

    int stride = size.x * m_data->m_bytes_per_elem;
    for (int j = 0; j < size.y; j++)
    {
        uint8_t *dst = (uint8_t *)rect.pBits + (origin.y + j) * rect.Pitch;
        uint8_t *src = (uint8_t *)data + j * stride;
        memcpy(dst, src, stride);
    }

    m_data->m_texture->UnlockRect(0);

#else
    glTexSubImage2D(GL_TEXTURE_2D, 0, origin.x, origin.y, size.x, size.y,
                    m_data->m_gl_format, m_data->m_gl_type, data);
#endif
}

void Texture::SetMagFiltering(TextureMagFilter filter)
{
#if defined _XBOX || defined USE_D3D9
    // In DirectX, texture filtering is a per-texture-unit state
    switch (filter)
    {
        case TextureMagFilter::LINEAR_TEXEL:
            m_data->m_mag_filter = D3DTEXF_LINEAR;
            break;
        case TextureMagFilter::NEAREST_TEXEL:
        default:
            m_data->m_mag_filter = D3DTEXF_POINT;
            break;
    }
#else
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);
    GLenum gl_filter;
    switch (filter)
    {
        case TextureMagFilter::LINEAR_TEXEL:
            gl_filter = GL_LINEAR;
            break;
        case TextureMagFilter::NEAREST_TEXEL:
        default:
            gl_filter = GL_NEAREST;
            break;
    }
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, gl_filter);
#endif
}

void Texture::SetMinFiltering(TextureMinFilter filter)
{
#if defined _XBOX || defined USE_D3D9
    // In DirectX, texture filtering is a per-texture-unit state
#define F(x, y) \
    m_data->m_min_filter = x; m_data->m_mip_filter = y;
    switch (filter)
    {
        case TextureMinFilter::LINEAR_TEXEL_NO_MIPMAP:
            F(D3DTEXF_POINT, D3DTEXF_NONE);
            break;
        case TextureMinFilter::NEAREST_TEXEL_NEAREST_MIPMAP:
            F(D3DTEXF_POINT, D3DTEXF_POINT);
            break;
        case TextureMinFilter::LINEAR_TEXEL_NEAREST_MIPMAP:
            F(D3DTEXF_LINEAR, D3DTEXF_POINT);
            break;
        case TextureMinFilter::NEAREST_TEXEL_LINEAR_MIPMAP:
            F(D3DTEXF_POINT, D3DTEXF_LINEAR);
            break;
        case TextureMinFilter::LINEAR_TEXEL_LINEAR_MIPMAP:
            F(D3DTEXF_LINEAR, D3DTEXF_LINEAR);
            break;
        case TextureMinFilter::NEAREST_TEXEL_NO_MIPMAP:
        default:
            F(D3DTEXF_POINT, D3DTEXF_NONE);
            break;
    }
#undef F

#else
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);
    GLenum gl_filter;
    switch (filter)
    {
        case TextureMinFilter::LINEAR_TEXEL_NO_MIPMAP:
            gl_filter = GL_LINEAR;
            break;
        case TextureMinFilter::NEAREST_TEXEL_NEAREST_MIPMAP:
            gl_filter = GL_NEAREST_MIPMAP_NEAREST;
            break;
        case TextureMinFilter::NEAREST_TEXEL_LINEAR_MIPMAP:
            gl_filter = GL_NEAREST_MIPMAP_LINEAR;
            break;
        case TextureMinFilter::LINEAR_TEXEL_NEAREST_MIPMAP:
            gl_filter = GL_LINEAR_MIPMAP_NEAREST;
            break;
        case TextureMinFilter::LINEAR_TEXEL_LINEAR_MIPMAP:
            gl_filter = GL_LINEAR_MIPMAP_LINEAR;
            break;
        case TextureMinFilter::NEAREST_TEXEL_NO_MIPMAP:
        default:
            gl_filter = GL_NEAREST;
            break;
    }
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, gl_filter);
#endif
}

void Texture::GenerateMipmaps()
{
#if defined USE_D3D9
    m_data->m_texture->->GenerateMipSubLevels();
#elif defined _XBOX
    /* FIXME: No direct mipmap generation support on X360 */
#else
    glBindTexture(GL_TEXTURE_2D, m_data->m_texture);
    glGenerateMipmap(GL_TEXTURE_2D);
#endif
}

Texture::~Texture()
{
#if defined USE_D3D9 || defined _XBOX
    m_data->m_texture->Release();
#else
    glDeleteTextures(1, &m_data->m_texture);
#endif

    delete m_data;
}

} /* namespace lol */

