/*!
  \example glTextureDraw.cpp

  \brief SDL_Surface を OpenGL で使うためのサンプル

  \author Satofumi KAMIMURA

  $Id: glTextureDraw.cpp 694 2009-03-29 17:31:03Z satofumi $

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

#include <SDL.h>
#include <SDL_opengl.h>
#include <algorithm>

using namespace std;


namespace
{
  enum {
    ScreenWidth = 640,
    ScreenHeight = 480,
    ScreenBpp = 32,
  };


  void set_SdlRect(SDL_Rect* rect, int x, int y, int w, int h)
  {
    rect->x = x;
    rect->y = y;
    rect->w = w;
    rect->h = h;
  }


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


  // SDL のサーフェスを受け取り、OpenGL のテクスチャーとして描画を行うためのクラス
  class GL_Texture
  {

  public:
    enum {
      InvalidTextureId = 0,
    };
    GLuint texture_id_;
    GLfloat basecoord_[4];
    GLfloat alpha_;
    size_t base_w_;
    size_t base_h_;

    size_t w;
    size_t h;


    GL_Texture(void)
      : texture_id_(InvalidTextureId), alpha_(1.0), w(0), h(0)
    {
    }


    ~GL_Texture(void)
    {
    }


    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 w = power_of_two(surface->w);
      int h = power_of_two(surface->h);
      basecoord_[0] = 0.0f;        // Min X
      basecoord_[1] = 0.0f;        // Min Y
      basecoord_[2] = (GLfloat)surface->w / w; // Max X
      basecoord_[3] = (GLfloat)surface->h / h; // Max Y

      SDL_Surface* image = SDL_CreateRGBSurface(SDL_SWSURFACE, w, h, 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, w, h, 0, GL_RGBA,
                   GL_UNSIGNED_BYTE, image->pixels);
      SDL_FreeSurface(image); // No longer needed

      texture_id_ = texture;
    }
  };


  GL_Texture* Gui_LoadTexture(SDL_Surface* surface)
  {
    if (surface == NULL) {
      return NULL;
    }
    GL_Texture* texture = new GL_Texture;
    texture->w = surface->w;
    texture->h = surface->h;

    texture->SDL_GL_LoadTexture(surface);

    return texture;
  }


  void enter2D(void)
  {
    glPushAttrib(GL_ENABLE_BIT);
    glDisable(GL_DEPTH_TEST);
    glDisable(GL_CULL_FACE);
    glEnable(GL_TEXTURE_2D);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

    glViewport(0, 0, ScreenWidth - 1, ScreenHeight - 1);

    glMatrixMode(GL_PROJECTION);
    glPushMatrix();
    glLoadIdentity();

    glOrtho(0.0, ScreenWidth - 1.0, ScreenHeight - 1.0, 0.0, 0.0, 1.0);

    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glLoadIdentity();

    glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL);
  }


  void leave2D(void)
  {
    glMatrixMode(GL_MODELVIEW);
    glPopMatrix();

    glMatrixMode(GL_PROJECTION);
    glPopMatrix();

    glPopAttrib();
  }


  void Gui_FreeSurface(GL_Texture* texture)
  {
    if (texture->texture_id_ != GL_Texture::InvalidTextureId) {
      glDeleteTextures(1, &texture->texture_id_);
      texture->texture_id_ = GL_Texture::InvalidTextureId;
    }
  }


  void Gui_BlitTexture(GL_Texture* texture, SDL_Rect* srcrect,
                       SDL_Rect* dstrect)
  {
    if (texture->texture_id_ == GL_Texture::InvalidTextureId) {
      return;
    }

    enter2D();

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

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

    if (srcrect) {
      texMinX = texMaxX * (1.0 * srcrect->x / texture->base_w_);
      texMinY = texMaxY * (1.0 * srcrect->y / texture->base_h_);
    }

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

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

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

    GLfloat screen_x = x;
    GLfloat screen_y = y;
    GLfloat screen_w = w;
    GLfloat screen_h = h;

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

    glTexCoord2f(texMinX, texMinY);
    glVertex2f(screen_x, screen_y);

    glTexCoord2f(width_ratio, texMinY);
    glVertex2f(screen_x + screen_w, screen_y);

    glTexCoord2f(texMinX, height_ratio);
    glVertex2f(screen_x, screen_y + screen_h);

    glTexCoord2f(width_ratio, height_ratio);
    glVertex2f(screen_x + screen_w, screen_y + screen_h);

    glEnd();

    leave2D();
  }
}


static void errorAndExit(const char* message)
{
  printf("%s: %s\n", message, SDL_GetError());
  exit(1);
}


static SDL_Surface* transparentSdlSurface(SDL_Surface* surface)
{
  SDL_Surface* scr = SDL_GetVideoSurface();
  if ((surface == NULL) || (scr == NULL)) {
    return NULL;
  }

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


static void initializeOpenGl(void)
{
  int bpp = SDL_GetVideoInfo()->vfmt->BitsPerPixel;

  // Initialize the display
  int rgb_size[3];
  switch (bpp) {
  case 8:
    rgb_size[0] = 3;
    rgb_size[1] = 3;
    rgb_size[2] = 2;
    break;

  case 15:
  case 16:
    rgb_size[0] = 5;
    rgb_size[1] = 5;
    rgb_size[2] = 5;
    break;

  default:
    rgb_size[0] = 8;
    rgb_size[1] = 8;
    rgb_size[2] = 8;
    break;
  }
  SDL_GL_SetAttribute(SDL_GL_RED_SIZE, rgb_size[0]);
  SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, rgb_size[1]);
  SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, rgb_size[2]);
  SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
}


static void setupOpenGlView(void)
{
  glPushAttrib(GL_ENABLE_BIT);
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);
  glEnable(GL_TEXTURE_2D);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glViewport(-1, -1, +1, +1);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();

  glOrtho(-1.0, +1.0, -1.0, +1.0, -10.0, +10.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glShadeModel(GL_SMOOTH);
}


static SDL_Surface* initializeScreen(void)
{
  if (SDL_Init(SDL_INIT_VIDEO) < 0) {
    errorAndExit("SDL_Init");
  }
  atexit(SDL_Quit);

  Uint32 mode = SDL_OPENGL;
  int width = ScreenWidth;
  int height =ScreenHeight;
  int bpp = ScreenBpp;

  initializeOpenGl();
  SDL_Surface* scr = SDL_SetVideoMode(width, height, bpp, mode);
  if (scr == NULL) {
    errorAndExit("SDL_SetVideoMode");
  }
  setupOpenGlView();

  return scr;
}


int main(int argc, char *argv[])
{
  static_cast<void>(argc);
  static_cast<void>(argv);

  // 画面の初期化
  initializeScreen();

  // BMP 画像を SDL_Surface として読み出す
  SDL_Surface* cross_surface = SDL_LoadBMP("cross_blue.bmp");
  if (cross_surface == NULL) {
    errorAndExit("SDL_LoadBMP");
  }

  // (0, 0) の位置の色を透明色として扱う
  SDL_Surface* transparent_surface = SDL_LoadBMP("cross_blue.bmp");
  if (transparent_surface == NULL) {
    errorAndExit("SDL_LoadBMP");
  }
  transparent_surface = transparentSdlSurface(transparent_surface);
  if (transparent_surface == NULL) {
    errorAndExit("tranparentSdlSurface");
  }

  // SDL_Surface を OpenGL に登録する
  GL_Texture* cross_texture = Gui_LoadTexture(cross_surface);
  if (cross_texture == NULL) {
    printf("Gui_LoadTexture: %s\n", "cross_surface");
    exit(1);
  }
  GL_Texture* transparent_texture = Gui_LoadTexture(transparent_surface);
  if (transparent_texture == NULL) {
    printf("Gui_LoadTexture: %s\n", "transparent_surface");
    exit(1);
  }

  // 画面へのテクスチャ描画
  SDL_Rect pos;
  set_SdlRect(&pos, 0, 0, 32, 32);
  Gui_BlitTexture(cross_texture, NULL, &pos);

  // 部分描画
  SDL_Rect area;
  set_SdlRect(&area, 0, 0, 16, 16);
  set_SdlRect(&pos, 48 + 32 + 16, 0, 16, 16);
  Gui_BlitTexture(cross_texture, &area, &pos);

  set_SdlRect(&area, 16, 16, 16, 16);
  set_SdlRect(&pos, 48 + 32 + 16 + 16, 0 + 16, 16, 16);
  Gui_BlitTexture(cross_texture, &area, &pos);

  // 一部の色を透明扱いにして描画
  set_SdlRect(&pos, 32 + 16, 0, 32, 32);
  Gui_BlitTexture(transparent_texture, NULL, &pos);


  // 描画の反映
  SDL_GL_SwapBuffers();
  SDL_Delay(1000);

  // リソースの解放
  Gui_FreeSurface(cross_texture);
  SDL_FreeSurface(cross_surface);

  return 0;
}
