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

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

  \author Satofumi KAMIMURA

  $Id: SDL_GL_Texture.cpp 812 2009-05-07 14:35:19Z satofumi $
*/

#include "SDL_GL_Texture.h"
#include "SdlUtils.h"
#include "log_printf.h"
#include <SDL_opengl.h>
#include <boost/bind.hpp>
#include <algorithm>
#include <set>

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


namespace
{
  // 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_;
  size_t base_w_;
  size_t 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)
  {
    SDL_FreeSurface(surface_);
  }


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


  void SDL_GL_LoadTexture(SDL_Surface* surface)
  {
    if (surface == NULL) {
      return;
    }
    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(surface->w);
    int height = power_of_two(surface->h);
    basecoord_[0] = 0.0f;        // Min X
    basecoord_[1] = 0.0f;        // Min Y
    basecoord_[2] = (GLfloat)surface->w / width; // Max X
    basecoord_[3] = (GLfloat)surface->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;
    }

    // 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, surface->w, surface->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

    texture_id_ = 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_);
  }
}


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);

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

  glTexCoord2f(texMinX, texMinY);
  glVertex2f(surface_x, surface_y);

  glTexCoord2f(width_ratio, texMinY);
  glVertex2f(surface_x + surface_w, surface_y);

  glTexCoord2f(texMinX, height_ratio);
  glVertex2f(surface_x, surface_y + surface_h);

  glTexCoord2f(width_ratio, height_ratio);
  glVertex2f(surface_x + surface_w, surface_y + surface_h);

  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::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));
}


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