/* moebiusgears, Copyright (c) 2007-2014 Jamie Zawinski <jwz@jwz.org>
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation.  No representations are made about the suitability of this
 * software for any purpose.  It is provided "as is" without express or 
 * implied warranty.
 */

#define DEFAULTS	"*delay:	30000       \n" \
			"*count:        17          \n" \
			"*showFPS:      False       \n" \
			"*wireframe:    False       \n" \
			"*suppressRotationAnimation: True\n" \

# define release_mgears 0

#include "xlockmore.h"
#include "involute.h"
#include "normals.h"
#include "rotator.h"
#include "gltrackball.h"
#include <ctype.h>

#ifdef USE_GL /* whole file */


#define DEF_SPIN        "True"
#define DEF_WANDER      "True"
#define DEF_ROLL        "True"
#define DEF_SPEED       "1.0"
#define DEF_TEETH       "15"

typedef struct {

  gear g;
  GLfloat pos_th;  /* position on ring of gear system */
  GLfloat pos_thz; /* rotation out of plane of gear system */
} mogear;

typedef struct {
  GLXContext *glx_context;
  rotator *rot;
  trackball_state *trackball;
  Bool button_down_p;

  int ngears;
  mogear *gears;
  GLfloat ring_r;  /* radius of gear system */
  GLfloat roll_th;

} mgears_configuration;

static mgears_configuration *bps = NULL;

static Bool do_spin;
static GLfloat speed;
static Bool do_wander;
static Bool do_roll;
static int teeth_arg;

static XrmOptionDescRec opts[] = {
  { "-spin",   ".spin",   XrmoptionNoArg, "True"  },
  { "+spin",   ".spin",   XrmoptionNoArg, "False" },
  { "-speed",  ".speed",  XrmoptionSepArg, 0      },
  { "-wander", ".wander", XrmoptionNoArg, "True"  },
  { "+wander", ".wander", XrmoptionNoArg, "False" },
  { "-roll",   ".roll",   XrmoptionNoArg, "True"  },
  { "+roll",   ".roll",   XrmoptionNoArg, "False" },
  { "-teeth",  ".teeth",  XrmoptionSepArg, 0      },
};

static argtype vars[] = {
  {&do_spin,   "spin",   "Spin",   DEF_SPIN,   t_Bool},
  {&do_wander, "wander", "Wander", DEF_WANDER, t_Bool},
  {&do_roll,   "roll",   "Roll",   DEF_ROLL,   t_Bool},
  {&speed,     "speed",  "Speed",  DEF_SPEED,  t_Float},
  {&teeth_arg, "teeth",  "Teeth",  DEF_TEETH,  t_Int},
};

ENTRYPOINT ModeSpecOpt mgears_opts = {countof(opts), opts, countof(vars), vars, NULL};


/* Window management, etc
 */
ENTRYPOINT void
reshape_mgears (ModeInfo *mi, int width, int height)
{
  GLfloat h = (GLfloat) height / (GLfloat) width;
  int y = 0;

  if (width > height * 5) {   /* tiny window: show middle */
    height = width * 9/16;
    y = -height/2;
    h = height / (GLfloat) width;
  }

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

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective (30.0, 1/h, 1.0, 100.0);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt( 0.0, 0.0, 30.0,
             0.0, 0.0, 0.0,
             0.0, 1.0, 0.0);

  {
    GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi)
                 ? (MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi))
                 : 1);
    glScalef (s, s, s);
  }

  glClear(GL_COLOR_BUFFER_BIT);
}


static void
reset_mgears (ModeInfo *mi)
{
  mgears_configuration *bp = &bps[MI_SCREEN(mi)];
  int wire = MI_IS_WIREFRAME(mi);
  int total_gears = MI_COUNT(mi);
  double gears_per_turn;
  double gear_r, tw, th, thick, slope;
  int i, nubs, size;

  if (! (total_gears & 1)) 
    total_gears++;		/* must be odd or gears intersect */

  /* Number of teeth must be odd if number of gears is odd, or teeth don't
     mesh when the loop closes.  And since number of gears must be odd...
  */
  if (! (teeth_arg & 1)) teeth_arg++;
  if (teeth_arg < 7) teeth_arg = 7;

  if (total_gears < 13)	/* gear mesh angle is too steep with less */
    total_gears = 13;

  thick = 0.2;
  nubs = (random() & 3) ? 0 : (random() % teeth_arg) / 2;

  slope = 0;

  /* Sloping gears are incompatible with "-roll" ... */
  /* slope= -M_PI * 2 / total_gears; */

  gears_per_turn = total_gears / 2.0;

  bp->ring_r = 3;
  gear_r = M_PI * bp->ring_r / gears_per_turn;
  tw = 0;
  th = gear_r * 2.5 / teeth_arg;

  /* If the gears are small, use a lower density mesh. */
  size = (gear_r > 0.60 ? INVOLUTE_HUGE   :
          gear_r > 0.32 ? INVOLUTE_LARGE  :
          gear_r > 0.13 ? INVOLUTE_MEDIUM :
          INVOLUTE_SMALL);

  /* If there are lots of teeth, use a lower density mesh. */
  if (teeth_arg > 77)
    size = INVOLUTE_SMALL;
  if (teeth_arg > 45 && size >= INVOLUTE_HUGE)
    size = INVOLUTE_MEDIUM;

  if (bp->gears)
    {
      for (i = 0; i < bp->ngears; i++)
        glDeleteLists (bp->gears[i].g.dlist, 1);
      free (bp->gears);
      bp->gears = 0;
    }

  bp->ngears = total_gears;

  bp->gears = (mogear *) calloc (bp->ngears, sizeof(*bp->gears));
  for (i = 0; i < bp->ngears; i++)
    {
      mogear *mg = &bp->gears[i];
      gear *g = &mg->g;

      g->r           = gear_r;
      g->size        = size;
      g->nteeth      = teeth_arg;
      g->tooth_w     = tw;
      g->tooth_h     = th;
      g->tooth_slope = slope;
      g->thickness   = g->r * thick;
      g->thickness2  = g->thickness * 0.1;
      g->thickness3  = g->thickness;
      g->inner_r     = g->r * 0.80;
      g->inner_r2    = g->r * 0.60;
      g->inner_r3    = g->r * 0.55;
      g->nubs        = nubs;
      mg->pos_th     = (M_PI * 2 / gears_per_turn) * i;
      mg->pos_thz    = (M_PI / 2 / gears_per_turn) * i;

      g->th = ((i & 1)
               ? (M_PI * 2 / g->nteeth)
               : 0);

      /* Colorize
       */
      g->color[0] = 0.7 + frand(0.3);
      g->color[1] = 0.7 + frand(0.3);
      g->color[2] = 0.7 + frand(0.3);
      g->color[3] = 1.0;

      g->color2[0] = g->color[0] * 0.85;
      g->color2[1] = g->color[1] * 0.85;
      g->color2[2] = g->color[2] * 0.85;
      g->color2[3] = g->color[3];

      /* Now render the gear into its display list.
       */
      g->dlist = glGenLists (1);
      if (! g->dlist)
        {
          check_gl_error ("glGenLists");
          abort();
        }

      glNewList (g->dlist, GL_COMPILE);
      g->polygons += draw_involute_gear (g, wire);
      glEndList ();
    }
}


ENTRYPOINT void 
init_mgears (ModeInfo *mi)
{
  mgears_configuration *bp;
  int wire = MI_IS_WIREFRAME(mi);

  MI_INIT (mi, bps);

  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  reshape_mgears (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  if (!wire)
    {
      GLfloat pos[4] = {1.0, 1.0, 1.0, 0.0};
      GLfloat amb[4] = {0.0, 0.0, 0.0, 1.0};
      GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
      GLfloat spc[4] = {0.0, 1.0, 1.0, 1.0};

      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_CULL_FACE);

      glLightfv(GL_LIGHT0, GL_POSITION, pos);
      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);
    }

  {
    double spin_speed   = 0.5;
    double wander_speed = 0.01;
    double spin_accel   = 2.0;

    bp->rot = make_rotator (do_spin ? spin_speed : 0,
                            do_spin ? spin_speed : 0,
                            do_spin ? spin_speed : 0,
                            spin_accel,
                            do_wander ? wander_speed : 0,
                            False /* don't randomize */
                            );
    bp->trackball = gltrackball_init (True);
  }

  reset_mgears (mi);
}


ENTRYPOINT Bool
mgears_handle_event (ModeInfo *mi, XEvent *event)
{
  mgears_configuration *bp = &bps[MI_SCREEN(mi)];

  if (gltrackball_event_handler (event, bp->trackball,
                                 MI_WIDTH (mi), MI_HEIGHT (mi),
                                 &bp->button_down_p))
    return True;
  else if (event->xany.type == KeyPress)
    {
      KeySym keysym;
      char c = 0;
      XLookupString (&event->xkey, &c, 1, &keysym, 0);
      if (c == '+' || c == '=' ||
          keysym == XK_Up || keysym == XK_Right || keysym == XK_Next)
        {
          MI_COUNT(mi) += 2;
          reset_mgears (mi);
          return True;
        }
      else if (c == '-' || c == '_' ||
               keysym == XK_Down || keysym == XK_Left || keysym == XK_Prior)
        {
          if (MI_COUNT(mi) <= 13)
            return False;
          MI_COUNT(mi) -= 2;
          reset_mgears (mi);
          return True;
        }
      else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
        goto DEF;
    }
  else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
    {
    DEF:
      MI_COUNT(mi) = 13 + (2 * (random() % 10));
      reset_mgears (mi);
      return True;
    }

  return False;
}


ENTRYPOINT void
draw_mgears (ModeInfo *mi)
{
  mgears_configuration *bp = &bps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  int i;

  if (!bp->glx_context)
    return;

  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);

  glShadeModel(GL_SMOOTH);

  glEnable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  glEnable(GL_CULL_FACE);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();

  glScalef(1.1, 1.1, 1.1);

  {
    double x, y, z;
    get_position (bp->rot, &x, &y, &z, !bp->button_down_p);
    glTranslatef ((x - 0.5) * 4,
                  (y - 0.5) * 4,
                  (z - 0.5) * 7);

    gltrackball_rotate (bp->trackball);

    get_rotation (bp->rot, &x, &y, &z, !bp->button_down_p);

    /* add a little rotation for -no-spin mode */
    x -= 0.14;
    y -= 0.06;

    glRotatef (x * 360, 1.0, 0.0, 0.0);
    glRotatef (y * 360, 0.0, 1.0, 0.0);
    glRotatef (z * 360, 0.0, 0.0, 1.0);
  }

  mi->polygon_count = 0;

  glScalef (1.5, 1.5, 1.5);

/*#define DEBUG*/

#ifdef DEBUG
  glScalef (.5, .5, .5);
  glTranslatef (0, -bp->gears[0].g.r * bp->ngears, 0);
#endif

  for (i = 0; i < bp->ngears; i++)
    {
      mogear *mg = &bp->gears[i];
      gear *g = &mg->g;

      glPushMatrix();
#ifndef DEBUG
      glRotatef (mg->pos_th  * 180 / M_PI, 0, 0, 1);  /* rotation on ring */
      glTranslatef (bp->ring_r, 0, 0);                /* position on ring */
      glRotatef (mg->pos_thz * 180 / M_PI, 0, 1, 0);  /* twist a bit */

      if (do_roll)
        {
          glRotatef (bp->roll_th * 180 / M_PI, 0, 1, 0);
          bp->roll_th += speed * 0.0005;
        }
#else
      glTranslatef (0, i * 2 * g->r, 0);
#endif
      glRotatef (g->th * 180 / M_PI, 0, 0, 1);

      glCallList (g->dlist);
      mi->polygon_count += g->polygons;
      glPopMatrix ();
    }

  glPopMatrix ();

#ifndef DEBUG
  /* spin gears */
  for (i = 0; i < bp->ngears; i++)
    {
      mogear *mg = &bp->gears[i];
      mg->g.th += speed * (M_PI / 100) * (i & 1 ? 1 : -1);
    }
#endif

  if (mi->fps_p) do_fps (mi);
  glFinish();

  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_mgears (ModeInfo *mi)
{
  mgears_configuration *bp = &bps[MI_SCREEN(mi)];
  if (!bp->glx_context) return;
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
  if (bp->gears) free (bp->gears);
  if (bp->rot) free_rotator (bp->rot);
  if (bp->trackball) gltrackball_free (bp->trackball);
}

XSCREENSAVER_MODULE_2 ("MoebiusGears", moebiusgears, mgears)

#endif /* USE_GL */
