#include <GL/gl.h>
#include <GL/glut.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <math.h>
#include "cube.h"
#include "control.h"
#include "matrix.h"
#include "textures.h"

float colors[7][4] = {
  {1,1,1,1},
  {0,0,1,1},
  {1,1,0,1},
  {0,1,0,1},
  {1,0.5,0,1},
  {1,0,0,1},
  {0.2, 0.2, 0.2, 1} // bevel
};

// 0, 1, 2, 3, 4, 5
// f, s, a, p, d, u
int adjacent[6][4][3] = {
  {{5, 0, 1},{1,-1, 0},{4, 0,-1},{3, 1, 0}},
  {{5, 1, 0},{2,-1, 0},{4, 1, 0},{0, 1, 0}},
  {{5, 0,-1},{3,-1, 0},{4, 0, 1},{1, 1, 0}},
  {{5,-1, 0},{0,-1, 0},{4,-1, 0},{2, 1, 0}},
  {{0, 0, 1},{1, 0, 1},{2, 0, 1},{3, 0, 1}},
  {{3, 0,-1},{2, 0,-1},{1, 0,-1},{0, 0,-1}}
};

float Cube::sticker_spec[4] = {1,1,1,0.8};
float Cube::bevel_spec[4] = {1,1,1,0.8};

Cube::Cube() {
  reset();
}

void Cube::reset() {
  twist_deg = 0;
  for (int i = 0; i < 6; i++) {
    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 3; k++) {
        int f = i | (k*3 + j) << 4;
        face[i][k][j] = f;
        twisted[i][j][k] = 0;
      }
    }
  }
}

void Cube::scramble() {
  //twist(1, 0);
  //twist(1, 1);

  for (int i = 0; i < 6; i++) {
    twist(i%2, i);
  }
}

void ind3adj(int f, int ai, int *dst) {
  int af = adjacent[f][ai][0];
  int ax = adjacent[f][ai][1];
  int ay = adjacent[f][ai][2];

  if (ax) {
    ax += 1;
    dst[0] = dst[1] = dst[2] = ax;
    dst[3] = 0; dst[4] = 1; dst[5] = 2;
  } else {
    ay += 1;
    dst[0] = 0; dst[1] = 1; dst[2] = 2;
    dst[3] = dst[4] = dst[5] = ay;
  }
}

void Cube::get3adj(int f, int ai, int *dst) {
  int af = adjacent[f][ai][0];

  int ind[6];
  ind3adj(f, ai, ind);

  for (int i = 0; i < 3; i++) {
    dst[i] = face[af][ind[i]][ind[i+3]];
    dst[i+3] = twisted[af][ind[i]][ind[i+3]];
  }
}
void Cube::set3adj(int f, int ai, int *src) {
  int af = adjacent[f][ai][0];

  int ind[6];
  ind3adj(f, ai, ind);

  for (int i = 0; i < 3; i++) {
    face[af][ind[i]][ind[i+3]] = src[i];
    twisted[af][ind[i]][ind[i+3]] = src[i+3];
  }
}

void Cube::twist(bool ccw, int f) {
  int t;

  t = ccw ? 1 : 3;
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      twisted[f][i][j] = (twisted[f][i][j] + t) % 4;
    }
  }

  if (ccw) {
    t = face[f][0][0];
    face[f][0][0] = face[f][0][2];
    face[f][0][2] = face[f][2][2];
    face[f][2][2] = face[f][2][0];
    face[f][2][0] = t;

    t = face[f][1][0];
    face[f][1][0] = face[f][0][1];
    face[f][0][1] = face[f][1][2];
    face[f][1][2] = face[f][2][1];
    face[f][2][1] = t;

    t = twisted[f][0][0];
    twisted[f][0][0] = twisted[f][0][2];
    twisted[f][0][2] = twisted[f][2][2];
    twisted[f][2][2] = twisted[f][2][0];
    twisted[f][2][0] = t;

    t = twisted[f][1][0];
    twisted[f][1][0] = twisted[f][0][1];
    twisted[f][0][1] = twisted[f][1][2];
    twisted[f][1][2] = twisted[f][2][1];
    twisted[f][2][1] = t;
  } else {
    t = face[f][0][0];
    face[f][0][0] = face[f][2][0];
    face[f][2][0] = face[f][2][2];
    face[f][2][2] = face[f][0][2];
    face[f][0][2] = t;

    t = face[f][1][0];
    face[f][1][0] = face[f][2][1];
    face[f][2][1] = face[f][1][2];
    face[f][1][2] = face[f][0][1];
    face[f][0][1] = t;

    t = twisted[f][0][0];
    twisted[f][0][0] = twisted[f][2][0];
    twisted[f][2][0] = twisted[f][2][2];
    twisted[f][2][2] = twisted[f][0][2];
    twisted[f][0][2] = t;

    t = twisted[f][1][0];
    twisted[f][1][0] = twisted[f][2][1];
    twisted[f][2][1] = twisted[f][1][2];
    twisted[f][1][2] = twisted[f][0][1];
    twisted[f][0][1] = t;
  }

  // rotate outside edge

  int tadj[6];
  int tadj2[6];

  int swap[4][4] = {{1,3,4,5},{2,5,2,4},{4,5,1,3},{2,4,2,5}};
  int rotate[2][4][4] = {
    {{3,3,3,3},{2,2,0,0},{1,1,1,1},{0,0,2,2}},
    {{1,1,1,1},{0,2,2,0},{3,3,3,3},{2,0,0,2}}
  };

  for (int i = 0; i < 4; i++) {
    int i2 = (i + 1) % 4;
    int j = ccw ? 3-i : i;
    int j2 = ccw ? 3-i2 : i2;

    int af = adjacent[f][j][0];

    bool s = f < 4 && (swap[f][ccw*2] == af || swap[f][ccw*2 + 1] == af);

    if (!i) get3adj(f, j, tadj2);
    if (!i2) memcpy(tadj, tadj2, sizeof(tadj));
    else get3adj(f, j2, tadj);
    if (s) {
      int t = tadj[0];
      tadj[0] = tadj[2];
      tadj[2] = t;
      t = tadj[3];
      tadj[3] = tadj[5];
      tadj[5] = t;
    }
    int rot = 0;
    if (f < 4) { // sticker rotation
      rot = rotate[ccw][f][i];
    }
    for (int i = 3; i < 6; i++) {
      tadj[i] = (tadj[i] + rot) % 4;
    }
    set3adj(f, j, tadj);
  }
}

int Cube::front_face(int front) {
  float vec[16] = {1,0,0,1, 0,1,0,1, 0,0,1,1};
  for (int i = 0; i < 3; i++) {
    matrixmult(rot, vec + i*4);
  }

  float max = 0;
  int maxi = 0;
  bool neg = false;
  for (int i = 0; i < 3; i++) {
    float a = vec[i*4 + front%3];
    bool n = a < 0;
    if (n) a = -a;
    if (a > max) {
      max = a;
      maxi = i;
      neg = n;
    }
  }
  if (front >= 3) neg = !neg;

  // in order: fore, starboard, aft, port, top, bottom
  switch (maxi) {
    case 2: // either fore or aft
      if (neg) return 2; // aft
      return 0; // fore
    case 0: // either port or starboard
      if (neg) return 3; // port
      return 1; // starboard
    case 1: // either top or bottom
      if (neg) return 5; // bottome
      return 4; // top
  }
}

// in order: left, top, front, right, bottom, back
int Cube::front_face() {
  return front_face(2);
}

void Cube::render() {
  glMatrixMode(GL_MODELVIEW);
  int front = front_face();

  float dist = sqrt(BEVELW2 * BEVELW2 + STICKER2 * STICKER2);
  float sn = BEVELW2 / dist;
  float cs = STICKER2 / dist;

  for (int i = 0; i < 6; i++) {
    glPushMatrix();
    if (i < 4) {
      glRotatef(i*90, 0, 1, 0);
    } else {
      glRotatef((i-4.5)*180, 1, 0, 0);
    }

    if (twist_deg && i == twistface) { // draw inside wall
      glPushMatrix();
      glTranslatef(0, 0, CUBLE2);
      glColor4fv(colors[6]);
      glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, bevel_spec);
      glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, BSHINE);

      float cubetrunc = CUBESIZE2 - TRUNCATE;
      glBegin(GL_TRIANGLE_FAN);
      glNormal3f(0, 0, 1);
      glVertex3f(0, 0, -BEVELW);
      glNormal3f(-cs * SQ2I, -cs * SQ2I, sn);
      glVertex2f(cubetrunc, cubetrunc);
      glNormal3f(cs * SQ2I, -cs * SQ2I, sn);
      glVertex2f(-cubetrunc, cubetrunc);
      glNormal3f(cs * SQ2I, cs * SQ2I, sn);
      glVertex2f(-cubetrunc, -cubetrunc);
      glNormal3f(-cs * SQ2I, cs * SQ2I, sn);
      glVertex2f(cubetrunc, -cubetrunc);
      glNormal3f(-cs * SQ2I, -cs * SQ2I, sn);
      glVertex2f(cubetrunc, cubetrunc);
      glEnd();
      glPopMatrix();
    }

    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 3; k++) {
        glPushMatrix();

        float cx = CUBLE * (k - 1);
        float cy = CUBLE * (j - 1);

        bool select = (i == front);
        if (i == twistface) glRotatef(twist_deg, 0, 0, 1);
        // if we're adjacent to the front face ...
        for (int a = 0; a < 4; a++) {
          if ((!select) && adjacent[front][a][0] == i) {
            int ax = adjacent[front][a][1];
            int ay = adjacent[front][a][2];
            select = (ax && ax + 1 == k) || (ay && ay + 1 == j);
          }
          if (twist_deg) {
            if ((i != twistface) && adjacent[twistface][a][0] == i) {
              int ax = adjacent[twistface][a][1];
              int ay = adjacent[twistface][a][2];
              if (ax && ax + 1 == k) {
                glRotatef(twist_deg, ax, 0, 0);
              }
              else if (ay && ay + 1 == j) {
                glRotatef(twist_deg, 0, ay, 0);
              }
            }
          }
        }

        glTranslatef(cx, cy, CUBESIZE2);

        if (select && 0) { // highlight the faces
          glEnable(GL_LIGHT1);
          glLightfv(GL_LIGHT1, GL_POSITION, (float[4]){0, 0, -CUBLE2, 1});
        }

        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, SSHINE);
        // texture alignment
        glPushMatrix();
        glRotatef(90 * twisted[i][k][j], 0, 0, 1);

        if (select) {
          glEnable(GL_LIGHT1);
        }

        int sface = face[i][k][j];
        int tloc = (sface & 0xf0) >> 4;
        int tlocx = tloc / 3;
        int tlocy = tloc % 3;
        sface &= 0xf;

        if (loaded_textures[0][sface]) {
          glBindTexture(GL_TEXTURE_2D, loaded_textures[1][sface]);
          glColor4fv(colors[0]);
        } else {
          glColor4fv(colors[sface]);
        }

        float tsize = CUBESIZE2 - 2 * TRUNCATE;
        float tstep = CUBLE2 / (CUBLE + STICKER2);
        float dt = 0.5 - tstep;
        float ctx =  (tlocx - 1) * tstep + 0.5;
        float cty = -(tlocy - 1) * tstep + 0.5;

        // draw the sticker
        glBegin(GL_TRIANGLE_FAN);
        glNormal3f(0, 0, 1);
        glTexCoord2f(ctx, cty);
        glVertex3f(0, 0, -BEVELW2);
        glNormal3f( cs * SQ2I,  cs * SQ2I, sn);
        glTexCoord2f(ctx - dt, cty + dt);
        glVertex2f(-STICKER2 , -STICKER2);
        glNormal3f( cs * SQ2I, -cs * SQ2I, sn);
        glTexCoord2f(ctx - dt, cty - dt);
        glVertex2f(-STICKER2 ,  STICKER2);
        glNormal3f(-cs * SQ2I, -cs * SQ2I, sn);
        glTexCoord2f(ctx + dt, cty - dt);
        glVertex2f( STICKER2 ,  STICKER2);
        glNormal3f(-cs * SQ2I,  cs * SQ2I, sn);
        glTexCoord2f(ctx + dt, cty + dt);
        glVertex2f( STICKER2 , -STICKER2);
        glNormal3f( cs * SQ2I,  cs * SQ2I, sn);
        glTexCoord2f(ctx - dt, cty + dt);
        glVertex2f(-STICKER2 , -STICKER2);
        glEnd();

        glBindTexture(GL_TEXTURE_2D, 0);

        glDisable(GL_LIGHT1);
        glEnable(GL_LIGHT0);
        glPopMatrix();
        // draw bevels

        glColor4fv(colors[6]);
        glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, bevel_spec);
        glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, BSHINE);

        bool outer = k != 1 || j != 1;
        int corner[4] = {j, j<2, 0, 0};
        int edge[4] = {1, 0, 0, 0};
        if (outer) {
          if (j < 2) edge[1] = corner[1] = 1;
          if (k < 2) {
            edge[2] = 1;
          }
          if (j) edge[3] = 1;
        }

        for (int bev = 0; bev < 4; bev++) {
          glColor3f(0.2, 0.2, 0.2);

          glPushMatrix();
          glRotatef(-90 * bev, 0, 0, 1);

          // corner
          int t = (bev / 2) * 2;
          int t2 = (((bev + 1) % 4) / 2) * 2;
          int t3 = 2 - t;
          bool outcorner = outer && (t == k || t2 == j);
          if (i > 3 && outcorner || k == 0 && corner[bev]) {
            glPushMatrix();
            glTranslatef(-CUBLE2, -CUBLE2, 0);
            glBegin(GL_TRIANGLES);
            glNormal3f(0, 0, 1);
            glVertex3f(TRUNCATE, TRUNCATE, 0);
            glNormal3f(0, -1, 0);
            glVertex3f(TRUNCATE, 0, -TRUNCATE);
            glNormal3f(-1, 0, 0);
            glVertex3f(0, TRUNCATE, -TRUNCATE);
            glEnd();
            glPopMatrix();
          }
          // edge
          if (i > 3 || !outer || edge[bev]) {
            glPushMatrix();
            glTranslatef(-CUBLE2, 0, 0);
            glBegin(GL_QUADS);
            glNormal3f(-1, 0, 0);
            glVertex3f(0, -STICKER2, -TRUNCATE);
            glVertex3f(0,  STICKER2, -TRUNCATE);
            glNormal3f(0, 0, 1);
            glVertex2f( TRUNCATE,  STICKER2);
            glVertex2f( TRUNCATE, -STICKER2);
            glEnd();
            // joint
            glBegin(GL_TRIANGLES);
            if (!(outer && (t2 == k || t3 == j))) {
              glNormal3f(0, 0, 1);
              glVertex3f(TRUNCATE,  STICKER2, 0);
              glNormal3f(-SQ2I, SQ2I, 0);
              glVertex3f(0,  STICKER2 + TRUNCATE, -TRUNCATE);
              glNormal3f(-1, 0, 0);
              glVertex3f(0,  STICKER2, -TRUNCATE);
            }
            if (!outcorner) {
              glNormal3f(-1, 0, 0);
              glVertex3f(0, -STICKER2, -TRUNCATE);
              glNormal3f(-SQ2I, -SQ2I, 0);
              glVertex3f(0, -STICKER2 - TRUNCATE, -TRUNCATE);
              glNormal3f(0, 0, 1);
              glVertex3f(TRUNCATE, -STICKER2, 0);
            }
            glEnd();
            glPopMatrix();
          }
          glPopMatrix();
        }

        glPopMatrix();
      }
    }
    glPopMatrix();
  }
}

Cube cube;

void draw() {
  int view[4];
  glGetIntegerv(GL_VIEWPORT, view);
  float ratiox = float(view[2]) / view[3];
  float ratioy = 1;
  if (ratiox < 1) {
    ratioy = 1 / ratiox;
    ratiox = 1;
  }

  glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-CUBEWIN * ratiox/2, CUBEWIN * ratiox/2,
      -CUBEWIN * ratioy/2, CUBEWIN * ratioy/2, 300, -300);
  gluPerspective(15, 1, 30, 100);
  glTranslatef(0, 0, -CUBEMAX * 1.5);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glMultMatrixf(rot);
  cube.render();
  //for (int i = 0; i < 4; i++) {
  //  float cs = CUBESIZE / 2;
  //  glColor4fv(colors[i]);
  //  glPushMatrix();
  //  glRotatef(90 * i, 1, 0, 0);
  //  glBegin(GL_QUADS);
  //  glVertex3i(-cs, -cs, -cs);
  //  glVertex3i(-cs,  cs, -cs);
  //  glVertex3i( cs,  cs, -cs);
  //  glVertex3i( cs, -cs, -cs);
  //  glEnd();
  //  glPopMatrix();
  //}
  glutSwapBuffers();
}

int main(int argc, char **argv) {
  bzero(rot, sizeof(rot));

  for (int i = 0; i < 4; i++) {
    rot[i + i*4] = 1;
  }

  glutInit(&argc, argv);
  glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);
  glutInitWindowSize(800, 600);

  int window = glutCreateWindow("Fancy Cube!");

  glutDisplayFunc(&draw);
  glutKeyboardFunc(&keypress);
  glutMouseFunc(&mousebutton);
  glutMotionFunc(&mousemove);
  glutPassiveMotionFunc(&mousemove);
  glutEntryFunc(&mouseinout);

  glClearColor(0.1, 0.1, 0.1, 1);

  glEnable(GL_BLEND);
  glEnable(GL_SMOOTH);
  glEnable(GL_POLYGON_SMOOTH);
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  glEnable(GL_LIGHTING);
  glEnable(GL_COLOR_MATERIAL);
  glEnable(GL_TEXTURE_2D);

  // main light
  glLightfv(GL_LIGHT0, GL_DIFFUSE, (float[4]){0.4, 0.4, 0.4, 1});
  glLightfv(GL_LIGHT0, GL_AMBIENT, (float[4]){0.2, 0.2, 0.2, 1});
  //glLightfv(GL_LIGHT0, GL_SPECULAR, (float[4]){0, 0, 0, 1});
  glLightfv(GL_LIGHT0, GL_POSITION, (float[4]){CUBEWIN/2, CUBEWIN/4, CUBEWIN, 1});

  // inside (selection) light
  //glLightfv(GL_LIGHT1, GL_SPECULAR, (float[4]){0, 0, 0, 1});
  glLightfv(GL_LIGHT1, GL_AMBIENT, (float[4]){0.5, 0.5, 0.5, 1});
  glLightfv(GL_LIGHT1, GL_DIFFUSE, (float[4]){0.9, 0.9, 0.9, 1});
  glLightfv(GL_LIGHT1, GL_SPECULAR, (float[4]){0.0, 0.0, 0.0, 1});
  glLightfv(GL_LIGHT1, GL_POSITION, (float[4]){0, 0, 0, 1});
  //glLightfv(GL_LIGHT1, GL_POSITION, (float[4]){CUBEWIN/2, CUBEWIN/4, CUBEWIN, 1});

  load_all_textures();

  glutMainLoop();

  return 0;
}
