#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 <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/texture_cube.vp.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/texture_cube.fp.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_DYNAMIC_DRAW);
  return buffer;
}

static float vertex_buffer_data[5 * 6 * 2 * 3] = {
  // 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 {
  struct {
    unsigned int position;
    unsigned int texture;
  } attrib;
  struct {
    unsigned int texture1;
  } uniform;
};

typedef struct {
  float x;
  float y;
  float z;
} vec3;

typedef struct {
  float x;
  float y;
} vec2;

typedef struct {
  int p;
  int t;
} face;

const vec3 position[] = {
  {1.0, 1.0, -1.0},
  {1.0, -1.0, -1.0},
  {1.0, 1.0, 1.0},
  {1.0, -1.0, 1.0},
  {-1.0, 1.0, -1.0},
  {-1.0, -1.0, -1.0},
  {-1.0, 1.0, 1.0},
  {-1.0, -1.0, 1.0},
};

const vec2 texture[] = {
  {1.0, 0.0},
  {0.0, 1.0},
  {0.0, 0.0},
  {1.0, 1.0},
};

const face faces[] = {
  {5,1}, {3,2}, {1,3},
  {3,1}, {8,2}, {4,3},
  {7,1}, {6,2}, {8,3},
  {2,1}, {8,2}, {6,3},
  {1,1}, {4,2}, {2,3},
  {5,1}, {2,2}, {6,3},
  {5,1}, {7,4}, {3,2},
  {3,1}, {7,4}, {8,2},
  {7,1}, {5,4}, {6,2},
  {2,1}, {4,4}, {8,2},
  {1,1}, {3,4}, {4,2},
  {5,1}, {1,4}, {2,2},
};

static inline vec3 rotate(vec3 vertex, float theta)
{
  float x = vertex.x;
  float y = vertex.y;
  float z = vertex.z;
  float t;

  t  = y * cosf(theta) - z * sinf(theta);
  z  = y * sinf(theta) + z * cosf(theta);
  y  = t;

  float theta2 = 3.14 * sinf(theta / 2);

  t  = x * cosf(theta2) - z * sinf(theta2);
  z  = x * sinf(theta2) + z * cosf(theta2);
  x  = t;

  return (vec3){x, y, z};
}

int main()
{
  glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
  glfwInit();
  GLFWmonitor* monitor = glfwGetPrimaryMonitor();
  float xscale, yscale;
  glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
  glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
  glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_FALSE);
  glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE);
  glfwGetMonitorContentScale(monitor, &xscale, &yscale);
  printf("%f %f\n", xscale, yscale);

  //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);

  int wwidth, wheight;
  glfwGetWindowSize 	(window,
                                 &wwidth,
                                 &wheight);
  printf("window : %d %d\n", wwidth, wheight);

  glViewport(0, 0, 1600, 1200);
  //glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
 int vertex_buffer = make_buffer(GL_ARRAY_BUFFER,
                                           vertex_buffer_data,
                                           (sizeof (vertex_buffer_data)));

  printf("compile start\n");
  unsigned int shaderProgram = compile_shaders();
  printf("compile end\n");

  struct location location;

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

  location.uniform.texture1 = glGetUniformLocation(shaderProgram, "texture1");
  printf("uniforms:\n  texture %u\n",
         location.uniform.texture1);

  unsigned int texture1;
  glGenTextures(1, &texture1);
  glActiveTexture(GL_TEXTURE0 + 0);
  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);

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

    glClearDepthf(-1000.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    glClear(GL_DEPTH_BUFFER_BIT);
    glDepthFunc(GL_GREATER);
    glEnable(GL_DEPTH_TEST);

    //
    // render
    //

    glUseProgram(shaderProgram);

    theta += 0.005f;
    const int faces_length = (sizeof (faces)) / (sizeof (faces[0]));
    const int vertex_buffer_length = (sizeof (vertex_buffer_data)) / (sizeof (vertex_buffer_data[0]));
    assert(faces_length == (vertex_buffer_length / 5));
    for (int i = 0; i < faces_length; i++) {
      vec3 p = rotate(position[faces[i].p - 1], theta);
      p.x *= 0.2;
      p.y *= 0.2;
      p.z *= 0.2;
      p.z += 0.5;
      vec2 t = texture[faces[i].t - 1];

      vertex_buffer_data[i * 5 + 0] = p.x;
      vertex_buffer_data[i * 5 + 1] = p.y;
      vertex_buffer_data[i * 5 + 2] = p.z;
      vertex_buffer_data[i * 5 + 3] = t.x;
      vertex_buffer_data[i * 5 + 4] = t.y;

      /*
      printf("% .02f % .02f % .02f ; %01.01f %01.01f\n", p.x, p.y, p.z, t.x, t.y);
      if (i % 3 == 2)
        printf("\n");
      */
    }
    //return 0;

    //printf("%f\n", theta);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer);
    glBufferData(GL_ARRAY_BUFFER, (sizeof (vertex_buffer_data)), vertex_buffer_data, GL_DYNAMIC_DRAW);

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

    glUniform1i(location.uniform.texture1, 0);

    glDrawArrays(GL_TRIANGLES, 0, 3 * 6 * 2);


    glfwSwapBuffers(window);
    glfwPollEvents();
  }

  glfwTerminate();

  return 0;
}
