#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <math.h>

#include <GLFW/glfw3.h>
#include <GLES2/gl2.h>

static void * read_file(const char * filename)
{
  int fd = open(filename, O_RDONLY);
  if (fd == -1) {
    fprintf(stderr, "open(%s): %s\n", filename, strerror(errno));
    return NULL;
  }

  off_t size = lseek(fd, 0, SEEK_END);
  if (size == (off_t)-1) {
    fprintf(stderr, "lseek(%s, SEEK_END): %s\n", filename, strerror(errno));
    return NULL;
  }

  off_t start = lseek(fd, 0, SEEK_SET);
  if (start == (off_t)-1) {
    fprintf(stderr, "lseek(%s, SEEK_SET): %s\n", filename, strerror(errno));
    return NULL;
  }

  void * buf = malloc(size+1);

  ssize_t read_size = read(fd, buf, size);
  if (read_size == -1) {
    fprintf(stderr, "read(%s): %s\n", filename, strerror(errno));
    return NULL;
  }
  ((char*)buf)[read_size] = 0;

  return buf;
}

void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
  glViewport(0, 0, width, height);
}

unsigned int compile_shaders()
{
  void * vertexShaderSource = read_file("src/vertex.glsl");
  unsigned int vertexShader;
  vertexShader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertexShader, 1, (const char **)&vertexShaderSource, NULL);
  glCompileShader(vertexShader);
  {
    int  success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if(!success) {
      glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
      printf("vertex shader compile failed:\n%s\n", infoLog);
    }
  }
  free(vertexShaderSource);

  void * fragmentShaderSource = read_file("src/fragment.glsl");
  unsigned int fragmentShader;
  fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fragmentShader, 1, (const char **)&fragmentShaderSource, NULL);
  glCompileShader(fragmentShader);
  {
    int  success;
    char infoLog[512];
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if(!success) {
      glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
      printf("fragment shader compile failed:\n%s\n", infoLog);
    }
  }
  free(fragmentShaderSource);

  unsigned int shaderProgram;
  shaderProgram = glCreateProgram();

  glAttachShader(shaderProgram, vertexShader);
  glAttachShader(shaderProgram, fragmentShader);
  glLinkProgram(shaderProgram);

  {
    int  success;
    char infoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if(!success) {
      glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
      printf("program link failed:\n%s\n", infoLog);
    }
  }

  glDeleteShader(vertexShader);
  glDeleteShader(fragmentShader);

  return shaderProgram;
}

static int make_buffer(unsigned int target,
                       const void * data,
                       size_t size)
{
  unsigned int buffer;
  glGenBuffers(1, &buffer);
  glBindBuffer(target, buffer);
  glBufferData(target, size, data, GL_STATIC_DRAW);
  return buffer;
}

static const float vertex_buffer_data[] = {
  -1.0f, -1.0f, 0.0f, 1.0f,
   1.0f, -1.0f, 0.0f, 1.0f,
  -1.0f,  1.0f, 0.0f, 1.0f,
   1.0f,  1.0f, 0.0f, 1.0f,
};

static const unsigned short element_buffer_data[] = {
  0, 1, 2, 3
};

static unsigned int make_texture(const char * filename)
{
  unsigned int texture;
  int width = 512;
  int height = 512;

  void * pixels = read_file(filename);
  if (!pixels)
    return 0;

  glGenTextures(1, &texture);
  glBindTexture(GL_TEXTURE_2D, texture);

  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S,     GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T,     GL_CLAMP_TO_EDGE);

  glTexImage2D(GL_TEXTURE_2D, 0,
               GL_RGBA8,
               width, height, 0,
               GL_RGBA, GL_UNSIGNED_BYTE,
               pixels);
  free(pixels);
  return texture;
}

struct location {
  struct {
    unsigned int time;
    unsigned int textures[2];
  } uniform;
  struct {
    unsigned int position;
  } attrib;
};

int main()
{
  glfwInit();
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  //glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GLFW_TRUE);

  GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
  if (window == NULL) {
    const char* description;
    glfwGetError(&description);
    printf("Failed to create GLFW window: %s\n", description);
    glfwTerminate();
    return -1;
  }
  glfwMakeContextCurrent(window);

  glViewport(0, 0, 800, 600);
  glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

  unsigned int vertex_buffer = make_buffer(GL_ARRAY_BUFFER,
                                           vertex_buffer_data,
                                           (sizeof (vertex_buffer_data)));

  unsigned int element_buffer = make_buffer(GL_ARRAY_BUFFER,
                                            element_buffer_data,
                                            (sizeof (element_buffer_data)));

  unsigned int texture_0 = make_texture("texture/gl2-hello-1.data");
  unsigned int texture_1 = make_texture("texture/gl2-hello-2.data");

  unsigned int shaderProgram = compile_shaders();

  struct location location;

  location.uniform.time = glGetUniformLocation(shaderProgram, "time");
  location.uniform.textures[0] = glGetUniformLocation(shaderProgram, "textures[0]");
  location.uniform.textures[1] = glGetUniformLocation(shaderProgram, "textures[1]");

  printf("uniforms:\n  time: %u\n  textures_0: %u\n  textures_1: %u\n",
         location.uniform.time,
         location.uniform.textures[0],
         location.uniform.textures[1]);

  location.attrib.position = glGetAttribLocation(shaderProgram, "position");
  printf("attributes:\n  position %u\n",
         location.attrib.position);

  float time = 0;

  struct timespec last;
  struct timespec cur;

  clock_gettime(CLOCK_MONOTONIC, &last);

  while(!glfwWindowShouldClose(window)) {
    clock_gettime(CLOCK_MONOTONIC, &cur);
    uint64_t last_msec = (last.tv_sec * 1000) + (last.tv_nsec * 1e-06);
    uint64_t cur_msec = (cur.tv_sec * 1000) + (cur.tv_nsec * 1e-06);
    uint64_t dt = cur_msec - last_msec;
    time = (float)((double)dt * 0.001);

    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
      glfwSetWindowShouldClose(window, true);

    glClearColor(0.1f, 0.1f, 0.1f, 0.1f);
    glClear(GL_COLOR_BUFFER_BIT);

    //
    // render
    //
    glUseProgram(shaderProgram);
    glUniform1f(location.uniform.time, time);

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texture_0);
    glUniform1i(location.uniform.textures[0], 0);

    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, texture_1);
    glUniform1i(location.uniform.textures[1], 1);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
    glVertexAttribPointer(location.attrib.position,
                          4,
                          GL_FLOAT,
                          GL_FALSE,
                          (sizeof (float)) * 4,
                          (void*)0
                          );
    glEnableVertexAttribArray(location.attrib.position);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, element_buffer);
    glDrawElements(GL_TRIANGLE_STRIP,
                   4,
                   GL_UNSIGNED_SHORT,
                   (void*)0
                   );

    glDisableVertexAttribArray(location.attrib.position);

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwTerminate();

  return 0;
}
