/*!
  \example glTextureDraw.cpp

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

  \author Satofumi KAMIMURA

  $Id: glTextureDraw.cpp 1814 2010-05-03 07:57:38Z 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;
}
