//
// Display a color cube
//
// Colors are assigned to each vertex and then the rasterizer interpolates
//   those colors across the triangles.  We us an orthographic projection
//   as the default projetion.

#include "Angel.h"
#include <algorithm>
#include <fstream>
#include <iostream>
#include <limits>
#include <vector>

#define NORM true

typedef Angel::vec4 color4;
typedef Angel::vec4 point4;

std::vector<point4> points;
std::vector<color4> colors;

inline vec4 multq(vec4 a, vec4 b) {
  // extract axes
  vec3 aa = vec3(a[1], a[2], a[3]);
  vec3 bb = vec3(b[1], b[2], b[3]);
  vec3 cc = a[0] * bb + b[0] * aa + cross(bb, aa);
  // reassemble quaternion in vec4
  return (vec4(a[0] * b[0] - dot(aa, bb), cc[0], cc[1], cc[2]));
}

// Array of rotation angles (in degrees) for each coordinate axis
int winWidth, winHeight;

vec3 axis;
vec4 rquat = vec4(1.0, 0.0, 0.0, 0.0);
vec4 move = vec4(0.0, 0.0, 0.0, 0.0);
GLfloat angle;
bool trackingMouse = false;
bool redrawContinue = false;
bool trackballMove = false;
float lastPos[3] = {0.0F, 0.0F, 0.0F};
int curx, cury;
int startX, startY;

GLuint vao;
GLuint buffer;
GLuint program;
GLuint vPosition;
GLuint vColor;
GLuint rquat_loc;
GLuint move_loc;

//----------------------------------------------------------------------------

void load_model(const std::string &file_name) {
  /* assume model file is vaild
   vertices nums == vertex_color nums
   only contain triangles
   the triangles' vertices are in vertices
  */
  std::vector<point4> vertices;
  std::vector<color4> vertex_colors;
  std::fstream s(file_name, std::fstream::in);
  if (!s.is_open()) {
    std::cerr << "failed to open" << file_name << std::endl;
  } else {
    std::string t;
    float x, y, z;
    float xmax = std::numeric_limits<float>::min(),
          ymax = std::numeric_limits<float>::min(),
          zmax = std::numeric_limits<float>::min();
    float xmin = std::numeric_limits<float>::max(),
          ymin = std::numeric_limits<float>::max(),
          zmin = std::numeric_limits<float>::max();
    int a, b, c;
    int num = 0;
    while (s >> t >> x >> y >> z) {
      if (t == "v") {
        num++;
        vertices.emplace_back(x, y, z, 1.0);
        xmax = std::max(x, xmax);
        xmin = std::min(x, xmin);
        ymax = std::max(y, ymax);
        ymin = std::min(y, ymin);
        zmax = std::max(z, zmax);
        zmin = std::min(z, zmin);
      } else {
        vertex_colors.emplace_back(x, y, z, 1.0);
        break;
      }
    }
    if (xmax > 1.0f || xmin < -1.0f || ymax > 1.0f || ymin < -1.0f ||
        zmax > 1.0f || zmin < -1.0f || NORM) {
      float scale = 1.0f / std::max({xmax - xmin, ymax - ymin, zmax - zmin});
      std::cout << "scale: " << scale << std::endl;
      float xmid = 0.5f * (xmax + xmin);
      float ymid = 0.5f * (ymax + ymin);
      float zmid = 0.5f * (zmax + zmin);

      for (point4 &p : vertices) {
        p[0] -= xmid;
        p[0] *= scale;
        p[1] -= ymid;
        p[1] *= scale;
        p[2] -= zmid;
        p[2] *= scale;
      }
    }
    std::cout << "there are " << num << " vertex(ices)" << std::endl;
    for (int i = 1; i < num; ++i) {
      s >> t >> x >> y >> z;
      vertex_colors.emplace_back(x, y, z, 1.0);
    }
    num = 0;
    points.clear();
    colors.clear();
    while (s >> t >> a >> b >> c) {
      points.push_back(vertices[a - 1]);
      points.push_back(vertices[b - 1]);
      points.push_back(vertices[c - 1]);
      colors.push_back(vertex_colors[a - 1]);
      colors.push_back(vertex_colors[b - 1]);
      colors.push_back(vertex_colors[c - 1]);
      num++;
    }
    std::cout << "there are " << num << " triangle(s)" << std::endl;
  }
}

// OpenGL initialization
void init(const std::string &file_name) {
  load_model(file_name);
  // Create a vertex array object
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  // Create and initialize a buffer object
  glGenBuffers(1, &buffer);
  glBindBuffer(GL_ARRAY_BUFFER, buffer);
  glBufferData(GL_ARRAY_BUFFER, sizeof(vec4) * (points.size() + colors.size()),
               NULL, GL_STATIC_DRAW);
  glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vec4) * points.size(),
                  points.data());
  glBufferSubData(GL_ARRAY_BUFFER, sizeof(vec4) * points.size(),
                  sizeof(vec4) * colors.size(), colors.data());
  // Load shaders and use the resulting shader program
  program = InitShader("vshader36.glsl", "fshader36.glsl");
  glUseProgram(program);
  // set up vertex arrays
  vPosition = glGetAttribLocation(program, "vPosition");
  glEnableVertexAttribArray(vPosition);
  glVertexAttribPointer(vPosition, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
  vColor = glGetAttribLocation(program, "vColor");
  glEnableVertexAttribArray(vColor);
  glVertexAttribPointer(vColor, 4, GL_FLOAT, GL_FALSE, 0,
                        BUFFER_OFFSET(sizeof(vec4) * points.size()));
  rquat_loc = glGetUniformLocation(program, "rquat");
  move_loc = glGetUniformLocation(program, "move");
  glEnable(GL_DEPTH_TEST);
  glClearColor(1.0, 1.0, 1.0, 1.0);
}

//----------------------------------------------------------------------------

void display(void) {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  if (trackballMove) {
    rquat = multq(vec4(cos(angle * 0.5f), sin(angle * 0.5f) * axis[0],
                       sin(angle * 0.5) * axis[1], sin(angle * 0.5f) * axis[2]),
                  rquat);
  }
  glUniform4fv(rquat_loc, 1, rquat);
  glUniform4fv(move_loc, 1, move);
  glDrawArrays(GL_TRIANGLES, 0, points.size());
  glutSwapBuffers();
}

//----------------------------------------------------------------------------

void keyboard(unsigned char key, int x, int y) {

  switch (key) {
  case 033: // Escape Key
  case 'q':
  case 'Q':
    exit(EXIT_SUCCESS);
    break;
  case 'd':
    move[0] += 0.1;
    glutPostRedisplay();
    break;
  case 'a':
    move[0] -= 0.1;
    glutPostRedisplay();
    break;
  case 'w':
    move[1] += 0.1;
    glutPostRedisplay();
    break;
  case 's':
    move[1] -= 0.1;
    glutPostRedisplay();
    break;
  case 'z':
    move[3] += 0.1;
    glutPostRedisplay();
    break;
  case 'Z':
    move[3] -= 0.1;
    glutPostRedisplay();
    break;
  }
}

/*----------------------------------------------------------------------*/
/*
** These functions implement a simple trackball-like motion control.
*/

void trackball_ptov(int x, int y, int width, int height, float v[3]) {
  float d, a;
  /* project x,y onto a hemi-sphere centered within width, height */
  v[0] = (2.0F * x - width) / width;
  v[1] = (height - 2.0F * y) / height;
  d = (float)sqrt(v[0] * v[0] + v[1] * v[1]);
  v[2] = (float)cos((M_PI / 2.0F) * ((d < 1.0F) ? d : 1.0F));
  a = 1.0F / (float)sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
  v[0] *= a;
  v[1] *= a;
  v[2] *= a;
}

void mouseMotion(int x, int y) {
  float curPos[3], dx, dy, dz;
  trackball_ptov(x, y, winWidth, winHeight, curPos);
  if (trackingMouse) {
    dx = curPos[0] - lastPos[0];
    dy = curPos[1] - lastPos[1];
    dz = curPos[2] - lastPos[2];
    if (dx || dy || dz) {
      angle = 1.1 * M_PI * 0.5 * sqrt(dx * dx + dy * dy + dz * dz);
      axis[0] = lastPos[1] * curPos[2] - lastPos[2] * curPos[1];
      axis[1] = lastPos[2] * curPos[0] - lastPos[0] * curPos[2];
      axis[2] = lastPos[0] * curPos[1] - lastPos[1] * curPos[0];
      float z = 1.0f /
                sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
      axis[0] *= z;
      axis[1] *= z;
      axis[2] *= z;
      lastPos[0] = curPos[0];
      lastPos[1] = curPos[1];
      lastPos[2] = curPos[2];
    }
  }
  glutPostRedisplay();
}

void startMotion(int x, int y) {
  trackingMouse = true;
  redrawContinue = false;
  startX = x;
  startY = y;
  curx = x;
  cury = y;
  trackball_ptov(x, y, winWidth, winHeight, lastPos);
  trackballMove = true;
}

void stopMotion(int x, int y) {
  trackingMouse = false;
  if (startX != x || startY != y) {
    redrawContinue = true;
  } else {
    angle = 0.0F;
    redrawContinue = false;
    trackballMove = false;
  }
}

void mouseButton(int button, int state, int x, int y) {
  if (button == GLUT_RIGHT_BUTTON)
    exit(0);
  if (button == GLUT_LEFT_BUTTON)
    switch (state) {
    case GLUT_DOWN:
      y = winHeight - y;
      startMotion(x, y);
      break;
    case GLUT_UP:
      stopMotion(x, y);
      break;
    }
}

void myReshape(int w, int h) {
  glViewport(0, 0, w, h);
  winWidth = w;
  winHeight = h;
}

void spinCube() {
  if (redrawContinue)
    glutPostRedisplay();
}

/*----------------------------------------------------------------------*/

int main(int argc, char **argv) {
  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);
  glutInitWindowSize(512, 512);
  glutInitContextVersion(3, 1);
  glutInitContextProfile(GLUT_CORE_PROFILE);
  glutCreateWindow("model viewer");

  if (glewInit() != GLEW_OK) {
    std::cerr << "Failed to initialize GLEW ... exiting" << std::endl;
    exit(EXIT_FAILURE);
  }

  init("v1_color.obj");
  glutReshapeFunc(myReshape);
  glutIdleFunc(spinCube);
  glutDisplayFunc(display);
  glutKeyboardFunc(keyboard);
  glutMouseFunc(mouseButton);
  glutMotionFunc(mouseMotion);
  glutMainLoop();
  return 0;
}
