#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 <assert.h>

#include <GLFW/glfw3.h>
#include <GLES3/gl3.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(const char * vp_path, const char * fp_path)
{
  void * vertexShaderSource = read_file(vp_path);
  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(fp_path);
  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 triangle_vertex_buffer_data[] = {
  // position          // color
   0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // bottom right
  -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // bottom left
   0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // top
};

static const float texture_vertex_buffer_data[] = {
  // position           // texture
   0.5f,  0.5f, 0.0f,   1.0f, 0.0f, // top right
   0.5f, -0.5f, 0.0f,   1.0f, 1.0f, // bottom right
  -0.5f, -0.5f, 0.0f,   0.0f, 1.0f, // bottom left

  -0.5f,  0.5f, 0.0f,   0.0f, 0.0f, // top left
   0.5f,  0.5f, 0.0f,   1.0f, 0.0f, // top right
  -0.5f, -0.5f, 0.0f,   0.0f, 1.0f, // bottom left
};

struct location {
  unsigned int program;
  struct {
    unsigned int position;
    unsigned int color;
    unsigned int texture;
  } attrib;
  struct {
    unsigned int texture1;
  } uniform;
};

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

  GLFWwindow* window = glfwCreateWindow(1600, 1200, "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, 1600, 1200);
  //glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

  unsigned int triangle_vertex_buffer = make_buffer(GL_ARRAY_BUFFER,
                                                    triangle_vertex_buffer_data,
                                                    (sizeof (triangle_vertex_buffer_data)));

  unsigned int texture_vertex_buffer = make_buffer(GL_ARRAY_BUFFER,
                                                    texture_vertex_buffer_data,
                                                    (sizeof (texture_vertex_buffer_data)));

  // vertex color start
  struct location vertex_color;
  printf("compile start: vertex color\n");
  vertex_color.program = compile_shaders("src/vertex_color.vp.glsl",
                                         "src/vertex_color_fp_render.fp.glsl");
  printf("compile end: vertex color\n");

  glUseProgram(vertex_color.program);
  vertex_color.attrib.position = glGetAttribLocation(vertex_color.program, "position");
  vertex_color.attrib.color = glGetAttribLocation(vertex_color.program, "color");
  printf("vertex color attributes:\n  position %u\n  color %u\n",
         vertex_color.attrib.position,
         vertex_color.attrib.color);
  vertex_color.uniform.texture1 = glGetUniformLocation(vertex_color.program, "test");

  // vertex color end

  // texture start
  struct location texture;
  printf("compile start: texture\n");
  texture.program = compile_shaders("src/texture.vp.glsl",
                                    "src/texture.fp.glsl");
  printf("compile end: texture\n");

  glUseProgram(texture.program);
  texture.attrib.position = glGetAttribLocation(texture.program, "pos");
  texture.attrib.texture = glGetAttribLocation(texture.program, "tex");
  texture.uniform.texture1 = glGetUniformLocation(texture.program, "texture1");
  printf("texture attributes:\n  position %u\n  texture %u\n",
         texture.attrib.position,
         texture.attrib.texture);
  printf("texture uniforms:\n  texture1 %u\n",
         texture.uniform.texture1);


  unsigned int texture1;
  glGenTextures(1, &texture1);
  glActiveTexture(GL_TEXTURE1);
  glBindTexture(GL_TEXTURE_2D, texture1);
  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_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  void * texture_buf = read_file("texture/butterfly_1024x1024_rgb888.data");
  assert(texture_buf != NULL);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 1024, 1024, 0, GL_RGB, GL_UNSIGNED_BYTE, texture_buf);
  free(texture_buf);

  // texture end

  unsigned int fp_framebuffer;
  glGenFramebuffers(1, &fp_framebuffer);
  glBindFramebuffer(GL_FRAMEBUFFER, fp_framebuffer);

  unsigned int colorbuffer[2];
  glGenTextures(2, colorbuffer);
  for (int i = 0; i < 2; i++) {
    glBindTexture(GL_TEXTURE_2D, colorbuffer[i]);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, 1600, 1200, 0, GL_RGBA, GL_FLOAT, NULL);
    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);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, colorbuffer[i], 0);
  }
  unsigned int depthbuffer;
  {
    glGenRenderbuffers(1, &depthbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, depthbuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, 1600, 1200);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthbuffer);
    unsigned int attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, attachments);
    //glDrawBuffer(GL_COLOR_ATTACHMENT0);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
      printf("Framebuffer not complete!");
  }

  while(!glfwWindowShouldClose(window)) {
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
      glfwSetWindowShouldClose(window, true);

    //glBindFramebuffer(GL_FRAMEBUFFER, 0);


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

    //
    // render
    //
    //glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, fp_framebuffer);
    glUseProgram(vertex_color.program);

    glBindBuffer(GL_ARRAY_BUFFER, triangle_vertex_buffer);
    glVertexAttribPointer(vertex_color.attrib.position,
                          3,
                          GL_FLOAT,
                          GL_FALSE,
                          (sizeof (float)) * 6,
                          (void*)0
                          );
    glVertexAttribPointer(vertex_color.attrib.color,
                          3,
                          GL_FLOAT,
                          GL_FALSE,
                          (sizeof (float)) * 6,
                          (void*)(3 * 4)
                          );
    glEnableVertexAttribArray(vertex_color.attrib.position);
    glEnableVertexAttribArray(vertex_color.attrib.color);

    glUniform1i(vertex_color.uniform.texture1, 3);

    glDrawArrays(GL_TRIANGLES, 0, 3);

    //
    if (0) {
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
      glUseProgram(texture.program);

      glBindBuffer(GL_ARRAY_BUFFER, texture_vertex_buffer);
      glVertexAttribPointer(texture.attrib.position,
                            3,
                            GL_FLOAT,
                            GL_FALSE,
                            (sizeof (float)) * 5,
                            (void*)0
                            );
      glVertexAttribPointer(texture.attrib.texture,
                            2,
                            GL_FLOAT,
                            GL_FALSE,
                            (sizeof (float)) * 5,
                            (void*)(3 * 4)
                            );
      glEnableVertexAttribArray(texture.attrib.position);
      glEnableVertexAttribArray(texture.attrib.texture);

      glActiveTexture(GL_TEXTURE1);
      //glBindTexture(GL_TEXTURE_2D, texture1);
      glBindTexture(GL_TEXTURE_2D, colorbuffer[0]);
      glUniform1i(texture.uniform.texture1, 1);

      glDrawArrays(GL_TRIANGLES, 0, 6);
    }

    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwTerminate();

  return 0;
}
