/*
 * Copyright (C) 2009 Chia-I Wu <olv@0xlab.org>
 *
 * Based on eglgears by
 * Copyright (C) 1999-2001  Brian Paul   All Rights Reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <assert.h>

#include <GLES/gl.h>
#include "winsys.h"

#ifndef M_PI
#define M_PI 3.14159265
#endif


struct gear {
   GLuint vbo;
   GLfloat *vertices;
   GLsizei stride;

   GLint num_teeth;
};

static GLfloat view_rotx = 20.0, view_roty = 30.0, view_rotz = 0.0;
static struct gear gears[3];
static GLfloat angle = 0.0;

/*
 *  Initialize a gear wheel.
 *
 *  Input:  gear - gear to initialize
 *          inner_radius - radius of hole at center
 *          outer_radius - radius at center of teeth
 *          width - width of gear
 *          teeth - number of teeth
 *          tooth_depth - depth of tooth
 */
static void
init_gear(struct gear *gear, GLfloat inner_radius, GLfloat outer_radius,
          GLfloat width, GLint teeth, GLfloat tooth_depth)
{
   GLfloat r0, r1, r2;
   GLfloat a0, da;
   GLint verts_per_tooth, total_verts, total_size;
   GLint count, i;
   GLfloat *verts;

   r0 = inner_radius;
   r1 = outer_radius - tooth_depth / 2.0;
   r2 = outer_radius + tooth_depth / 2.0;

   a0 = 2.0 * M_PI / teeth;
   da = a0 / 4.0;

   gear->vbo = 0;
   gear->vertices = NULL;
   gear->stride = sizeof(GLfloat) * 6; /* XYZ + normal */
   gear->num_teeth = teeth;

   verts_per_tooth = 10 + 4;
   total_verts = teeth * verts_per_tooth;
   total_size = total_verts * gear->stride;

   verts = malloc(total_size);
   if (!verts) {
      printf("failed to allocate vertices\n");
      return;
   }

#define GEAR_VERT(r, n, sign)                      \
   do {                                            \
      verts[count * 6 + 0] = (r) * vx[n];          \
      verts[count * 6 + 1] = (r) * vy[n];          \
      verts[count * 6 + 2] = (sign) * width * 0.5; \
      verts[count * 6 + 3] = normal[0];            \
      verts[count * 6 + 4] = normal[1];            \
      verts[count * 6 + 5] = normal[2];            \
      count++;                                     \
   } while (0)

   count = 0;
   for (i = 0; i < teeth; i++) {
      GLfloat normal[3];
      GLfloat vx[5], vy[5];
      GLfloat u, v;

      normal[0] = 0.0;
      normal[1] = 0.0;
      normal[2] = 0.0;

      vx[0] = cos(i * a0 + 0 * da);
      vy[0] = sin(i * a0 + 0 * da);
      vx[1] = cos(i * a0 + 1 * da);
      vy[1] = sin(i * a0 + 1 * da);
      vx[2] = cos(i * a0 + 2 * da);
      vy[2] = sin(i * a0 + 2 * da);
      vx[3] = cos(i * a0 + 3 * da);
      vy[3] = sin(i * a0 + 3 * da);
      vx[4] = cos(i * a0 + 4 * da);
      vy[4] = sin(i * a0 + 4 * da);

      /* outward faces of a tooth, 10 verts */
      normal[0] = vx[0];
      normal[1] = vy[0];
      GEAR_VERT(r1, 0,  1);
      GEAR_VERT(r1, 0, -1);

      u = r2 * vx[1] - r1 * vx[0];
      v = r2 * vy[1] - r1 * vy[0];
      normal[0] = v;
      normal[1] = -u;
      GEAR_VERT(r2, 1,  1);
      GEAR_VERT(r2, 1, -1);

      normal[0] = vx[0];
      normal[1] = vy[0];
      GEAR_VERT(r2, 2,  1);
      GEAR_VERT(r2, 2, -1);

      u = r1 * vx[3] - r2 * vx[2];
      v = r1 * vy[3] - r2 * vy[2];
      normal[0] = v;
      normal[1] = -u;
      GEAR_VERT(r1, 3,  1);
      GEAR_VERT(r1, 3, -1);

      normal[0] = vx[0];
      normal[1] = vy[0];
      GEAR_VERT(r1, 4,  1);
      GEAR_VERT(r1, 4, -1);

      /* inside radius cylinder, 4 verts */
      normal[0] = -vx[4];
      normal[1] = -vy[4];
      GEAR_VERT(r0, 4,  1);
      GEAR_VERT(r0, 4, -1);

      normal[0] = -vx[0];
      normal[1] = -vy[0];
      GEAR_VERT(r0, 0,  1);
      GEAR_VERT(r0, 0, -1);

      assert(count % verts_per_tooth == 0);
   }
   assert(count == total_verts);
#undef GEAR_VERT

   gear->vertices = verts;

   /* setup VBO */
   glGenBuffers(1, &gear->vbo);
   if (gear->vbo) {
      glBindBuffer(GL_ARRAY_BUFFER, gear->vbo);
      glBufferData(GL_ARRAY_BUFFER, total_size, verts, GL_STATIC_DRAW);
   }
}


static void
draw_gear(const struct gear *gear)
{
   GLint i;

   if (!gear->vbo && !gear->vertices) {
      printf("nothing to be drawn\n");
      return;
   }

   if (gear->vbo) {
      glBindBuffer(GL_ARRAY_BUFFER, gear->vbo);
      glVertexPointer(3, GL_FLOAT, gear->stride, (const GLvoid *) 0);
      glNormalPointer(GL_FLOAT, gear->stride, (const GLvoid *) (sizeof(GLfloat) * 3));
   } else {
      glBindBuffer(GL_ARRAY_BUFFER, 0);
      glVertexPointer(3, GL_FLOAT, gear->stride, gear->vertices);
      glNormalPointer(GL_FLOAT, gear->stride, gear->vertices + 3);
   }

   glEnableClientState(GL_VERTEX_ARRAY);

   for (i = 0; i < gear->num_teeth; i++) {
      const GLint base = (10 + 4) * i;
      GLushort indices[7];

      glShadeModel(GL_FLAT);

      /* front face */
      indices[0] = base + 12;
      indices[1] = base +  0;
      indices[2] = base +  2;
      indices[3] = base +  4;
      indices[4] = base +  6;
      indices[5] = base +  8;
      indices[6] = base + 10;

      glNormal3f(0.0, 0.0, 1.0);
      glDrawElements(GL_TRIANGLE_FAN, 7, GL_UNSIGNED_SHORT, indices);

      /* back face */
      indices[0] = base + 13;
      indices[1] = base + 11;
      indices[2] = base +  9;
      indices[3] = base +  7;
      indices[4] = base +  5;
      indices[5] = base +  3;
      indices[6] = base +  1;

      glNormal3f(0.0, 0.0, -1.0);
      glDrawElements(GL_TRIANGLE_FAN, 7, GL_UNSIGNED_SHORT, indices);

      glEnableClientState(GL_NORMAL_ARRAY);

      /* outward face of a tooth */
      glDrawArrays(GL_TRIANGLE_STRIP, base, 10);

      /* inside radius cylinder */
      glShadeModel(GL_SMOOTH);
      glDrawArrays(GL_TRIANGLE_STRIP, base + 10, 4);

      glDisableClientState(GL_NORMAL_ARRAY);
   }

   glDisableClientState(GL_VERTEX_ARRAY);
}


static void
gears_draw(void *data)
{
   static const GLfloat red[4] = { 0.8, 0.1, 0.0, 1.0 };
   static const GLfloat green[4] = { 0.0, 0.8, 0.2, 1.0 };
   static const GLfloat blue[4] = { 0.2, 0.2, 1.0, 1.0 };

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

   glPushMatrix();
   glRotatef(view_rotx, 1.0, 0.0, 0.0);
   glRotatef(view_roty, 0.0, 1.0, 0.0);
   glRotatef(view_rotz, 0.0, 0.0, 1.0);

   glPushMatrix();
   glTranslatef(-3.0, -2.0, 0.0);
   glRotatef(angle, 0.0, 0.0, 1.0);

   glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, red);
   draw_gear(&gears[0]);

   glPopMatrix();

   glPushMatrix();
   glTranslatef(3.1, -2.0, 0.0);
   glRotatef(-2.0 * angle - 9.0, 0.0, 0.0, 1.0);

   glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, green);
   draw_gear(&gears[1]);

   glPopMatrix();

   glPushMatrix();
   glTranslatef(-3.1, 4.2, 0.0);
   glRotatef(-2.0 * angle - 25.0, 0.0, 0.0, 1.0);

   glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, blue);
   draw_gear(&gears[2]);

   glPopMatrix();

   glPopMatrix();

   /* advance rotation for next frame */
   angle += 0.5; /* 0.5 degree per frame */
   if (angle > 3600.0)
      angle -= 3600.0;
}


static void gears_fini(void)
{
   GLint i;
   for (i = 0; i < 3; i++) {
      struct gear *gear = &gears[i];
      if (gear->vbo) {
         glDeleteBuffers(1, &gear->vbo);
         gear->vbo = 0;
      }
      if (gear->vertices) {
         free(gear->vertices);
         gear->vertices = NULL;
      }
   }
}


static void gears_init(void)
{
   static const GLfloat pos[4] = { 5.0, 5.0, 10.0, 0.0 };

   glLightfv(GL_LIGHT0, GL_POSITION, pos);
   glEnable(GL_CULL_FACE);
   glEnable(GL_LIGHTING);
   glEnable(GL_LIGHT0);
   glEnable(GL_DEPTH_TEST);
   glEnable(GL_NORMALIZE);

   init_gear(&gears[0], 1.0, 4.0, 1.0, 20, 0.7);
   init_gear(&gears[1], 0.5, 2.0, 2.0, 10, 0.7);
   init_gear(&gears[2], 1.3, 2.0, 0.5, 10, 0.7);
}


/* new window size or exposure */
static void
gears_reshape(int width, int height)
{
   GLfloat h = (GLfloat) height / (GLfloat) width;

   glViewport(0, 0, (GLint) width, (GLint) height);

   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glFrustumf(-1.0, 1.0, -h, h, 5.0, 60.0);

   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
   glTranslatef(0.0, 0.0, -40.0);
}


static void gears_run(void)
{
   winsysRun(5.0, gears_draw, NULL);
}


int
main(int argc, char *argv[])
{
   EGLint width, height;

   if (!winsysInitScreen())
      exit(1);
   winsysQueryScreenSize(&width, &height);

   gears_init();
   gears_reshape(width, height);
   gears_run();
   gears_fini();

   winsysFiniScreen();

   return 0;
}