
#ifdef __APPLE__
#define glGenVertexArrays glGenVertexArraysAPPLE
#define glBindVertexArray glBindVertexArrayAPPLE
#endif

#define GL_GLEXT_PROTOTYPES // for Linux

#include <GL/glfw.h>
#include <iostream>
#include <fstream>
#include <stdint.h>
#include <stdlib.h>
#include <vector>
#include <assert.h>
#include <string>
#include <string.h>
#include <cmath>

#define glError() { \
  GLenum err = glGetError(); \
  while (err != GL_NO_ERROR) { \
    std::cerr << "glError: " << (char *)gluErrorString(err) << " at " << __LINE__; \
    err = glGetError(); \
  } \
}

// picopng
int decodePNG(std::vector<unsigned char>& out_image, unsigned long& image_width, unsigned long& image_height, const unsigned char* in_png, size_t in_size, bool convert_to_rgba32 = true);
////

typedef struct {
  float pos[3];
} Vertex;

typedef struct {
  uint32_t vbo_id;
  uint32_t ibo_id;
  uint32_t vao_id;
  uint32_t num_poly;
} Mesh;

typedef struct {
  uint32_t prog_id;
  uint32_t vert_id;
  uint32_t frag_id;
} Shader;

Shader shaders[1];
Mesh popcan;
uint32_t popcan_tex_id;

char basic_vert_shader[] =
"varying vec4 color;"
"varying vec2 texcoord;"
"void main () {"
"  color = vec4(clamp(gl_Vertex.y,0.0,0.7),0.3,0.3,1.0);"
"  float theta = atan(gl_Vertex.z, gl_Vertex.x+0.1) + 3.142;"
"  texcoord = vec2(theta / 6.28, gl_Vertex.y / 7.0 + 0.5);"
"  gl_Position = gl_ModelViewProjectionMatrix*gl_Vertex;"
"}";

char basic_frag_shader[] =
"uniform sampler2D texture;"
"varying vec4 color;"
"varying vec2 texcoord;"
"void main () {"
"  gl_FragColor = mix(color, texture2D(texture, texcoord), 0.7);"
"}";

void render () {
  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glLoadIdentity ();
  glUseProgram (shaders[0].prog_id
          );
  static float x = 0;
  glTranslatef (-8,0,-20);
  glRotatef (x+=0.01, 0,1,0);
  glRotatef (30.0, 1.0,0,0);

  GLint shader_tex_loc = glGetUniformLocation(shaders[0].prog_id, "texture");
  glUniform1i(shader_tex_loc, 0);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, popcan_tex_id);
  glError();

  glBindVertexArray (popcan.vao_id);
  glDrawElements(GL_TRIANGLES, popcan.num_poly, GL_UNSIGNED_INT, 0);
}

void print_shader_error (uint32_t id, uint32_t type) {
  char *log;
  GLint length=0, result;
  if (type == GL_COMPILE_STATUS) glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
  else               glGetProgramiv (id, GL_INFO_LOG_LENGTH, &length);
  if (length == 0) {
    std::cerr << "Unknown error.\n";
    return;
  }
  log = new char[length];
  if (type == GL_COMPILE_STATUS) glGetShaderInfoLog(id, length, &result, log);
  else               glGetProgramInfoLog(id, length, &result, log);
  std::cerr << log << "\n";
  delete[] log;
}

Mesh load_mesh (std::string filename) {
  Mesh m;
  uint32_t object_count = 0;

  std::ifstream fin (filename.c_str ());
  if (!fin.good ()) { std::cerr << "Couldn't open " << filename << "\n"; exit(1); }
  std::vector<Vertex> vert_data;
  std::vector<uint32_t> index_data;
  while (!fin.eof ()) {
    char line[128];
    fin.getline(line, 128);
  if (line[0] == 'o') {
    object_count++;
    if (object_count != 1) { std::cerr << "More than one object in mesh.\n"; exit(1); }
  } else if (line[0] == 'v') {
      Vertex v;
      char *fstr = strtok(&(line[1]), " ");
      v.pos[0] = atof(fstr);
      fstr = strtok(NULL, " ");
      v.pos[1] = atof(fstr);
      fstr = strtok(NULL, " ");
      v.pos[2] = atof(fstr);
      assert (strtok (NULL, " ") == NULL);
      vert_data.push_back(v);
    } else if (line[0] == 'f') {
      char *intstr = strtok(&(line[1]), " ");
      index_data.push_back(atoi (intstr) - 1);
      intstr = strtok(NULL, " ");
      index_data.push_back(atoi (intstr) - 1);
      intstr = strtok(NULL, " ");
      index_data.push_back(atoi (intstr) - 1);
      if (strtok(NULL, " ") != NULL) { std::cerr << "You got some quads bro.\n"; exit (1); }
    }
  }
  glGenVertexArrays (1, &(m.vao_id));
  glBindVertexArray (m.vao_id);

  glGenBuffers(2, &m.vbo_id);
  glBindBuffer(GL_ARRAY_BUFFER, m.vbo_id);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m.ibo_id);
  glBufferData(GL_ARRAY_BUFFER, sizeof(Vertex)*vert_data.size (), &(vert_data[0]), GL_STATIC_DRAW);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(uint32_t)*index_data.size (), &(index_data[0]), GL_STATIC_DRAW);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  glBindVertexArray (0);
  m.num_poly = (uint32_t)index_data.size ();
  std::cerr << "Loaded " << filename << "\n";
  return m;
}

uint32_t load_texture (std::string filename) {
  std::vector<unsigned char> image_data;
  long unsigned int image_w, image_h;

  size_t png_size;
  std::ifstream fin(filename.c_str());
  if (!fin.good ()) { std::cerr << "Couldn't open " << filename << "\n"; exit(1); }
  fin.seekg(0, std::ios::end);
  png_size = fin.tellg();
  fin.seekg(0, std::ios::beg);

  unsigned char png_data[png_size];
  fin.read((char*)png_data, png_size);

  { int r = decodePNG(image_data, image_w, image_h, png_data, png_size);
    if(r) { std::cerr << "Couldn't read PNG data from " << filename << "\n"; exit(1); }}

  uint32_t tex_id;

  glGenTextures(1, &tex_id);
  glBindTexture(GL_TEXTURE_2D, tex_id);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  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, (GLint)image_w, (GLint)image_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)&(image_data.at(0)));
  glGenerateMipmap(GL_TEXTURE_2D);
  glError();
  std::cerr << "Loaded " << filename << " (" << image_w << "x" << image_h << ")\n";
  return tex_id;
}

uint32_t compile_individual_shader (const char *src, uint32_t len, uint32_t type) {
  uint32_t id = glCreateShader (type);
  glShaderSource (id, 1, &src, NULL);
  glCompileShader (id);

  GLint compiled;
  glGetShaderiv (id, GL_COMPILE_STATUS, &compiled);
  if (compiled == GL_FALSE) {
    std::cerr << "Error compiling shader: ";
    print_shader_error(id, GL_COMPILE_STATUS);
    exit (1);
  }
  return id;
}

Shader make_shader (char* vert_src, uint32_t vert_len, char* frag_src, uint32_t frag_len) {
  Shader s;
  s.vert_id = compile_individual_shader (vert_src, vert_len, GL_VERTEX_SHADER);
  s.frag_id = compile_individual_shader (frag_src, frag_len, GL_FRAGMENT_SHADER);
  s.prog_id = glCreateProgram ();
  glAttachShader (s.prog_id, s.frag_id);
  glAttachShader (s.prog_id, s.vert_id);
  glLinkProgram (s.prog_id);
  GLint linked;
  glGetProgramiv(s.prog_id, GL_LINK_STATUS, &linked);
  if (linked == GL_FALSE) {
    std::cerr << "Error linking shader: ";
    print_shader_error (s.prog_id, GL_COMPILE_STATUS);
    exit (1);
  }
  return s;
}



void GL_init (float w, float h) {
  glShadeModel (GL_SMOOTH);
  glEnable (GL_DEPTH_TEST);
  glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

  glFrontFace (GL_CCW);
  glCullFace (GL_BACK);
  glEnable (GL_CULL_FACE);

  glClearColor (0,0,0,0);
  glEnable (GL_TEXTURE_2D);

  glViewport (0, 0, w, h);

  // TODO: This code is old and bad, needs to go
  glMatrixMode (GL_PROJECTION);
  glLoadIdentity();
  float near = 0.001;
  float far = 1000.0;
  float fH = tan(45.0/360.0*M_PI)*near;
  float fW = fH*w/h;
  glFrustum(-fW, fW, -fH, fH, near, far);
  glMatrixMode (GL_MODELVIEW);
  // TODO: replace above

  popcan = load_mesh ("can.obj");
  shaders[0] = make_shader (basic_vert_shader, sizeof (basic_vert_shader), basic_frag_shader, sizeof (basic_frag_shader));

  popcan_tex_id = load_texture("can.png");
}

int main () {
  float scale = 1;
  float w = 480*scale;
  float h = 272*scale;
  if (!glfwInit () || !glfwOpenWindow (w, h, 8,8,8,0,32,0,GLFW_WINDOW)) {
    std::cerr << "Something GLFW failed.\n";
    return 1;
  }
  GL_init (w, h);

  int running = 1;

  float t0 = glfwGetTime ();
  uint32_t frames = 0;
  //glfwSwapInterval (0); // Uncomment this to test FPS without vsync
  while (running) {
    float t = glfwGetTime ();
    if (t-t0 >= 5.0) {
      std::cerr << (float)frames/5.0 << " FPS\n";
      t0 = t;
      frames = 0;
    }
    render ();
    frames++;
    glfwSwapBuffers ();
    running = glfwGetWindowParam (GLFW_OPENED);
  }

  glfwTerminate ();
}
