#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include "SDL_version.h"
#include <png.h>
#include <ft2build.h>
#include <freetype2/freetype.h>
#include <freetype2/ftglyph.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>

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

/* Requirements. */
#define OPENGL_REQ_MULTITEX 2 /**< 2 is minimum OpenGL 1.2. Must have. */

glInfo gl_screen; /**< Give data of current opengl settings. */
Vec2* gl_camera;  /**< Camera we are using. */

/* offsets to Adjust the pilot's place onscreen */
/*to be in the middle, even with the GUI. */
extern double gui_xoff;
extern double gui_yoff;

/*
 * Graphic list.
 */
typedef struct glTexList_ {
  struct glTexList_* next;  /**< Next in linked list */
  glTexture* tex;           /**< Assosciated texture. */
  int used;                 /**< Count how many times texture is being used. */
} glTexList;
static glTexList* texture_list = NULL;

/* Misc. */
static int SDL_VFlipSurface(SDL_Surface* surface);
static int SDL_IsTrans(SDL_Surface* s, int x, int y);
static uint8_t* SDL_MapTrans(SDL_Surface* s);
/* glTexture. */
static GLuint gl_loadSurface(SDL_Surface* surface, int* rw, int* rh);
static glTexture* gl_loadNewImage(const char* path);
static void gl_blitTexture(const glTexture* texture,
    const double x, const double y,
    const double tx, const double ty, const glColour* c);

/* PNG. */
int write_png(const char* file_name, png_bytep* rows, int w, int h,
              int colourtype, int bitdepth);
/* Global. */
static GLboolean gl_hasExt(char* name);

/*
 * MISC!
 */

/* Get the closest power of two. */
int gl_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 = 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;
}

/* Return true if position (x,y) of s is transparent. */
static int SDL_IsTrans(SDL_Surface* s, int x, int y) {
  int bpp = s->format->BytesPerPixel;
  /* p is the address to the pixel we want to retrieve. */
  Uint8* p = (Uint8*)s->pixels + y * s->pitch + x*bpp;

  Uint32 pixelcolour = 0;

  switch(bpp) {
  case 1:
    pixelcolour = *p;
    break;
  case 2:
    pixelcolour = *(Uint16*)p;
    break;
  case 3:
    if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
      pixelcolour = p[0] << 16 | p[1] << 8 | p[2];
    else
      pixelcolour = p[0] | p[1] << 8 | p[2] << 16;
    break;
  case 4:
    pixelcolour = *(Uint32*)p;
    break;
  }
  /* Test whetehr the pixels color is equal to color of */
  /*transparent pixels for that surface. */
  return (pixelcolour == s->format->colorkey);
}

/* Map the surface transparancy. */
/* Return 0 on success. */
static uint8_t* SDL_MapTrans(SDL_Surface* s) {
  /* Allocate memory for just enough bits to hold all the data we need. */
  int size = s->w*s->h/8 + ((s->w*s->h%8)?1:0);
  uint8_t* t = malloc(size);
  memset(t, 0, size); /* *must* be set to zero. */

  if(t == NULL) {
    WARN("Out of memeory");
    return NULL;
  }

  int i, j;
  for(i = 0; i < s->h; i++)
    for(j = 0; j < s->w; j++) /* Set each bit to be 1 if not transparent or 0 if it is. */
      t[(i*s->w+j)/8] |= (SDL_IsTrans(s,j,i)) ? 0 : (1<<((i*s->w+j)%8));

  return t;
}

/* Take a screenshot. */
void gl_screenshot(const char* filename) {
  SDL_Surface* screen = SDL_GetVideoSurface();
  unsigned rowbytes = screen->w * 3;
  unsigned char screenbuf[screen->h][rowbytes], *rows[screen->h];
  int i;

  glReadPixels(0, 0, screen->w, screen->h, GL_RGB, GL_UNSIGNED_BYTE, screenbuf);

  for(i = 0; i < screen->h; i++) rows[i] = screenbuf[screen->h - i - 1];
  write_png(filename, rows, screen->w, screen->h, PNG_COLOR_TYPE_RGB, 8);

  gl_checkErr();
}

/* Save a surface to a file as a png. */
int SDL_savePNG(SDL_Surface* surface, const char* file) {
  static unsigned char** ss_rows;
  static int ss_size;
  static int ss_w, ss_h;
  SDL_Surface* ss_surface;
  SDL_Rect ss_rect;
  int r, i;
  int alpha = 0;
  int pixel_bits = 32;

  unsigned surf_flags;
  unsigned surf_alpha;

  ss_rows = 0;
  ss_size = 0;
  ss_surface = 0;

  ss_w = surface->w;
  ss_h = surface->h;

  if(surface->format->Amask) {
    alpha = 1;
    pixel_bits = 32;
  } else {
    pixel_bits = 24;
  }

  ss_surface = SDL_CreateRGBSurface(SDL_SWSURFACE | SDL_SRCALPHA, ss_w, ss_h,
      pixel_bits, RGBAMASK);

  if(ss_surface == 0) {
    return -1;
  }

  surf_flags = surface->flags & (SDL_SRCALPHA | SDL_SRCCOLORKEY);
  surf_alpha = surface->format->alpha;
  if(surf_flags & SDL_SRCALPHA)
    SDL_SetAlpha(surface, 0, SDL_ALPHA_OPAQUE);
  if(surf_flags & SDL_SRCCOLORKEY)
    SDL_SetColorKey(surface, 0, surface->format->colorkey);

  ss_rect.x = 0;
  ss_rect.y = 0;
  ss_rect.w = ss_w;
  ss_rect.h = ss_h;
  SDL_BlitSurface(surface, &ss_rect, ss_surface, 0);

  if(ss_size == 0) {
    ss_size = ss_h;
    ss_rows = (unsigned char**)malloc(sizeof(unsigned char*)*ss_size);
    if(ss_rows == 0) {
      return -1;
    }
  }
  if(surf_flags & SDL_SRCALPHA)
    SDL_SetAlpha(surface, SDL_SRCALPHA, (Uint8)surf_alpha);
  if(surf_flags & SDL_SRCCOLORKEY)
    SDL_SetColorKey(surface, SDL_SRCCOLORKEY, surface->format->colorkey);

  for(i = 0; i < ss_h; i++) {
    ss_rows[i] = ((unsigned char*)ss_surface->pixels) + i * ss_surface->pitch;
  }

  if(alpha) {
    r = write_png(file, ss_rows, surface->w, surface->h, PNG_COLOR_TYPE_RGB_ALPHA, 8);
  } else {
    r = write_png(file, ss_rows, surface->w, surface->h, PNG_COLOR_TYPE_RGB, 8);
  }
  
  free(ss_rows);
  SDL_FreeSurface(ss_surface);
  ss_surface = NULL;

  return r;
}

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

/* Returns the texture ID.
 * Stores real sizes in rw/rh (from POT padding).
 */
SDL_Surface* gl_prepareSurface(SDL_Surface* surface) {
  SDL_Surface* tmp;
  Uint32 saved_flags;
  Uint8  saved_alpha;
  int potw, poth;
  SDL_Rect rtemp;

  /* Make size power of two. */
  potw = gl_pot(surface->w);
  poth = gl_pot(surface->h);

  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, SDL_ALPHA_OPAQUE);
  if((saved_flags & SDL_SRCALPHA) == SDL_SRCALPHA)
    SDL_SetColorKey(surface, 0, surface->format->colorkey);

  /* Create the temp POT surface. */
  tmp = SDL_CreateRGBSurface(SDL_SRCCOLORKEY,
      potw, poth, surface->format->BytesPerPixel*8, RGBAMASK);
  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;
  }

  /* Change the surface to the new blitted one. */
  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);

  return surface;
}

/*
 * Return 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;

  surface = gl_prepareSurface(surface);
  if(rw != NULL) (*rw) = surface->w;
  if(rh != NULL) (*rh) = surface->h;

  /* Opengl texture binding. */
  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);

  /* Always wrap just in case. */
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);

  /* Now lead the texture data up. */
  SDL_LockSurface(surface);
  glTexImage2D(GL_TEXTURE_2D, 0, surface->format->BytesPerPixel,
               surface->w, surface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surface->pixels);

  /* Cleanup. */
  SDL_UnlockSurface(surface);
  SDL_FreeSurface(surface);

  gl_checkErr();

  return texture;
}

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

  /* Set up the texture defaults. */
  glTexture* texture = MALLOC_L(glTexture);
  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;

  texture->trans  = NULL;
  texture->name   = NULL;

  return texture;
}

/* Load the image if not already done. */
glTexture* gl_newImage(const char* path) {
  glTexList* cur, *last;

  /* Check to see if it already exists. */
  if(texture_list != NULL) {
    for(cur = texture_list; cur != NULL; cur = cur->next) {
      if(strcmp(path, cur->tex->name)==0) {
        cur->used += 1;
        return cur->tex;
      }
      last = cur;
    }
  }


  /* Create the new node. */
  cur = malloc(sizeof(glTexList));
  cur->next = NULL;
  cur->used = 1;

  /* Load the image. */
  cur->tex = gl_loadNewImage(path);

  if(texture_list == NULL) /* Special condition - creating new list. */
    texture_list = cur;
  else
    last->next = cur;

  return cur->tex;
}

/* Load the image as an opengl texture directly. */
static glTexture* gl_loadNewImage(const char* path) {
  SDL_Surface* tmp, *surface;
  glTexture* t;
  uint8_t* trans = NULL;
  uint32_t filesize;
  char* buf;

  /* Load from packfile. */
  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. */

  /* We have to flip our surfaces to match the ortho. */
  if(SDL_VFlipSurface(surface)) {
    WARN("Error flipping surface");
    return NULL;
  }

  /* Do after flipping for collision detection. */
  SDL_LockSurface(surface);
  trans = SDL_MapTrans(surface);
  SDL_UnlockSurface(surface);

  /* Set the texture. */
  t = gl_loadImage(surface);
  t->trans = trans;
  t->name = strdup(path);
  return t;
}

/* Load the texture immediately, but also set is as a sprite. */
glTexture* gl_newSprite(const char* path, const int sx, const int sy) {
  glTexture* texture;
  if((texture = gl_newImage(path)) == NULL)
    return NULL;

  /*
   * Will possibly overwrite an existing textures properties
   * so we have to load some texture, always the same sprite.
   */
  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(glTexture* texture) {
  glTexList* cur, *last;

  /* Shouldn't be NULL (won't segfault though). */
  if(texture == NULL) {
    WARN("Attempting to free NULL texture!");
    return;
  }

  /* See if we can find it in stack. */
  last = NULL;
  for(cur = texture_list; cur != NULL; cur = cur->next) {
    if(cur->tex == texture) { /* Found it! */
      cur->used--;
      if(cur->used <= 0) {
        /* Not used anymore - Free the texture. */
        glDeleteTextures(1, &texture->texture);
        if(texture->trans != NULL) free(texture->trans);
        if(texture->name != NULL)  free(texture->name);
        free(texture);

        /* Free the list node. */
        if(last == NULL) { /* Case there's no texture before it. */
          if(cur->next != NULL)
            texture_list = cur->next;
          else /* Case it's the last texture. */
            texture_list = NULL;
        } else
          last->next = cur->next;
        free(cur);
      }
      return; /* We already found it so we can exit. */
    }
    last = cur;
  }

  /* Not found. */
  if(texture->name != NULL) /* Surface will have NULL names. */
    WARN("Attempring to free texture '%s' not found in stack!", texture->name);

  /* Free anyways. */
  glDeleteTextures(1, &texture->texture);
  if(texture->trans != NULL)  free(texture->trans);
  if(texture->name != NULL)   free(texture->name);
  free(texture);

  gl_checkErr();
}

/* Return true if pixel at pos (x,y) is transparent. */
int gl_isTrans(const glTexture* t, const int x, const int y) {
  return !(t->trans[(y*(int)(t->w)+x)/8] & (1<<((y*(int)(t->w)+x)%8)));
}

/* Set x and y to be the appropriate sprite for glTexture using dir. */
void gl_getSpriteFromDir(int* x, int* y, const glTexture* t, const double dir) {
  int s, sx, sy;

  double shard, rdir;

  /* What each image represents in angle. */
  shard = 2.0*M_PI / (t->sy*t->sx);

  rdir = dir + shard/2.;
  if(rdir < 0.) rdir = 0.;

  /* Now calculate the sprite we need. */
  s = (int)(rdir / shard);
  sx = t->sx;
  sy = t->sy;

  /* Make sure the sprite is "in range". */
  if(s > (sy*sx-1)) s = s % (sy*sx);

  (*x) = s % sx;
  (*y) = s / sx;
}

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

static void gl_blitTexture(const glTexture* texture,
    const double x, const double y,
    const double tx, const double ty, const glColour* c) {

  double tw, th;

  /* Texture dimensions. */
  tw = texture->sw / texture->rw;
  th = texture->sh / texture->rh;

  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture->texture);
  glBegin(GL_QUADS);
    /* Set colour or default if not set. */
    if(c == NULL) glColor4d(1., 1., 1., 1.);
    else COLOUR(*c);

    glTexCoord2d(tx, ty);
    glVertex2d(x, y);

    glTexCoord2d(tx + tw, ty);
    glVertex2d(x + texture->sw, y);

    glTexCoord2d(tx + tw, ty + th);
    glVertex2d(x + texture->sw, y + texture->sh);

    glTexCoord2d(tx, ty + th);
    glVertex2d(x, y + texture->sh);
  glEnd();
  glDisable(GL_TEXTURE_2D);

  /* Did anything fail? */
  gl_checkErr();
}

/* Blit the sprite at given position. */
void gl_blitSprite(const glTexture* sprite, const double bx, const double by,
                   const int sx, const int sy, const glColour* c) {

  double x, y, tx, ty;

  /* Calculate position - we'll use relative coords to player. */
  x = bx - VX(*gl_camera) - sprite->sw/2. + gui_xoff;
  y = by - VY(*gl_camera) - sprite->sh/2. + gui_yoff;

  /* Check if inbounds. */
  if((fabs(x) > SCREEN_W/2 + sprite->sw) ||
      (fabs(y) > SCREEN_H/2 + sprite->sh))
    return;

  /* Texture coords. */
  tx = sprite->sw * (double)(sx)/sprite->rw;
  ty = sprite->sh * (sprite->sy-(double)sy-1)/sprite->rh;

  /* Actual blitting. */
  gl_blitTexture(sprite, x, y, tx, ty, c);
}

/* Blit the sprite at pos (blits absolute position). */
void gl_blitStaticSprite(const glTexture* sprite, const double bx,
    const double by, const int sx, const int sy, const glColour* c) {
  
  double x, y, tx, ty;

  x = bx - (double)SCREEN_W/2.;
  y = by - (double)SCREEN_H/2.;

  /* Texture coords. */
  tx = sprite->sw*(double)(sx)/sprite->rw;
  ty = sprite->sh*(sprite->sy-(double)sy-1)/sprite->rh;

  /* Actual blitting. */
  gl_blitTexture(sprite, x, y, tx, ty, c); 
}

/* Like gl_blitStatic but scales to size. */
void gl_blitScale(const glTexture* texture,
    const double bx, const double by,
    const double bw, const double bh, const glColour* c) {

  double x, y;
  double tw, th;
  double tx, ty;

  /* Here we use absolute coords. */
  x = bx - (double)SCREEN_W/2.;
  y = by - (double)SCREEN_H/2.;

  /* Texture dimensions. */
  tw = texture->sw / texture->rw;
  th = texture->sh / texture->rh;
  tx = ty = 0.;

  glEnable(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, texture->texture);
  glBegin(GL_QUADS);
    /* Set colour or default if not set. */
    if(c == NULL) glColor4d(1., 1., 1., 1.);
    else COLOUR(*c);

    glTexCoord2d(tx, ty);
    glVertex2d(x, y);

    glTexCoord2d(tx+tw, ty);
    glVertex2d(x+bw, y);
    
    glTexCoord2d(tx+tw, ty+th);
    glVertex2d(x+bw, y+bh);

    glTexCoord2d(tx, ty+th);
    glVertex2d(x, y+bh);
  glEnd();
  glDisable(GL_TEXTURE_2D);

  /* Anything failed? */
  gl_checkErr();
}

/* Just straight out blit the thing at position. */
void gl_blitStatic(const glTexture* texture, const double bx, const double by,
                   const glColour* c) {
  double x, y;

  /* Here we use absolute coords. */
  x = bx - (double)SCREEN_W/2.;
  y = by - (double)SCREEN_H/2.;

  /* Actual blitting.. */
  gl_blitTexture(texture, x, y, 0, 0, c);
}

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

/* Draw circles. */
void gl_drawCircle(const double cx, const double cy, const double r) {
  double x, y, p;

  x = 0;
  y = r;
  p = (5. - (r*4.)) / 4.;

  glBegin(GL_POINTS);
  glVertex2d(cx,  cy+y);
  glVertex2d(cx,   cy-y);
  glVertex2d(cx+y, cy);
  glVertex2d(cx-y, cy);

  while(x < y) {
    x++;
    if(p < 0) p += 2*(double)(x)+1;
    else p += 2*(double)(x-(--y))+1;

    if(x == 0) {
      glVertex2d(cx,   cy+y);
      glVertex2d(cx,   cy-y);
      glVertex2d(cx+y,  cy);
      glVertex2d(cx-y, cy);
    }
    else if(x == y) {
      glVertex2d(cx+x, cy+y);
      glVertex2d(cx-x,  cy+y);
      glVertex2d(cx+x,  cy-y);
      glVertex2d(cx-x, cy-y);
    }
    else if(x < y) {
      glVertex2d(cx+x,  cy+y);
      glVertex2d(cx-x, cy+y);
      glVertex2d(cx+x, cy-y);
      glVertex2d(cx-x,  cy-y);
      glVertex2d(cx+y, cy+x);
      glVertex2d(cx-y,  cy+x);
      glVertex2d(cx+y, cy-x);
      glVertex2d(cx-y, cy-x);
    }
  }
  glEnd();

  gl_checkErr();
}

/* Draw a cirlce in a rect. */
#define PIXEL(x,y) \
  if((x>rx) && (y>ry) && (x<rxw) && (y<ryh)) \
  glVertex2d(x,y)

void gl_drawCircleInRect(const double cx, const double cy, const double r,
                         const double rx, const double ry, const double rw, const double rh) {

  double rxw, ryh, x, y, p;

  rxw = rx+rw;
  ryh = ry+rh;

  /* Are we offscreen? */
  if((cx+r < rx) || (cy+r < ry) || (cx-r > rxw) || (cy-r > ryh))
    return;
  /* Can be drawn normally. */
  else if((cx-r > rx) && (cy-r > ry) && (cx+r < rxw) && (cy+r < ryh)) {
    gl_drawCircle(cx, cy, r);
    return;
  }

  x = 0;
  y = r;
  p = (5. - (r*4.)) / 4.;

  glBegin(GL_POINTS);
  PIXEL(cx,  cy+y);
  PIXEL(cx,  cy-y);
  PIXEL(cx+y, cy);
  PIXEL(cx-y, cy);

  while(x < y) {
    x++;
    if(p < 0) p += 2*(double)(x)+1;
    else p += 2*(double)(x-(--y))+1;

    if(x == 0) {
      PIXEL(cx,   cy+y);
      PIXEL(cx,   cy-y);
      PIXEL(cx+y,  cy);
      PIXEL(cx-y, cy);
    }
    else if(x == y) {
      PIXEL(cx+x, cy+y);
      PIXEL(cx-x,  cy+y);
      PIXEL(cx+x,  cy-y);
      PIXEL(cx-x, cy-y);
    }
    else if(x < y) {
      PIXEL(cx+x,  cy+y);
      PIXEL(cx-x, cy+y);
      PIXEL(cx+x, cy-y);
      PIXEL(cx-x,  cy-y);
      PIXEL(cx+y, cy+x);
      PIXEL(cx-y,  cy+x);
      PIXEL(cx+y, cy-x);
      PIXEL(cx-y, cy-x);
    }
  }
  glEnd();
  
  gl_checkErr();
}
#undef PIXEL

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

/* Check for extensions. */
static GLboolean gl_hasExt(char* name) {
  /* ====================================================== */
  /* Search for name in the extensions string. Use of strstr() */
  /* is not sufficient because extensions names can be prefixes of */
  /* other extension names. Could use strtol() but the constant */
  /* string returned by glGetString can be in read-only memory. */
  /* ====================================================== */

  char* p, *end;
  size_t len, n;

  p = (char*) glGetString(GL_EXTENSIONS);
  len = strlen(name);
  end = p + strlen(p);

  while(p < end) {
    n = strcspn(p, " ");
    if((len == n) && (strncmp(name, p, n)==0))
      return GL_TRUE;

    p += (n+1);
  }
  return GL_FALSE;
}

/* Check and report if there's been an error. */
/*#ifndef gl_checkErr // I know, I know, it's a little hackish. */
void gl_checkErr(void) {
#ifdef DEBUG
  GLenum err;
  char* errstr;

  err = glGetError();

  if(err == GL_NO_ERROR) return; /* No error. */

  switch(err) {
    case GL_INVALID_ENUM:
      errstr = "GL invalid enum";
      break;
    case GL_INVALID_VALUE:
      errstr = "GL invalid operation";
      break;
      case GL_INVALID_OPERATION:
      errstr = "GL invalid operation";
      break;
    case GL_STACK_OVERFLOW:
      errstr = "GL stack overflow";
      break;
    case GL_STACK_UNDERFLOW:
      errstr = "GL stack underflow";
      break;
    case GL_OUT_OF_MEMORY:
      errstr = "GL out of memory";
      break;
    case GL_TABLE_TOO_LARGE:
      errstr = "GL table too large";
      break;
    default:
      errstr = "GL unkown error";
      break;
  }
  WARN("OpenGL error: %s", errstr);
#endif
}
/*#endif */

/* Initialize SDL/OpenGL etc. */
int gl_init(void) {
  int doublebuf, depth, i, j, off, toff, supported, fsaa;
  SDL_Rect** modes;
  int flags = SDL_OPENGL;
  flags |= SDL_FULLSCREEN * (gl_has(OPENGL_FULLSCREEN) ? 1: 0);

  supported  = 0;

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

  /* Set opengl flags. */
  SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); /* Ideally want double buffering. */
  if(gl_has(OPENGL_FSAA)) {
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, gl_screen.fsaa);
  }
  if(gl_has(OPENGL_VSYNC))
    SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1);

  if(gl_has(OPENGL_FULLSCREEN)) {
    /* Try to use desktop resolution if nothing is specifically set. */
#if SDL_VERSION_ATLEAST(1,2,10)
    if(!gl_has(OPENGL_DIM_DEF)) {
      const SDL_VideoInfo* vidinfo = SDL_GetVideoInfo();
      gl_screen.w = vidinfo->current_w;
      gl_screen.h = vidinfo->current_h;
    }
#endif
    /* Get available modes and see what we can use. */
    modes = SDL_ListModes(NULL, SDL_OPENGL | SDL_FULLSCREEN);
    if(modes == NULL) { /* Could happen, but rare. */
      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 == SCREEN_W) &&
           (modes[i]->h == SCREEN_H))
          supported = 1; /* Mode we asked for is supported. */
      }
    }

    /* Make sure fullscreen mode is supported. */
    if((flags & SDL_FULLSCREEN) && !supported) {
      /* Try to get the closest aproximation to mode we asked for. */
      off = -1;
      j = 0;
      for(i = 0; modes[i]; i++) {
        toff = ABS(SCREEN_W-modes[i]->w) + ABS(SCREEN_H-modes[i]->h);
        if((off == -1) || (toff < off)) {
          j = i;
          off = toff;
        }
      }
      WARN("Fullscreen mode %dx%d is not supported by your setup\n"
           " Switching to %dx%d",
           SCREEN_W, SCREEN_H,
           modes[j]->w, modes[j]->h);

      gl_screen.w = modes[j]->w;
      gl_screen.h = modes[j]->h;
    }
  }

  /* Test the setup - aim for 32. */
  gl_screen.depth = 32;
  depth = SDL_VideoModeOK(SCREEN_W, SCREEN_H, gl_screen.depth, flags);
  if(depth == 0)
     WARN("Video mode %dx%d @ %d bpp not supported"
         "  going to try to create it anyway...",
         SCREEN_W, SCREEN_H, gl_screen.depth);
  if(depth != gl_screen.depth)
    LOG("Depth: %d bpp unavailable, will use %d bpp", gl_screen.depth, depth);

  gl_screen.depth = depth;

  /* Actually creating the screen. */
  if(SDL_SetVideoMode(SCREEN_W, SCREEN_H, gl_screen.depth, flags) == NULL) {
    if(gl_has(OPENGL_FSAA)) {
      LOG("Unable to create OpenGL window: Trying without FSAA.");
      gl_screen.flags &= ~OPENGL_FSAA;
      SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 0);
      SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 0);
    }
    if(SDL_SetVideoMode(SCREEN_W, SCREEN_H, gl_screen.depth, flags)==NULL) {
      ERR("Unable to create OpenGL window: %s", SDL_GetError());
      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,        &doublebuf);
  SDL_GL_GetAttribute(SDL_GL_MULTISAMPLESAMPLES,  &fsaa);
  if(doublebuf) gl_screen.flags |= OPENGL_DOUBLEBUF;
  gl_screen.depth = gl_screen.r + gl_screen.g + gl_screen.b + gl_screen.a;

  /* Get info about some extensions. */
  if(gl_hasExt("GL_ARB_vertex_program")==GL_TRUE)
    gl_screen.flags |= OPENGL_VERT_SHADER;
  if(gl_hasExt("GL_ARB_fragment_program")==GL_TRUE)
    gl_screen.flags |= OPENGL_FRAG_SHADER;

  /* Texture information. */
  glGetIntegerv(GL_MAX_TEXTURE_SIZE, &gl_screen.tex_max);
  glGetIntegerv(GL_MAX_TEXTURE_UNITS, &gl_screen.multitex_max);

  /* Debug heaven. */
  DEBUG("OpenGL Window Created: %dx%d@%dbpp %s", SCREEN_W, SCREEN_H,
        gl_screen.depth, (gl_has(OPENGL_FULLSCREEN)) ? "fullscreen" : "window");

  DEBUG("r: %d, g: %d, b: %d, a: %d, db: %s, fsaa: %d, tex: %d",
        gl_screen.r, gl_screen.g, gl_screen.b, gl_screen.a,
        gl_has(OPENGL_DOUBLEBUF) ? "yes" : "no",
        fsaa, gl_screen.tex_max);

  DEBUG("Renderer: %s", glGetString(GL_RENDERER));
  DEBUG("Version: %s", glGetString(GL_VERSION));
  /* Now check for things that can be bad. */
  if(gl_screen.multitex_max < OPENGL_REQ_MULTITEX)
    WARN("Missing texture units (%d required, %d found)",
        OPENGL_REQ_MULTITEX, gl_screen.multitex_max);
  if(gl_has(OPENGL_FSAA) && (fsaa != gl_screen.fsaa))
    WARN("Unable to get requested fsaa level (%d requested, got %d)",
        gl_screen.fsaa, fsaa);
  if(!gl_has(OPENGL_FRAG_SHADER))
    DEBUG("No fragment shader extension detected"); /* Not a warning yet. */
  DEBUG("");

  /* Some openGL options. */
  glClearColor(0., 0., 0., 1.);

  /* Enable/Disable. */
  glDisable(GL_DEPTH_TEST);   /* Set for doing 2D shidazles. */
  /*glEnable(GL_TEXTURE_2D);  // Don't enable globally, it will break non-texture blits. */
  glDisable(GL_LIGHTING);     /* No lighting, it is done when rendered. */
  glEnable(GL_BLEND);         /* Alpha blending ftw. */

  /* Models. */
  glShadeModel(GL_FLAT);      /* Default shade model. Functions should keep this when done.. */
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); /* Good blend model. */

  /* Set up the matrix. */
  gl_defViewport();

  /* Finishing touches. */
  glClear(GL_COLOR_BUFFER_BIT); /* Must clear the buffer first. */
  gl_checkErr();

  return 0;
}

/* Reset viewport to default. */
void gl_defViewport(void) {
  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. */
}

/* Clean up our mess. */
void gl_exit(void) {
  glTexList* tex;

  /* Make sure there's no texture leak. */
  if(texture_list != NULL) {
    DEBUG("Texture leak detected!");
    for(tex = texture_list; tex != NULL; tex = tex->next)
      DEBUG("   '%s' opened %d times", tex->tex->name, tex->used);
  }

  /* Shut down the subsystem. */
  SDL_QuitSubSystem(SDL_INIT_VIDEO);
}

/* Saves a png. */
int write_png(const char* file_name, png_bytep* rows, int w, int h,
              int colourtype, int bitdepth) {
  png_structp png_ptr;
  png_infop info_ptr;
  FILE* fp = NULL;
  char* doing = "Open for writing";

  if(!(fp = fopen(file_name, "wb"))) goto fail;

  doing = "Create png write struct";
  if(!(png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING,
                                         NULL, NULL, NULL))) goto fail;

  doing = "Create png info struct";
  if(!(info_ptr = png_create_info_struct(png_ptr))) goto fail;
  if(setjmp(png_jmpbuf(png_ptr))) goto fail;

  doing = "Init IO";
  png_init_io(png_ptr, fp);
  png_set_IHDR(png_ptr, info_ptr, w, h, bitdepth, colourtype, PNG_INTERLACE_NONE,
               PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

  doing = "Write info";
  png_write_info(png_ptr, info_ptr);

  doing = "Write image";
  png_write_image(png_ptr, rows);

  doing = "Write end";
  png_write_end(png_ptr, NULL);

  doing = "Closing file";
  if(0 != fclose(fp)) goto fail;

  return 0;

fail:
  WARN("write_png: Could not %s", doing);
  return -1;
}