/*!
  \file
  \brief SDL + OpenGL 環境でのサーフェス描画

  SDL-1.2.12/test/testgl.c のソースコードを元に実装

  \author Satofumi KAMIMURA

  $Id: SDL_GL_Texture.cpp 1966 2011-08-16 08:57:00Z satofumi $
*/

#include "SDL_GL_Texture.h"
#include "SdlUtils.h"
#include "Rotate.h"
#include "log_printf.h"
#define NO_SDL_GLEXT
#include <GL/glew.h>
#include <SDL_opengl.h>
#include <boost/bind.hpp>
#include <algorithm>
#include <set>
#include <vector>

using namespace qrk;
using namespace boost;
using namespace std;


namespace
{
    enum {
        InvalidTextureId = 0,
    };


    // Quick utility function for texture creation
    int power_of_two(int input)
    {
        int value = 1;
        while (value < input) {
            value <<= 1;
        }
        return value;
    }
}


struct SDL_GL_Texture::pImpl
{
    static set<SDL_GL_Texture*> textures_;

    SDL_Surface* surface_;
    bool is_valid_;
    GLuint texture_id_;
    GLfloat basecoord_[4];
    GLfloat alpha_;
    Angle rotate_angle_;
    int base_w_;
    int base_h_;
    bool transparent_;


    pImpl(SDL_Surface* surface, bool transparent)
        : surface_(surface), is_valid_(false), texture_id_(InvalidTextureId),
          alpha_(1.0), base_w_(0), base_h_(0), transparent_(transparent)
    {
        if (! surface_) {
            //log_printf("SDL_GL_Texture(NULL)\n");
            return;
        }

        if (transparent_) {
            // 透過処理
            surface_ = transparentizeSdlSurface(surface_);
        }

        // isValid() の結論を出すため、テクスチャ作成は遅延させずに、
        // ここで作成する
        recreateTexture();
    }


    ~pImpl(void)
    {
        // recreateTexture() 用にデストラクタまでリソースを解放をしない
        SDL_FreeSurface(surface_);
    }


    void recreateTexture(void)
    {
        is_valid_ = false;
        texture_id_ = SDL_GL_LoadTexture(surface_);
        if (texture_id_ == InvalidTextureId) {
            return;
        }
        is_valid_ = true;
    }


    GLuint SDL_GL_LoadTexture(SDL_Surface* surface)
    {
        if (surface == NULL) {
            return InvalidTextureId;
        }
        texture_id_ = InvalidTextureId;
        base_w_ = surface->w;
        base_h_ = surface->h;

        // Use the surface width and height expanded to powers of 2
        int width = power_of_two(base_w_);
        int height = power_of_two(base_h_);
        basecoord_[0] = 0.0f;        // Min X
        basecoord_[1] = 0.0f;        // Min Y
        basecoord_[2] = static_cast<GLfloat>(base_w_) / width; // Max X
        basecoord_[3] = static_cast<GLfloat>(base_h_) / height; // Max Y

        SDL_Surface* image =
            SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 32,
#if SDL_BYTEORDER == SDL_LIL_ENDIAN // OpenGL RGBA masks
                                                  0x000000FF,
                                                  0x0000FF00,
                                                  0x00FF0000,
                                                  0xFF000000
#else
                                                  0xFF000000,
                                                  0x00FF0000,
                                                  0x0000FF00,
                                                  0x000000FF
#endif
                                                  );
        if (image == NULL) {
            return InvalidTextureId;
        }

        // Save the alpha blending attributes
        Uint32 saved_flags = surface->flags & (SDL_SRCALPHA | SDL_RLEACCELOK);
        Uint8 saved_alpha = surface->format->alpha;
        if ((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA) {
            SDL_SetAlpha(surface, 0, 0);
        }

        // Copy the surface into the GL texture image
        SDL_Rect area;
        set_SdlRect(&area, 0, 0, base_w_, base_h_);
        SDL_BlitSurface(surface, &area, image, &area);

        // Restore the alpha blending attributes
        if ((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA) {
            SDL_SetAlpha(surface, saved_flags, saved_alpha);
        }

        // Create an OpenGL texture for the image
        GLuint texture = InvalidTextureId;
        glGenTextures(1, &texture);
        glBindTexture(GL_TEXTURE_2D, texture);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA,
                     GL_UNSIGNED_BYTE, image->pixels);
        SDL_FreeSurface(image); // No longer needed

        return texture;
    }
};
set<SDL_GL_Texture*> SDL_GL_Texture::pImpl::textures_;


SDL_GL_Texture::SDL_GL_Texture(SDL_Surface* surface, bool transparent)
    : pimpl(new pImpl(surface, transparent)),
      w(pimpl->base_w_), h(pimpl->base_h_)
{
    pImpl::textures_.insert(this);
}


SDL_GL_Texture::~SDL_GL_Texture(void)
{
    deleteTexture();
    pImpl::textures_.erase(this);
}


void SDL_GL_Texture::deleteTexture(void)
{
    if (pimpl->texture_id_ != InvalidTextureId) {
        glDeleteTextures(1, &pimpl->texture_id_);
        pimpl->texture_id_ = InvalidTextureId;
    }
}


void SDL_GL_Texture::recreateTexture(void)
{
    pimpl->recreateTexture();
}


bool SDL_GL_Texture::isValid(void) const
{
    return pimpl->is_valid_;
}


void SDL_GL_Texture::blitTexture(const Rect<long>* area, const Rect<long>* dest)
{
    if (! isValid()) {
        return;
    }

    GLfloat texMinX = pimpl->basecoord_[0];
    GLfloat texMinY = pimpl->basecoord_[1];
    GLfloat texMaxX = pimpl->basecoord_[2];
    GLfloat texMaxY = pimpl->basecoord_[3];

    if (area) {
        texMinX =
            static_cast<GLfloat>(texMaxX * (1.0 * area->x / pimpl->base_w_));
        texMinY =
            static_cast<GLfloat>(texMaxY * (1.0 * area->y / pimpl->base_h_));
    }

    int x = dest->x;
    int y = dest->y;
    int w = dest->w;
    int h = dest->h;

    GLfloat width_ratio = texMaxX * w / pimpl->base_w_;
    GLfloat height_ratio = texMaxY * h / pimpl->base_h_;

    if (area) {
        width_ratio = texMaxX * (area->x + area->w) / pimpl->base_w_;
        height_ratio = texMaxY * (area->y + area->h) / pimpl->base_h_;
    }

    // 透過処理のための記述
    glEnable(GL_TEXTURE_2D);
    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glColor4d(1.0, 1.0, 1.0, pimpl->alpha_);

    GLfloat surface_x = static_cast<GLfloat>(x);
    GLfloat surface_y = static_cast<GLfloat>(y);
    GLfloat surface_w = static_cast<GLfloat>(w);
    GLfloat surface_h = static_cast<GLfloat>(h);

    // !!! OpenGL の回転機能を使うべき
    vector<Point<GLfloat> > offset;
    offset.push_back(Point<GLfloat>(-surface_w / 2.0, -surface_h / 2.0));
    offset.push_back(Point<GLfloat>(+surface_w / 2.0, -surface_h / 2.0));
    offset.push_back(Point<GLfloat>(-surface_w / 2.0, +surface_h / 2.0));
    offset.push_back(Point<GLfloat>(+surface_w / 2.0, +surface_h / 2.0));

    for (int i = 0; i < 4; ++i) {
        offset[i] = rotate(offset[i], pimpl->rotate_angle_) - offset[i];
    }

    glBindTexture(GL_TEXTURE_2D, pimpl->texture_id_);
    glBegin(GL_TRIANGLE_STRIP);

    glTexCoord2f(texMinX, texMinY);
    glVertex2f(surface_x + offset[0].x, surface_y + offset[0].y);

    glTexCoord2f(width_ratio, texMinY);
    glVertex2f(surface_x + surface_w + offset[1].x, surface_y + offset[1].y);

    glTexCoord2f(texMinX, height_ratio);
    glVertex2f(surface_x + offset[2].x, surface_y + surface_h + offset[2].y);

    glTexCoord2f(width_ratio, height_ratio);
    glVertex2f(surface_x + surface_w + offset[3].x,
               surface_y + surface_h + offset[3].y);

    glEnd();
    glDisable(GL_TEXTURE_2D);
}


void SDL_GL_Texture::setAlpha(float alpha)
{
    pimpl->alpha_ = alpha;
}


float SDL_GL_Texture::alpha(void) const
{
    return pimpl->alpha_;
}


void SDL_GL_Texture::setRotateAngle(const Angle& angle)
{
    pimpl->rotate_angle_ = angle;
}


void SDL_GL_Texture::setMagnify(float magnify)
{
    static_cast<void>(magnify);
    // !!!
}


float SDL_GL_Texture::magnify(void)
{
    // !!!
    return 1.0;
}


bool SDL_GL_Texture::transparent(void) const
{
    return pimpl->transparent_;
}


SDL_Surface* SDL_GL_Texture::transparentizeSdlSurface(SDL_Surface* surface)
{
    SDL_Surface* scr = SDL_GetVideoSurface();
    if (! (scr->flags & SDL_OPENGL)) {
        log_printf("OpenGL is not initialized: use Screen::show(SDL_OPENGL)\n");
    }
    if ((surface == NULL) || (scr == NULL)) {
        return surface;
    }

    Uint32 trans_color = 0;
    if (surface->format->BitsPerPixel == 8) {
        // ピクセルフォーマットが 8 bit の場合
        Uint8 index = *(Uint8 *)surface->pixels;
        SDL_Color& color = surface->format->palette->colors[index];
        trans_color = SDL_MapRGB(surface->format, color.r, color.g, color.b);
    } else {
        trans_color = *(Uint32*)surface->pixels;
    }

    SDL_SetColorKey(surface, SDL_SRCCOLORKEY | SDL_RLEACCEL, trans_color);
    SDL_Surface* transparent_surface = SDL_DisplayFormat(surface);
    swap(surface, transparent_surface);
    SDL_FreeSurface(transparent_surface);

    return surface;
}


void SDL_GL_Texture::deleteTextures(void)
{
    for_each(pImpl::textures_.begin(), pImpl::textures_.end(),
             bind(&SDL_GL_Texture::deleteTexture, _1));
    pImpl::textures_.clear();
}


void SDL_GL_Texture::recreateTextures(void)
{
    for_each(pImpl::textures_.begin(), pImpl::textures_.end(),
             bind(&SDL_GL_Texture::recreateTexture, _1));
}
