#include <SDL.h>
#include <SDL_image.h>
#include <ft2build.h>
#include <freetype/freetype.h>
#include <freetype/ftglyph.h>
#include <math.h>
#include <stdarg.h>
#include <string.h>

#include "main.h"
#include "log.h"
#include "pack.h"
#include "opengl.h"

#define SCREEN_W    gl_screen.w
#define SCREEN_H    gl_screen.h

#define FONT_DEF "../gfx/fonts/FreeSans.ttf"

// The screen info, gives data of current opengl settings.
gl_info gl_screen;

// Our precious camera.
Vec2* gl_camera;

// Default font.
gl_font gl_defFont;

// Misc.
static int SDL_VFlipSurface(SDL_Surface* surface);
static int pot(int n);
// gl_texture.
static GLuint gl_loadSurface(SDL_Surface* surface, int* rw, int* rh);
// Gl font.
static void gl_fontMakeDList(FT_Face face, char ch, GLuint list_base, GLuint* tex_base);

// ================
// MISC!
// ================

// Get me the closest power of two plox.
static int pot(int n) {
  int i = 1;
  while(i < n)
    i<<=1;
  return i;
}

// Flips the surface vertically. Return 0 on success.
static int SDL_VFlipSurface(SDL_Surface* surface) {
  // Flip the image.
  Uint8* rowhi, *rowlo, *tmpbuf;
  int y;

  tmpbuf = (Uint8*)malloc(surface->pitch);
  if(tmpbuf == NULL) {
    WARN("Out of memory");
    return -1;
  }

  rowhi = (Uint8*)surface->pixels;
  rowlo = rowhi + (surface->h * surface->pitch) - surface->pitch;
  for(y = 0; y < surface->h / 2; ++y) {
    memcpy(tmpbuf, rowhi, surface->pitch);
    memcpy(rowhi, rowlo, surface->pitch);
    memcpy(rowlo, tmpbuf, surface->pitch);
    rowhi += surface->pitch;
    rowlo -= surface->pitch;
  }
  free(tmpbuf);
  // Might be obvious, but I'm done flipping.
  return 0;
}

// ================
// TEXTURE!
// ================

// Returns the texture ID.
// Stores real sizes in rw/rh (from POT padding).
static GLuint gl_loadSurface(SDL_Surface* surface, int* rw, int* rh) {
  GLuint texture;
  SDL_Surface* tmp;
  Uint32 saved_flags;
  Uint8  saved_alpha;
  int potw, poth;
  
  // Make size power of two.
  potw = pot(surface->w);
  poth = pot(surface->h);
  if(rw)*rw = potw;
  if(rh)*rh = poth;

  if(surface->w != potw || surface->h != poth) {
    // Size isn't original.
    SDL_Rect rtemp;
    rtemp.x = rtemp.y = 0;
    rtemp.w = surface->w;
    rtemp.h = surface->h;

    // Save alpha.
    saved_flags = surface->flags & (SDL_SRCALPHA | SDL_RLEACCELOK);
    saved_alpha = surface->format->alpha;
    if((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA)
      SDL_SetAlpha(surface, 0, 0);

    // Create the temp POT surface.
    tmp = SDL_CreateRGBSurface(SDL_SRCCOLORKEY,
          potw, poth, surface->format->BytesPerPixel*8, RGBMASK);
    if(tmp == NULL) {
      WARN("Unable to create POT surface %s", SDL_GetError());
      return 0;
    }
    if(SDL_FillRect(tmp, NULL, SDL_MapRGBA(surface->format, 0, 0, 0, SDL_ALPHA_TRANSPARENT))) {
      WARN("Unable to fill rect: %s", SDL_GetError());
      return 0;
    }

    SDL_BlitSurface(surface, &rtemp, tmp, &rtemp);
    SDL_FreeSurface(surface);

    surface = tmp;

    // Set saved alpha.
    if((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA)
      SDL_SetAlpha(surface, 0, 0);

    // Create the temp POT surface.
    tmp = SDL_CreateRGBSurface(SDL_SRCCOLORKEY,
          potw, poth, surface->format->BytesPerPixel*8, RGBMASK);
    if(tmp == NULL) {
      WARN("Unable to create POT surface %s", SDL_GetError());
      return 0;
    }
    if(SDL_FillRect(tmp, NULL, SDL_MapRGBA(surface->format, 0, 0, 0, SDL_ALPHA_TRANSPARENT))) {
      WARN("Unable to fill rect: %s", SDL_GetError());
      return 0;
    }

    SDL_BlitSurface(surface, &rtemp, tmp, &rtemp);
    SDL_FreeSurface(surface);

    surface = tmp;
  
    // Set saved alpha.
    if((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA)
      SDL_SetAlpha(surface, saved_flags, saved_alpha);
  }

  glGenTextures(1, &texture); // Create the texure.
  glBindTexture(GL_TEXTURE_2D, texture); // Load the texture.

  // Filtering, LINEAR is better for scaling, nearest looks nicer, LINEAR
  // also seems to create a bit of artifacts around the edges.
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

  SDL_LockSurface(surface);
  glTexImage2D(GL_TEXTURE_2D, 0, surface->format->BytesPerPixel,
        surface->w, surface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surface->pixels);

  SDL_UnlockSurface(surface);
  SDL_FreeSurface(surface);

  return texture;
}

// Load the SDL_Surface to an openGL texture.
gl_texture* gl_loadImage(SDL_Surface* surface) {
  int rw, rh;

  // Set up the texture defaults.
  gl_texture* texture = MALLOC_L(gl_texture);
  texture->w = (double)surface->w;
  texture->h = (double)surface->h;
  texture->sx = 1.;
  texture->sy = 1.;

  texture->texture = gl_loadSurface(surface, &rw, &rh);

  texture->rw = (double)rw;
  texture->rh = (double)rh;
  texture->sw = texture->w;
  texture->sh = texture->h;
  
  return texture;
}

// Load the image directly as an opengl texture.
gl_texture* gl_newImage(const char* path) {
  SDL_Surface* tmp, *surface;
  uint32_t filesize;
  char* buf = pack_readfile(DATA, (char*)path, &filesize);
  if(buf == NULL) {
    ERR("Loading surface from packfile.");
    return NULL;
  }
  SDL_RWops* rw = SDL_RWFromMem(buf, filesize);
  tmp = IMG_Load_RW(rw, 1);
  free(buf);

  if(tmp == 0) {
    ERR("'%s' could not be opened: %s", path, IMG_GetError());
    return NULL;
  }

  surface = SDL_DisplayFormatAlpha(tmp); // Sets the surface to what we use.
  if(surface == 0) {
    WARN("Error converting image to screen format: %s", SDL_GetError());
    return NULL;
  }

  SDL_FreeSurface(tmp); // Free the temp surface.

  if(SDL_VFlipSurface(surface)) {
    WARN("Error flipping surface");
    return NULL;
  }
  return gl_loadImage(surface);
}

// Load the texture immediately, but also set is as a sprite.
gl_texture* gl_newSprite(const char* path, const int sx, const int sy) {
  gl_texture* texture;
  if((texture = gl_newImage(path)) == NULL)
    return NULL;
  texture->sx = (double)sx;
  texture->sy = (double)sy;
  texture->sw = texture->w/texture->sx;
  texture->sh = texture->h/texture->sy;
  return texture;
}

// Free the texture.
void gl_freeTexture(gl_texture* texture) {
  glDeleteTextures(1, &texture->texture);
  free(texture);
}

// ================
// BLITTING!
// ================

// Blit the sprite at given position.
void gl_blitSprite(const gl_texture* sprite, const Vec2* pos, const int sx, const int sy) {
  // Don't bother drawing if offscreen -- waste of cycles.
  if(fabs(VX(*pos) -VX(*gl_camera)) > gl_screen.w / 2 + sprite->sw / 2 ||
        fabs(VY(*pos) -VY(*gl_camera)) > gl_screen.h / 2 + sprite->sh / 2)
    return;

  glEnable(GL_TEXTURE_2D);
  glMatrixMode(GL_TEXTURE);
  glPushMatrix();
  glTranslated(sprite->sw * (double)(sx)/sprite->rw,
        sprite->sh*(sprite->sy-(double)sy-1)/sprite->rh, 0.);

  glMatrixMode(GL_PROJECTION);
  glPushMatrix(); // Projection translation matrix.
  glTranslated(VX(*pos) - VX(*gl_camera) - sprite->sw/2.,
        VY(*pos) -VY(*gl_camera) - sprite->sh/2., 0.);
  glScalef((double)gl_screen.w/SCREEN_W, (double)gl_screen.h/SCREEN_H, 0.);

  // Actual blitting....
  glBindTexture(GL_TEXTURE_2D, sprite->texture);
  glBegin(GL_TRIANGLE_STRIP);
    glColor4d(1., 1., 1., 1.);
    glTexCoord2d(0., 0.);
      glVertex2d(0., 0.);
    glTexCoord2d(sprite->sw/sprite->rw, 0.);
      glVertex2d(sprite->sw, 0.);
    glTexCoord2d(0., sprite->sh/sprite->rh);
      glVertex2d(0., sprite->sh);
    glTexCoord2d(sprite->sw/sprite->rw, sprite->sh/sprite->rh);
      glVertex2d(sprite->sw, sprite->sh);
  glEnd();

  glPopMatrix(); // Projection translation matrix.

  glMatrixMode(GL_TEXTURE);
  glPopMatrix(); // Sprite translation matrix.
  
  glDisable(GL_TEXTURE_2D);
}

// Just straight out blit the thing at position.
void gl_blitStatic(const gl_texture* texture, const Vec2* pos) {
  glEnable(GL_TEXTURE_2D);
  glMatrixMode(GL_PROJECTION);
  glPushMatrix(); // Set up translation matrix.
  glTranslated(VX(*pos) - (double)gl_screen.w/2., VY(*pos) - (double)gl_screen.h/2., 0);
  glScaled((double)gl_screen.w/SCREEN_W, (double)gl_screen.h/SCREEN_H, 0.);
  
  // Actual blitting..
  glBindTexture(GL_TEXTURE_2D, texture->texture);
  glBegin(GL_TRIANGLE_STRIP);
    glColor4ub(1., 1., 1., 1.);
    glTexCoord2d(0., 0.);
      glVertex2d(0., 0.);
    glTexCoord2d(texture->w/texture->rw, 0.);
      glVertex2d(texture->w, 0.);
    glTexCoord2d(0., texture->h/texture->rh);
      glVertex2d(0., texture->h);
    glTexCoord2d(texture->w/texture->rw, texture->h/texture->rh);
      glVertex2d(texture->w, texture->h);
  glEnd();

  glPopMatrix(); // Pop the translation matrix.

  glDisable(GL_TEXTURE_2D);
}

// Bind our precious camera to a vector.
void gl_bindCamera(const Vec2* pos) {
  gl_camera = (Vec2*)pos;
}

// Print text on screen! YES!!!! Just like printf! But different!
// Defaults ft_font to gl_defFont if NULL.
void gl_print(const gl_font* ft_font, const Vec2* pos, const char* fmt, ...) {
  //float h = ft_font->h / .63; // Slightly increases font size.
  char text[256];
  va_list ap;

  if(ft_font == NULL) ft_font = &gl_defFont;

  if(fmt == NULL) return;
  else {
    // convert the symbols to text.
    va_start(ap, fmt);
    vsprintf(text, fmt, ap);
    va_end(ap);
  }

  glEnable(GL_TEXTURE_2D);

  glListBase(ft_font->list_base);

  glMatrixMode(GL_PROJECTION);

  glPushMatrix(); // Translation matrix.
  glTranslated(VX(*pos) - (double)gl_screen.w/2., VY(*pos) - (double)gl_screen.h/2., 0);

  glColor4d(1., 1., 1., 1.);
  glCallLists(strlen(text), GL_UNSIGNED_BYTE, &text);

  glPopMatrix(); // Translation matrix.
  glDisable(GL_TEXTURE_2D);
}

// ================
// FONT!
// ================
static void gl_fontMakeDList(FT_Face face, char ch, GLuint list_base, GLuint* tex_base) {
  FT_Glyph glyph;
  FT_Bitmap bitmap;
  GLubyte* expanded_data;
  int w, h;
  int i, j;

  if(FT_Load_Glyph(face, FT_Get_Char_Index(face, ch), FT_LOAD_DEFAULT))
    WARN("FT_Load_Glyph failed");

  if(FT_Get_Glyph(face->glyph, &glyph))
    WARN("FT_Ge_Glyph failed");

  // Convert your glyph to a bitmap.
  FT_Glyph_To_Bitmap(&glyph, ft_render_mode_normal, 0, 1);
  FT_BitmapGlyph bitmap_glyph = (FT_BitmapGlyph)glyph;

  bitmap = bitmap_glyph->bitmap; // To simplify.

  // Need the POT wrapping for GL.
  w = pot(bitmap.width);
  h = pot(bitmap.rows);

  // Memory for textured data.
  // Bitmap is useing two channels, one for luminosity and one for alpha.
  expanded_data = (GLubyte*)malloc(sizeof(GLubyte)*2*w*h);
  for(j = 0; j < h; j++) {
    for(i = 0; i < w; i++) {
      expanded_data[2*(i+j*w)] = expanded_data[2*(i+j*w)+1] =
          (i >= bitmap.width || j >= bitmap.rows) ? 0 : bitmap.buffer[i + bitmap.width*j];
    }
  }
  // Create the GL texture.
  glBindTexture(GL_TEXTURE_2D, tex_base[(int)ch]);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, expanded_data);

  free(expanded_data); // No need for this now.

  // Create the display lists.
  glNewList(list_base+ch, GL_COMPILE);

  // Corrects a spacing flaw between letters and
  // downwards correction for letters like g or y.
  glPushMatrix();
  glTranslated(bitmap_glyph->left, bitmap_glyph->top-bitmap.rows, 0);

  // Take the opengl POT wrapping into account.
  double x = (double)bitmap.width/(double)w;
  double y = (double)bitmap.rows/(double)h;

  // Draw the texture mapped quad.
  glBindTexture(GL_TEXTURE_2D, tex_base[(int)ch]);
  glBegin(GL_TRIANGLE_STRIP);
    glTexCoord2d(0, 0);
      glVertex2d(0, bitmap.rows);
    glTexCoord2d(x, 0);
      glVertex2d(bitmap.width, bitmap.rows);
    glTexCoord2d(0, y);
      glVertex2d(0, 0);
    glTexCoord2d(x, y);
      glVertex2d(bitmap.width, 0);
  glEnd();

  glPopMatrix();
  glTranslated(face->glyph->advance.x >> 6, 0,0);

  // End of the display list.
  glEndList();
}

void gl_fontInit(gl_font* font, const char* fname, unsigned int h) {
  if(font == NULL) font = &gl_defFont;
  
  uint32_t bufsize;
  FT_Byte* buf = pack_readfile(DATA, (fname) ? fname : FONT_DEF, &bufsize);

  font->textures = malloc(sizeof(GLuint)*128);
  font->h = h;

  // Create a FreeType font library.
  FT_Library library;
  if(FT_Init_FreeType(&library)) {
    WARN("FT_Init_FreeType failed");
  }

  // Objects that freetype uses to store font info.
  FT_Face face;
  if(FT_New_Memory_Face(library, buf, bufsize, 0, &face))
    WARN("FT_New_Memory_Face failed loading library from %s", fname);

  // FreeType is pretty nice and measures using 1/64 of a pixel, therfore expand.
  FT_Set_Char_Size(face, h << 6, h << 6, 96, 96);

  // Have OpenGL allocate space for the textures / display lists.
  font->list_base = glGenLists(128);
  glGenTextures(128, font->textures);

  // Create each of the font display lists.
  unsigned char i;
  for(i = 0; i < 128; i++)
    gl_fontMakeDList(face, i, font->list_base, font->textures);

  // We can now free the face and library.
  FT_Done_Face(face);
  FT_Done_FreeType(library);
  free(buf);
}

void gl_freeFont(gl_font* font) {
  if(font == NULL) font = &gl_defFont;
  glDeleteLists(font->list_base, 128);
  glDeleteTextures(128, font->textures);
  free(font->textures);
}

// ================
// GLOBAL.
// ================

// Initialize SDL/OpenGL etc.
int gl_init(void) {
  int depth, i, supported = 0;
  SDL_Rect** modes;
  int flags = SDL_OPENGL;
  flags |= SDL_FULLSCREEN * gl_screen.fullscreen;

  // Initializes video.
  if(SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
    WARN("Unable to initialize SDL: %s", SDL_GetError());
    return -1;
  }

  // FFUUUU Ugly cursor thing.
  // -- Ok, Maybe for now.
  //SDL_ShowCursor(SDL_DISABLE);

  // Get available fullscreen modes.
  if(gl_screen.fullscreen) {
    modes = SDL_ListModes(NULL, SDL_OPENGL | SDL_FULLSCREEN);
    if(modes == NULL) {
      WARN("No fullscreen modes available");
      if(flags & SDL_FULLSCREEN) {
        WARN("Disabling fullscreen mode");
        flags ^= SDL_FULLSCREEN;
      }
    }
    else if(modes == (SDL_Rect**)-1)
      DEBUG("All fullscreen modes available");
    else {
      DEBUG("Available fullscreen modes:");
      for(i = 0; modes[i]; ++i) {
        DEBUG("\t%dx%d", modes[i]->w, modes[i]->h);
        if(flags & SDL_FULLSCREEN && modes[i]->w == gl_screen.w && modes[i]->h == gl_screen.h)
          supported = 1;
      }
    }
    // Make sure fullscreen mode is supported.
    if(flags & SDL_FULLSCREEN && !supported) {
      WARN("Fullscreen mode %dx%d is not supported by your current setup, switching to another mode",
            gl_screen.w, gl_screen.h);
      gl_screen.w = modes[0]->w;
      gl_screen.h = modes[0]->h;
    }
  }

  // Test the setup.
  depth = SDL_VideoModeOK(gl_screen.w, gl_screen.h, gl_screen.depth, flags);
  if(depth != gl_screen.depth)
    WARN("Depth: %d bpp unavailable, will use %d bpp", gl_screen.depth, depth);

  gl_screen.depth = depth;

  // Actually creating the screen.
  if(SDL_SetVideoMode(gl_screen.w, gl_screen.h, gl_screen.depth, flags) == NULL) {
    WARN("Unable to create OpenGL window: %s", SDL_GetError());
    SDL_Quit();
    return -1;
  }

  // Grab some info.
  SDL_GL_GetAttribute(SDL_GL_RED_SIZE,      &gl_screen.r);
  SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE,    &gl_screen.g);
  SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE,     &gl_screen.b);
  SDL_GL_GetAttribute(SDL_GL_ALPHA_SIZE,    &gl_screen.a);
  SDL_GL_GetAttribute(SDL_GL_DOUBLEBUFFER,  &gl_screen.doublebuf);
  gl_screen.depth = gl_screen.r + gl_screen.g + gl_screen.b + gl_screen.a;

  // Debug heaven.
  DEBUG("OpenGL Window Created: %dx%d@%dbpp %s", gl_screen.w, gl_screen.h, gl_screen.depth,
        gl_screen.fullscreen ? "fullscreen" : "window");
  DEBUG("r: %d, g: %d, b: %d, a: %d, doublebuffer: %s", gl_screen.r, gl_screen.g, gl_screen.b, gl_screen.a,
        gl_screen.doublebuf ? "yes" : "no");
  DEBUG("Renderer: %s", glGetString(GL_RENDERER));

  // Some openGL options.
  glClearColor(0., 0., 0., 1.);
  glDisable(GL_DEPTH_TEST); // Set for doing 2D shidazles.
  //glEnable(GL_TEXTURE_2D);
  glDisable(GL_LIGHTING); // No lighting, it is done when rendered.
  glMatrixMode(GL_PROJECTION);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-SCREEN_W /2,   // Left edge.
          SCREEN_W  /2,   // Right edge.
          -SCREEN_H /2,   // Bottom edge.
          SCREEN_H  /2,   // Top edge.
          -1.,            // Near.
          1.);            // Far.
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Alpha.
  glEnable(GL_BLEND);

  glPointSize(1.); // Default is 1.
  glClear(GL_COLOR_BUFFER_BIT);

  return 0;
}

// Clean up our mess.
void gl_exit(void) {
  //SDL_ShowCursor(SDL_ENABLE);
  SDL_Quit();
}