/* lavalite --- 3D Simulation a Lava Lite, written by jwz.
 *
 * This software Copyright (c) 2002-2017 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.
 *
 * LAVA®, LAVA LITE®, LAVA WORLD INTERNATIONAL® and the configuration of the
 * LAVA® brand motion lamp are registered trademarks of Haggerty Enterprises,
 * Inc.  The configuration of the globe and base of the motion lamp are
 * registered trademarks of Haggerty Enterprises, Inc. in the U.S.A. and in
 * other countries around the world.
 *
 * Official Lava Lite web site: http://www.lavaworld.com/
 *
 * Implementation details:
 *
 * The blobs are generated using metaballs.  For an explanation of what
 * those are, see http://astronomy.swin.edu.au/~pbourke/modelling/implicitsurf/
 * or http://www.fifi.org/doc/povray-manual/pov30005.htm
 *
 * Basically, each bubble of lava is a few (4) overlapping spherical metaballs
 * of various sizes following similar but slightly different steep, slow
 * parabolic arcs from the bottom to the top and back.
 *
 * We then polygonize the surface of the lava using the marching squares
 * algorithm implemented in marching.c.
 *
 * Like real lavalites, this program is very slow.
 *
 * Surprisingly, it's loading the CPU and not the graphics engine: the speed
 * bottleneck is in *computing* the scene rather than *rendering* it.  We
 * actually don't use a huge number of polygons, but computing the mesh for
 * the surface takes a lot of cycles.
 *
 * I eliminated all the square roots, but there is still a lot of
 * floating-point multiplication in here.  I tried optimizing away the
 * fp divisions, but that didn't seem to make a difference.
 *
 *     -style            lamp shape: classic, giant, cone, rocket, or random.
 *     -speed            frequency at which new blobs launch.
 *     -resolution       density of the polygon mesh.
 *     -count            max number of blobs allowed at once.
 *     -wander, -spin    whether to roll the scene around the screen.
 *     -lava-color       color of the blobbies
 *     -fluid-color      color of the stuff the blobbies float in
 *     -base-color       color of the base of the lamp
 *     -table-color      color of the table under the lamp
 *     -impatient        at startup, skip forward in the animation so
 *                       that at least one blob is fully exposed.
 *
 * TODO:
 *
 *    - make the table look better, somehow.
 *    - should the lava be emissive?  should the one at the bottom be
 *      more brightly colored than the ones at the top?  light distance?
 *    - is there some way to put a specular reflection on the front glass
 *      itself?  Maybe render it twice with FRONT/BACK tweaked, or alpha
 *      with depth buffering turned off?
 */

#define DEFAULTS	"*delay:	30000       \n" \
			"*showFPS:      False       \n" \
			"*wireframe:    False       \n" \
			"*geometry:	600x900\n"      \
			"*count:      " DEF_COUNT " \n" \

# define release_lavalite 0


#define BLOBS_PER_GROUP 4

#define GRAVITY         0.000013    /* odwnward acceleration */
#define CONVECTION      0.005       /* initial upward velocity (bell curve) */
#define TILT            0.00166666  /* horizontal velocity (bell curve) */

#undef ABS
#define ABS(n) ((n)<0?-(n):(n))
#undef SIGNOF
#define SIGNOF(n) ((n)<0?-1:1)

#include "xlockmore.h"
#include "marching.h"
#include "rotator.h"
#include "gltrackball.h"
#include "ximage-loader.h"
#include <ctype.h>

#ifdef USE_GL /* whole file */


#define DEF_SPIN        "Z"
#define DEF_WANDER      "False"
#define DEF_SPEED       "0.003"
#define DEF_RESOLUTION  "40"
#define DEF_SMOOTH      "True"
#define DEF_COUNT       "3"
#define DEF_STYLE       "random"
#define DEF_IMPATIENT   "False"
#define DEF_LCOLOR	"#FF0000" /* lava */
#define DEF_FCOLOR	"#00AAFF" /* fluid */
#define DEF_BCOLOR	"#666666" /* base */
#define DEF_TCOLOR	"#000000" /*"#00FF00"*/ /* table */

#define DEF_FTEX	"(none)"
#define DEF_BTEX	"(none)"
#define DEF_TTEX	"(none)"

typedef struct metaball metaball;

struct metaball {

  Bool alive_p;
  Bool static_p;

  double r;		/* hard radius */
  double R;		/* radius of field of influence */

  double z;		/* vertical position */
  double pos_r;		/* position on horizontal circle */
  double pos_th;	/* position on horizontal circle */
  double dr, dz;	/* current velocity */

  double x, y;		/* h planar position - compused from the above */

  metaball *leader;	/* stay close to this other ball */
};


typedef enum { CLASSIC = 0, GIANT, CONE, ROCKET } lamp_style;
typedef enum { CAP = 100, BOTTLE, BASE } lamp_part;

typedef struct {
  lamp_part part;
  GLfloat elevation;
  GLfloat radius;
  GLfloat texture_elevation;
} lamp_geometry;

static const lamp_geometry classic_lamp[] = {
  { CAP,    1.16, 0.089, 0.00 },
  { BOTTLE, 0.97, 0.120, 0.40 },
  { BOTTLE, 0.13, 0.300, 0.87 },
  { BOTTLE, 0.07, 0.300, 0.93 },
  { BASE,   0.00, 0.280, 0.00 },
  { BASE,  -0.40, 0.120, 0.50 },
  { BASE,  -0.80, 0.280, 1.00 },
  { 0, 0, 0, 0 },
};

static const lamp_geometry giant_lamp[] = {
  { CAP,    1.12, 0.105, 0.00 },
  { BOTTLE, 0.97, 0.130, 0.30 },
  { BOTTLE, 0.20, 0.300, 0.87 },
  { BOTTLE, 0.15, 0.300, 0.93 },
  { BASE,   0.00, 0.230, 0.00 },
  { BASE,  -0.18, 0.140, 0.20 },
  { BASE,  -0.80, 0.280, 1.00 },
  { 0, 0, 0, 0 },
};

static const lamp_geometry cone_lamp[] = {
  { CAP,    1.35, 0.001, 0.00 },
  { CAP,    1.35, 0.020, 0.00 },
  { CAP,    1.30, 0.055, 0.05 },
  { BOTTLE, 0.97, 0.120, 0.40 },
  { BOTTLE, 0.13, 0.300, 0.87 },
  { BASE,   0.00, 0.300, 0.00 },
  { BASE,  -0.04, 0.320, 0.04 },
  { BASE,  -0.60, 0.420, 0.50 },
  { 0, 0, 0, 0 },
};

static const lamp_geometry rocket_lamp[] = {
  { CAP,    1.35, 0.001, 0.00 },
  { CAP,    1.34, 0.020, 0.00 },
  { CAP,    1.30, 0.055, 0.05 },
  { BOTTLE, 0.97, 0.120, 0.40 },
  { BOTTLE, 0.13, 0.300, 0.87 },
  { BOTTLE, 0.07, 0.300, 0.93 },
  { BASE,   0.00, 0.280, 0.00 },
  { BASE,  -0.50, 0.180, 0.50 },
  { BASE,  -0.75, 0.080, 0.75 },
  { BASE,  -0.80, 0.035, 0.80 },
  { BASE,  -0.90, 0.035, 1.00 },
  { 0, 0, 0, 0 },
};



typedef struct {
  GLXContext *glx_context;
  lamp_style style;
  const lamp_geometry *model;
  rotator *rot;
  rotator *rot2;
  trackball_state *trackball;
  Bool button_down_p;

  GLfloat max_bottle_radius;	   /* radius of widest part of the bottle */

  GLfloat launch_chance;	   /* how often to percolate */
  int blobs_per_group;		   /* how many metaballs we launch at once */
  Bool just_started_p;		   /* so we launch some goo right away */

  int grid_size;		   /* resolution for marching-cubes */
  int nballs;
  metaball *balls;

  GLuint bottle_list;
  GLuint ball_list;

  int bottle_poly_count;	   /* polygons in the bottle only */

} lavalite_configuration;

static lavalite_configuration *bps = NULL;

static char *do_spin;
static char *do_style;
static GLfloat speed;
static Bool do_wander;
static int resolution;
static Bool do_smooth;
static Bool do_impatient;

static char *lava_color_str, *fluid_color_str, *base_color_str,
  *table_color_str;
static char *fluid_tex, *base_tex, *table_tex;

static GLfloat lava_color[4], fluid_color[4], base_color[4], table_color[4];

static const GLfloat lava_spec[4] = {1.0, 1.0, 1.0, 1.0};
static const GLfloat lava_shininess = 128.0;
static const GLfloat foot_color[4] = {0.2, 0.2, 0.2, 1.0};

static const GLfloat light0_pos[4] = {-0.6, 0.0, 1.0, 0.0};
static const GLfloat light1_pos[4] = { 1.0, 0.0, 0.2, 0.0};
static const GLfloat light2_pos[4] = { 0.6, 0.0, 1.0, 0.0};



static XrmOptionDescRec opts[] = {
  { "-style",  ".style",  XrmoptionSepArg, 0 },
  { "-spin",   ".spin",   XrmoptionSepArg, 0 },
  { "+spin",   ".spin",   XrmoptionNoArg, "" },
  { "-speed",  ".speed",  XrmoptionSepArg, 0 },
  { "-wander", ".wander", XrmoptionNoArg, "True" },
  { "+wander", ".wander", XrmoptionNoArg, "False" },
  { "-resolution", ".resolution", XrmoptionSepArg, 0 },
  { "-smooth", ".smooth", XrmoptionNoArg, "True" },
  { "+smooth", ".smooth", XrmoptionNoArg, "False" },
  { "-impatient", ".impatient", XrmoptionNoArg, "True" },
  { "+impatient", ".impatient", XrmoptionNoArg, "False" },

  { "-lava-color",   ".lavaColor",   XrmoptionSepArg, 0 },
  { "-fluid-color",  ".fluidColor",  XrmoptionSepArg, 0 },
  { "-base-color",   ".baseColor",   XrmoptionSepArg, 0 },
  { "-table-color",  ".tableColor",  XrmoptionSepArg, 0 },

  { "-fluid-texture",".fluidTexture",  XrmoptionSepArg, 0 },
  { "-base-texture", ".baseTexture",   XrmoptionSepArg, 0 },
  { "-table-texture",".tableTexture",  XrmoptionSepArg, 0 },
};

static argtype vars[] = {
  {&do_style,     "style",      "Style",      DEF_STYLE,      t_String},
  {&do_spin,      "spin",       "Spin",       DEF_SPIN,       t_String},
  {&do_wander,    "wander",     "Wander",     DEF_WANDER,     t_Bool},
  {&speed,        "speed",      "Speed",      DEF_SPEED,      t_Float},
  {&resolution,   "resolution", "Resolution", DEF_RESOLUTION, t_Int},
  {&do_smooth,    "smooth",     "Smooth",     DEF_SMOOTH,     t_Bool},
  {&do_impatient, "impatient",  "Impatient",  DEF_IMPATIENT,  t_Bool},

  {&lava_color_str,  "lavaColor",    "LavaColor",  DEF_LCOLOR, t_String},
  {&fluid_color_str, "fluidColor",   "FluidColor", DEF_FCOLOR, t_String},
  {&base_color_str,  "baseColor",    "BaseColor",  DEF_BCOLOR, t_String},
  {&table_color_str, "tableColor",   "TableColor", DEF_TCOLOR, t_String},

  {&fluid_tex,       "fluidTexture", "FluidTexture", DEF_FTEX, t_String},
  {&base_tex,        "baseTexture",  "BaseTexture",  DEF_BTEX, t_String},
  {&table_tex,       "tableTexture", "BaseTexture",  DEF_TTEX, t_String},
};

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


/* Window management, etc
 */
ENTRYPOINT void
reshape_lavalite (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 * 3;
    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);

  glClear(GL_COLOR_BUFFER_BIT);
}



/* Textures
 */

static Bool
load_texture (ModeInfo *mi, const char *filename)
{
  Display *dpy = mi->dpy;
  Visual *visual = mi->xgwa.visual;
  char buf[1024];
  XImage *image;

  if (!filename ||
      !*filename ||
      !strcasecmp (filename, "(none)"))
    {
      glDisable (GL_TEXTURE_2D);
      return False;
    }

  image = file_to_ximage (dpy, visual, filename);
  if (!image) return False;

  clear_gl_error();
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA,
               image->width, image->height, 0,
               GL_RGBA, GL_UNSIGNED_BYTE, image->data);
  sprintf (buf, "texture: %.100s (%dx%d)",
           filename, image->width, image->height);
  check_gl_error(buf);

  glPixelStorei (GL_UNPACK_ALIGNMENT, 4);
  glPixelStorei (GL_UNPACK_ROW_LENGTH, image->width);
  glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
  glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
  glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameterf (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

  glEnable (GL_TEXTURE_2D);
  return True;
}



/* Generating the lamp's bottle, caps, and base.
 */

static int
draw_disc (GLfloat r, GLfloat z, int faces, Bool up_p, Bool wire)
{
  int j;
  GLfloat th;
  GLfloat step = M_PI * 2 / faces;
  int polys = 0;
  GLfloat x, y;

  glFrontFace (up_p ? GL_CW : GL_CCW);
  glNormal3f (0, (up_p ? 1 : -1), 0);
  glBegin (wire ? GL_LINE_LOOP : GL_TRIANGLES);

  x = r;
  y = 0;

  for (j = 0, th = 0; j <= faces; j++)
    {
      glTexCoord2f (-j / (GLfloat) faces, 1);
      glVertex3f (0, z, 0);

      glTexCoord2f (-j / (GLfloat) faces, 0);
      glVertex3f (x, z, y);

      th += step;
      x  = r * cos (th);
      y  = r * sin (th);

      glTexCoord2f (-j / (GLfloat) faces, 0);
      glVertex3f (x, z, y);

      polys++;
    }
  glEnd();

  return polys;
}


static int
draw_tube (GLfloat r0, GLfloat r1,
           GLfloat z0, GLfloat z1,
           GLfloat t0, GLfloat t1,
           int faces, Bool inside_out_p, Bool smooth_p, Bool wire)
{
  int polys = 0;
  GLfloat th;
  GLfloat x, y, x0=0, y0=0;
  GLfloat step = M_PI * 2 / faces;
  GLfloat s2 = step/2;
  int i;

  glFrontFace (inside_out_p ? GL_CW : GL_CCW);
  glBegin (wire ? GL_LINES : (smooth_p ? GL_QUAD_STRIP : GL_QUADS));

  th = 0;
  x = 1;
  y = 0;

  if (!smooth_p)
    {
      x0 = cos (s2);
      y0 = sin (s2);
    }

  if (smooth_p) faces++;

  for (i = 0; i < faces; i++)
    {
      int nsign = (inside_out_p ? -1 : 1);

      if (smooth_p)
        glNormal3f (x  * nsign, z1, y  * nsign);
      else
        glNormal3f (x0 * nsign, z1, y0 * nsign);

      glTexCoord2f (nsign * -i / (GLfloat) faces, 1-t1);
      glVertex3f (x * r1, z1, y * r1);

      glTexCoord2f (nsign * -i / (GLfloat) faces, 1-t0);
      glVertex3f (x * r0, z0, y * r0);

      th += step;
      x  = cos (th);
      y  = sin (th);

      if (!smooth_p)
        {
          x0 = cos (th + s2);
          y0 = sin (th + s2);

          glTexCoord2f (nsign * -(i+1) / (double) faces, 1-t0);
          glVertex3f (x * r0, z0, y * r0);

          glTexCoord2f (nsign * -(i+1) / (double) faces, 1-t1);
          glVertex3f (x * r1, z1, y * r1);
        }

      polys++;
    }
  glEnd();

  return polys;
}


static int
draw_table (GLfloat z, Bool wire)
{
  GLfloat faces = 6;
  GLfloat step = M_PI * 2 / faces;
  GLfloat s = 8;
  GLfloat th;
  int j;
  int polys = 0;

  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, table_color);

  glFrontFace(GL_CW);
  glNormal3f(0, 1, 0);
  glBegin(wire ? GL_LINE_LOOP : GL_TRIANGLE_FAN);

  if (! wire)
    {
      glTexCoord2f (-0.5, 0.5);
      glVertex3f(0, z, 0);
    }

  for (j = 0, th = 0; j <= faces; j++)
    {
      GLfloat x = cos (th);
      GLfloat y = sin (th);
      glTexCoord2f (-(x+1)/2.0, (y+1)/2.0);
      glVertex3f(x*s, z, y*s);
      th += step;
      polys++;
    }
  glEnd();
  return polys;
}


static int
draw_wing (GLfloat w, GLfloat h, GLfloat d, Bool wire)
{
  static const int coords[2][8][2] = {
    { {  0,   0 },
      { 10,  10 },
      { 20,  23 },
      { 30,  41 },
      { 40,  64 },
      { 45,  81 },
      { 50, 103 },
      { 53, 134 } },
    { {  0,  54 },
      { 10,  57 },
      { 20,  64 },
      { 30,  75 },
      { 40,  92 },
      { 45, 104 },
      { 50, 127 },
      { 51, 134 } 
    }
  };

  int polys = 0;
  int maxx = coords[0][countof(coords[0])-1][0];
  int maxy = coords[0][countof(coords[0])-1][1];
  unsigned int x;

  for (x = 1; x < countof(coords[0]); x++)
    {
      GLfloat px0 = (GLfloat) coords[0][x-1][0] / maxx * w;
      GLfloat py0 = (GLfloat) coords[0][x-1][1] / maxy * h;
      GLfloat px1 = (GLfloat) coords[1][x-1][0] / maxx * w;
      GLfloat py1 = (GLfloat) coords[1][x-1][1] / maxy * h;
      GLfloat px2 = (GLfloat) coords[0][x  ][0] / maxx * w;
      GLfloat py2 = (GLfloat) coords[0][x  ][1] / maxy * h;
      GLfloat px3 = (GLfloat) coords[1][x  ][0] / maxx * w;
      GLfloat py3 = (GLfloat) coords[1][x  ][1] / maxy * h;
      GLfloat zz = d/2;

      /* Left side
       */
      glFrontFace (GL_CW);
      glNormal3f (0, 0, -1);
      glBegin (wire ? GL_LINE_LOOP : GL_QUADS);

      glTexCoord2f (px0, py0); glVertex3f (px0, -py0, -zz);
      glTexCoord2f (px1, py1); glVertex3f (px1, -py1, -zz);
      glTexCoord2f (px3, py3); glVertex3f (px3, -py3, -zz);
      glTexCoord2f (px2, py2); glVertex3f (px2, -py2, -zz);
      polys++;
      glEnd();

      /* Right side
       */
      glFrontFace (GL_CCW);
      glNormal3f (0, 0, -1);
      glBegin (wire ? GL_LINE_LOOP : GL_QUADS);
      glTexCoord2f(px0, py0); glVertex3f (px0, -py0, zz);
      glTexCoord2f(px1, py1); glVertex3f (px1, -py1, zz);
      glTexCoord2f(px3, py3); glVertex3f (px3, -py3, zz);
      glTexCoord2f(px2, py2); glVertex3f (px2, -py2, zz);
      polys++;
      glEnd();

      /* Top edge
       */
      glFrontFace (GL_CCW);
      glNormal3f (1, -1, 0); /* #### wrong */
      glBegin (wire ? GL_LINE_LOOP : GL_QUADS);
      glTexCoord2f(px0, py0); glVertex3f (px0, -py0, -zz);
      glTexCoord2f(px0, py0); glVertex3f (px0, -py0,  zz);
      glTexCoord2f(px2, py2); glVertex3f (px2, -py2,  zz);
      glTexCoord2f(px2, py2); glVertex3f (px2, -py2, -zz);
      polys++;
      glEnd();

      /* Bottom edge
       */
      glFrontFace (GL_CW);
      glNormal3f (-1, 1, 0); /* #### wrong */
      glBegin (wire ? GL_LINE_LOOP : GL_QUADS);
      glTexCoord2f(px1, py1); glVertex3f (px1, -py1, -zz);
      glTexCoord2f(px1, py1); glVertex3f (px1, -py1,  zz);
      glTexCoord2f(px3, py3); glVertex3f (px3, -py3,  zz);
      glTexCoord2f(px3, py3); glVertex3f (px3, -py3, -zz);
      polys++;
      glEnd();


    }

  return polys;

}


static void
generate_bottle (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  int wire = MI_IS_WIREFRAME(mi);
  int faces = resolution * 1.5;
  Bool smooth = do_smooth;

  const lamp_geometry *top_slice = bp->model;
  const char *current_texture = 0;
  lamp_part last_part = 0;

  if (faces < 3)  faces = 3;
  else if (wire && faces > 20) faces = 20;
  else if (faces > 60) faces = 60;

  bp->bottle_poly_count = 0;

  glNewList (bp->bottle_list, GL_COMPILE);
  glPushMatrix();

  glRotatef (90, 1, 0, 0);
  glTranslatef (0, -0.5, 0);

  /* All parts of the lamp use the same specularity and shininess. */
  glMaterialfv (GL_FRONT, GL_SPECULAR,  lava_spec);
  glMateriali  (GL_FRONT, GL_SHININESS, lava_shininess);

  while (1)
    {
      const lamp_geometry *bot_slice = top_slice + 1;

      const char *texture = 0;
      GLfloat *color = 0;
      GLfloat t0, t1;

      glDisable (GL_LIGHT2);

      switch (top_slice->part)
        {
        case CAP:
        case BASE:
          texture = base_tex;
          color   = base_color;
          break;
        case BOTTLE:
          texture = fluid_tex;
          color   = fluid_color;
          if (!wire) glEnable (GL_LIGHT2);   /* light2 affects only fluid */
          break;
        default:
          abort();
          break;
        }

      if (!wire && texture && texture != current_texture)
        {
          current_texture = texture;
          load_texture (mi, current_texture);
        }
        
      /* Color the discs darker than the tube walls. */
      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, foot_color);

      /* Do a top disc if this is the first slice of the CAP or BASE.
       */
      if ((top_slice->part == CAP  && last_part == 0) ||
          (top_slice->part == BASE && last_part == BOTTLE))
        bp->bottle_poly_count +=
          draw_disc (top_slice->radius, top_slice->elevation, faces,
                     True, wire);

      /* Do a bottom disc if this is the last slice of the CAP or BASE.
       */
      if ((top_slice->part == CAP  && bot_slice->part == BOTTLE) ||
          (top_slice->part == BASE && bot_slice->part == 0))
        {
          const lamp_geometry *sl = (bot_slice->part == 0
                                     ? top_slice : bot_slice);
          bp->bottle_poly_count +=
            draw_disc (sl->radius, sl->elevation, faces, False, wire);
        }

      if (bot_slice->part == 0)    /* done! */
        break;

      /* Do a tube or cone
       */
      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color);

      t0 = top_slice->texture_elevation;
      t1 = bot_slice->texture_elevation;

      /* Restart the texture coordinates for the glass.
       */
      if (top_slice->part == BOTTLE)
        {
          Bool first_p = (top_slice[-1].part != BOTTLE);
          Bool last_p  = (bot_slice->part    != BOTTLE);
          if (first_p) t0 = 0;
          if (last_p)  t1 = 1;
        }

      bp->bottle_poly_count +=
        draw_tube (top_slice->radius, bot_slice->radius,
                   top_slice->elevation, bot_slice->elevation,
                   t0, t1,
                   faces,
                   (top_slice->part == BOTTLE),
                   smooth, wire);

      last_part = top_slice->part;
      top_slice++;
    }

  if (bp->style == ROCKET)
    {
      int i;
      for (i = 0; i < 3; i++)
        {
          glPushMatrix();
          glRotatef (120 * i, 0, 1, 0);
          glTranslatef (0.14, -0.05, 0);
          bp->bottle_poly_count += draw_wing (0.4, 0.95, 0.02, wire);
          glPopMatrix();
        }
      glTranslatef (0, -0.1, 0);  /* move floor down a little */
    }


  if (!wire) load_texture (mi, table_tex);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, table_color);
  bp->bottle_poly_count += draw_table (top_slice->elevation, wire);


  glPopMatrix ();
  glDisable (GL_TEXTURE_2D);   /* done with textured objects */
  glEndList ();
}




/* Generating blobbies
 */

static double
bellrand (double extent)    /* like frand(), but a bell curve. */
{
  return (((frand(extent) + frand(extent) + frand(extent)) / 3)
          - (extent/2));
}


static void move_ball (ModeInfo *mi, metaball *b);

/* Bring a ball into play, and re-randomize its values.
 */
static void
reset_ball (ModeInfo *mi, metaball *b)
{
/*  lavalite_configuration *bp = &bps[MI_SCREEN(mi)]; */

  b->r = 0.00001;
  b->R = 0.12 + bellrand(0.10);

  b->pos_r = bellrand (0.9);
  b->pos_th = frand(M_PI*2);
  b->z = 0;

  b->dr = bellrand(TILT);
  b->dz = CONVECTION;

  b->leader = 0;

  if (!b->alive_p)
    b->alive_p = True;

  move_ball (mi, b);
}


/* returns the first metaball that is not in use, or 0.
 */
static metaball *
get_ball (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  int i;
  for (i = 0; i < bp->nballs; i++)
    {
      metaball *b = &bp->balls[i];
      if (!b->alive_p)
        return b;
    }
  return 0;
}


/* Generate the blobs that don't move: the ones at teh top and bottom
   that are part of the scenery.
 */
static void
generate_static_blobs (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  metaball *b0, *b1;
  int i;

  b0 = get_ball (mi);
  if (!b0) abort();
  b0->static_p = True;
  b0->alive_p = True;
  b0->R = 0.6;
  b0->r = 0.3;

  /* the giant blob at the bottom of the bottle.
   */
  b0->pos_r = 0;
  b0->pos_th = 0;
  b0->dr = 0;
  b0->dz = 0;
  b0->x = 0;
  b0->y = 0;
  b0->z = -0.43;

  /* the small blob at the top of the bottle.
   */
  b1 = get_ball (mi);
  if (!b1) abort();

  *b1 = *b0;
  b1->R = 0.16;
  b1->r = 0.135;
  b1->z = 1.078;

  /* Some extra blobs at the bottom of the bottle, to jumble the surface.
   */
  for (i = 0; i < bp->blobs_per_group; i++)
    {
      b1 = get_ball (mi);
      if (!b1) abort();
      reset_ball (mi, b1);
      b1->static_p = True;
      b1->z = frand(0.04);
      b1->dr = 0;
      b1->dz = 0;
    }
}


static GLfloat
max_bottle_radius (lavalite_configuration *bp)
{
  GLfloat r = 0;
  const lamp_geometry *slice;
  for (slice = bp->model; slice->part != 0; slice++)
    {
      if (slice->part == BOTTLE && slice->radius > r)
        r = slice->radius;      /* top */
      if (slice[1].radius > r)
        r = slice[1].radius;    /* bottom */
    }
  return r;
}


static GLfloat
bottle_radius_at (lavalite_configuration *bp, GLfloat z)
{
  GLfloat topz = -999, botz = -999, topr = 0, botr = 0;
  const lamp_geometry *slice;
  GLfloat ratio;

  for (slice = bp->model; slice->part != 0; slice++)
    if (z > slice->elevation)
      {
        slice--;
        topz = slice->elevation;
        topr = slice->radius;
        break;
      }
  if (topz == -999) return 0;

  for (; slice->part != 0; slice++)
    if (z > slice->elevation)
      {
        botz = slice->elevation;
        botr = slice->radius;
        break;
      }
  if (botz == -999) return 0;

  ratio = (z - botz) / (topz - botz);

  return (botr + ((topr - botr) * ratio));
}


static void
move_ball (ModeInfo *mi, metaball *b)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  double gravity = GRAVITY;
  double real_r;

  if (b->static_p) return;

  b->pos_r += b->dr;
  b->z     += b->dz;

  b->dz -= gravity;

  if (b->pos_r > 0.9)
    {
      b->pos_r = 0.9;
      b->dr = -b->dr;
    }
  else if (b->pos_r < 0)
    {
      b->pos_r = -b->pos_r;
      b->dr = -b->dr;
    }

  real_r = b->pos_r * bottle_radius_at (bp, b->z);

  b->x = cos (b->pos_th) * real_r;
  b->y = sin (b->pos_th) * real_r;

  if (b->z < -b->R)  /* dropped below bottom of glass - turn it off */
    b->alive_p = False;
}


/* This function makes sure that balls that are part of a group always stay
   relatively close to each other.
 */
static void
clamp_balls (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  int i;
  for (i = 0; i < bp->nballs; i++)
    {
      metaball *b = &bp->balls[i];
      if (b->alive_p && b->leader)
        {
          double zslack = 0.1;
          double minz = b->leader->z - zslack;
          double maxz = b->leader->z + zslack;

          /* Try to keep the Z values near those of the leader.
             Don't let it go out of range (above or below) and clamp it
             if it does.  If we've clamped it, make sure dz will be
             moving it in the right direction (back toward the leader.)

             We aren't currently clamping r, only z -- doesn't seem to
             be needed.

             This is kind of flaky, I think.  Sometimes you can see
             the blobbies "twitch".  That's no good.
           */

          if (b->z < minz)
            {
              if (b->dz < 0) b->dz = -b->dz;
              b->z = minz - b->dz;
            }

          if (b->z > maxz)
            {
              if (b->dz > 0) b->dz = -b->dz;
              b->z = maxz + b->dz;
            }
        }
    }
}


static void
move_balls (ModeInfo *mi)   /* for great justice */
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  int i;
  for (i = 0; i < bp->nballs; i++)
    {
      metaball *b = &bp->balls[i];
      if (b->alive_p)
        move_ball (mi, b);
    }

  clamp_balls (mi);
}



/* Rendering blobbies using marching cubes.
 */

static double
compute_metaball_influence (lavalite_configuration *bp,
                            double x, double y, double z,
                            int nballs, metaball *balls)
{
  double vv = 0;
  int i;

  for (i = 0; i < nballs; i++)
    {
      metaball *b = &balls[i];
      double dx, dy, dz;
      double d2, r, R, r2, R2;

      if (!b->alive_p) continue;

      dx = x - b->x;
      dy = y - b->y;
      dz = z - b->z;
      R = b->R;

      if (dx > R || dx < -R ||    /* quick check before multiplying */
          dy > R || dy < -R ||
          dz > R || dz < -R)
        continue;

      d2 = (dx*dx + dy*dy + dz*dz);
      r = b->r;

      r2 = r*r;
      R2 = R*R;

      if (d2 <= r2)             /* (d <= r)   inside the hard radius */
        vv += 1;
      else if (d2 > R2)         /* (d > R)   outside the radius of influence */
        ;
      else          /* somewhere in between: linear drop-off from r=1 to R=0 */
        {
          /* was: vv += 1 - ((d-r) / (R-r)); */
          vv += 1 - ((d2-r2) / (R2-r2));
        }
    }

  return vv;
}


/* callback for marching_cubes() */
static void *
obj_init (double grid_size, void *closure)
{
  lavalite_configuration *bp = (lavalite_configuration *) closure;
  bp->grid_size = grid_size;

  return closure;
}


/* Returns True if the given point is outside of the glass tube.
 */
static double
clipped_by_glass_p (double x, double y, double z,
                    lavalite_configuration *bp)
{
  double d2, or, or2, ir2;

  or = bp->max_bottle_radius;

  if (x > or || x < -or ||    /* quick check before multiplying */
      y > or || y < -or)
    return 0;

  d2 = (x*x + y*y);
  or = bottle_radius_at (bp, z);

  or2 = or*or;

  if (d2 > or2)   /* (sqrt(d) > or) */
    return 0;

  ir2 = or2 * 0.7;

  if (d2 > ir2)  /* (sqrt(d) > ir) */
    {
      double dr1 = or2;
      double dr2 = ir2;
      /* was: (1 - (d-ratio2) / (ratio1-ratio2)) */
      return (1 - (d2-dr2) / (dr1-dr2));
    }

  return 1;
}



/* callback for marching_cubes() */
static double
obj_compute (double x, double y, double z, void *closure)
{
  lavalite_configuration *bp = (lavalite_configuration *) closure;
  double clip;

  x /= bp->grid_size;	/* convert from 0-N to 0-1. */
  y /= bp->grid_size;
  z /= bp->grid_size;

  x -= 0.5;	/* X and Y range from -.5 to +.5; z ranges from 0-1. */
  y -= 0.5;

  clip = clipped_by_glass_p (x, y, z, bp);
  if (clip == 0) return 0;

  return (clip *
          compute_metaball_influence (bp, x, y, z, bp->nballs, bp->balls));
}


/* callback for marching_cubes() */
static void
obj_free (void *closure)
{
}


/* Send a new blob travelling upward.
   This blob will actually be composed of N metaballs that are near
   each other.
 */
static void
launch_balls (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  metaball *b0 = get_ball (mi);
  int i;

  if (!b0) return;
  reset_ball (mi, b0);

  for (i = 0; i < bp->blobs_per_group; i++)
    {
      metaball *b1 = get_ball (mi);
      if (!b1) break;
      *b1 = *b0;

      reset_ball (mi, b1);
      b1->leader = b0;

# define FROB(FIELD,AMT) \
         b1->FIELD += (bellrand(AMT) * b0->FIELD)

   /* FROB (pos_r,  0.7); */
   /* FROB (pos_th, 0.7); */
      FROB (dr, 0.8);
      FROB (dz, 0.6);
# undef FROB
    }

}


static void
animate_lava (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  int wire = MI_IS_WIREFRAME(mi);
  Bool just_started_p = bp->just_started_p;

  double isolevel = 0.3;

  /* Maybe bubble a new blobby to the surface.
   */
  if (just_started_p ||
      frand(1.0) < bp->launch_chance)
    {
      bp->just_started_p = False;
      launch_balls (mi);

      if (do_impatient && just_started_p)
        while (1)
          {
            int i;
            move_balls (mi);
            for (i = 0; i < bp->nballs; i++)
              {
                metaball *b = &bp->balls[i];
                if (b->alive_p && !b->static_p && !b->leader &&
                    b->z > 0.5)
                  goto DONE;
              }
          }
      DONE: ;
    }

  move_balls (mi);

  glNewList (bp->ball_list, GL_COMPILE);
  glPushMatrix();

  glMaterialfv (GL_FRONT, GL_SPECULAR,            lava_spec);
  glMateriali  (GL_FRONT, GL_SHININESS,           lava_shininess);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, lava_color);

  /* For the blobbies, the origin is on the axis at the bottom of the
     glass bottle; and the top of the bottle is +1 on Z.
   */
  glTranslatef (0, 0, -0.5);

  mi->polygon_count = 0;
  {
    double s;
    if (bp->grid_size == 0) bp->grid_size = 1;  /* first time through */
    s = 1.0/bp->grid_size;

    glPushMatrix();
    glTranslatef (-0.5, -0.5, 0);
    glScalef (s, s, s);
    marching_cubes (resolution, isolevel, wire, do_smooth,
                    obj_init, obj_compute, obj_free, bp,
                    &mi->polygon_count);
    glPopMatrix();
  }

  mi->polygon_count += bp->bottle_poly_count;

  glPopMatrix();
  glEndList ();
}



/* Startup initialization
 */

ENTRYPOINT Bool
lavalite_handle_event (ModeInfo *mi, XEvent *event)
{
  lavalite_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;

  return False;
}


static void
parse_color (ModeInfo *mi, const char *name, const char *s, GLfloat *a)
{
  XColor c;
  a[3] = 1.0;  /* alpha */

  if (! XParseColor (MI_DISPLAY(mi), MI_COLORMAP(mi), s, &c))
    {
      fprintf (stderr, "%s: can't parse %s color %s", progname, name, s);
      exit (1);
    }
  a[0] = c.red   / 65536.0;
  a[1] = c.green / 65536.0;
  a[2] = c.blue  / 65536.0;
}


ENTRYPOINT void 
init_lavalite (ModeInfo *mi)
{
  lavalite_configuration *bp;
  int wire = MI_IS_WIREFRAME(mi);

  MI_INIT (mi, bps);

  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  reshape_lavalite (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  {
    char *s = do_style;
    if (!s || !*s || !strcasecmp (s, "classic")) bp->style = CLASSIC;
    else if (!strcasecmp (s, "giant"))  bp->style = GIANT;
    else if (!strcasecmp (s, "cone"))   bp->style = CONE;
    else if (!strcasecmp (s, "rocket")) bp->style = ROCKET;
    else if (!strcasecmp (s, "random"))
      {
        if (random() & 1) bp->style = CLASSIC;  /* half the time */
        else bp->style = (random() % ((int) ROCKET+1));
      }
    else
      {
        fprintf (stderr,
         "%s: style must be Classic, Giant, Cone, or Rocket (not \"%s\")\n",
                 progname, s);
        exit (1);
      }
  }

  parse_color (mi, "lava",  lava_color_str,  lava_color);
  parse_color (mi, "fluid", fluid_color_str, fluid_color);
  parse_color (mi, "base",  base_color_str,  base_color);
  parse_color (mi, "table", table_color_str, table_color);

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

      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
      glEnable(GL_LIGHT1);
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_CULL_FACE);
      glEnable(GL_NORMALIZE);
      glShadeModel(GL_SMOOTH);

      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT0, GL_SPECULAR, spc0);

      glLightfv(GL_LIGHT1, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT1, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT1, GL_SPECULAR, spc1);

      glLightfv(GL_LIGHT2, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT2, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT2, GL_SPECULAR, spc0);
    }

  {
    Bool spinx=False, spiny=False, spinz=False;
    double spin_speed   = 0.4;
    double wander_speed = 0.03;

    char *s = do_spin;
    while (*s)
      {
        if      (*s == 'x' || *s == 'X') spinx = True;
        else if (*s == 'y' || *s == 'Y') spiny = True;
        else if (*s == 'z' || *s == 'Z') spinz = True;
        else if (*s == '0') ;
        else
          {
            fprintf (stderr,
         "%s: spin must contain only the characters X, Y, or Z (not \"%s\")\n",
                     progname, do_spin);
            exit (1);
          }
        s++;
      }

    bp->rot = make_rotator (spinx ? spin_speed : 0,
                            spiny ? spin_speed : 0,
                            spinz ? spin_speed : 0,
                            1.0,
                            do_wander ? wander_speed : 0,
                            False);
    bp->rot2 = make_rotator (spin_speed, 0, 0,
                             1, 0.1,
                             False);
    bp->trackball = gltrackball_init (False);

    /* Tilt the scene a bit: lean the normal lamps toward the viewer,
       and the huge lamps away. */
    gltrackball_reset (bp->trackball,
                       -0.3 + frand(0.6),
                       (bp->style == ROCKET || bp->style == GIANT
                        ?  frand (0.2)
                        : -frand (0.6)));
  }

  switch (bp->style)
    {
    case CLASSIC: bp->model = classic_lamp; break;
    case GIANT:   bp->model = giant_lamp;   break;
    case CONE:    bp->model = cone_lamp;    break;
    case ROCKET:  bp->model = rocket_lamp;  break;
    default: abort(); break;
    }

  bp->max_bottle_radius = max_bottle_radius (bp);

  bp->launch_chance = speed;
  bp->blobs_per_group = BLOBS_PER_GROUP;
  bp->just_started_p = True;

  bp->nballs = (((MI_COUNT (mi) + 1) * bp->blobs_per_group)
                + 2);
  bp->balls = (metaball *) calloc (sizeof(*bp->balls), bp->nballs+1);

  bp->bottle_list = glGenLists (1);
  bp->ball_list = glGenLists (1);

  generate_bottle (mi);
  generate_static_blobs (mi);
}


/* Render one frame
 */

ENTRYPOINT void
draw_lavalite (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);

  if (!bp->glx_context)
    return;

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

  glMatrixMode (GL_MODELVIEW);
  glPushMatrix ();

  {
    double cx, cy, cz; /* camera position, 0-1. */
    double px, py, pz; /* object position, 0-1. */
    double rx, ry, rz; /* object rotation, 0-1. */

    get_position (bp->rot2, 0,   &cy, &cz, !bp->button_down_p);
    get_rotation (bp->rot2, &cx, 0,   0,   !bp->button_down_p);

    get_position (bp->rot,  &px, &py, &pz, !bp->button_down_p);
    get_rotation (bp->rot,  &rx, &ry, &rz, !bp->button_down_p);

#if 1
    cx = 0.5;
    cy = 0.5;
    cz = 1.0;

#else  /* #### this crud doesn't really work yet */


    /* We have c[xyz] parameters describing a camera position, but we don't
       want to just map those to points in space: the lamp doesn't look very
       good from the inside, or from underneath...

       Good observation points form a ring around the lamp: basically, a
       torus ringing the lamp, parallel to the lamp's floor.

       We interpret cz as distance from the origin.
       cy as elevation.
       cx is then used as position in the torus (theta).
     */

    {
      double cx2, cy2, cz2;
      double d;

      cx2 = 0.5;
      cy2 = 0.5;
      cz2 = 1.0;

      cy2 = (cy * 0.4);		/* cam elevation: 0.0 (table) - 0.4 up. */
      d = 0.9 + cz;		/* cam distance: 0.9 - 1.9. */

      cz2 = 0.5 + (d * cos (cx * M_PI * 2));
      cx2 = 0.5 + (d * sin (cx * M_PI * 2));


      cx = cx2;
      cy = cy2;
      cz = cz2;
    }
#endif  /* 0 */

    glLoadIdentity();
    glRotatef(current_device_rotation(), 0, 0, 1);

    gluLookAt ((cx - 0.5) * 8,		/* Position the camera */
               (cy - 0.5) * 8,
               (cz - 0.5) * 8,
               0, 0, 0,
               0, 1, 0);

    gltrackball_rotate (bp->trackball);	/* Apply mouse-based camera position */

    glRotatef (-90, 1, 0, 0);  /* Right side up */


    /* Place the lights relative to the object, before the object has
       been rotated or wandered within the scene. */
    glLightfv(GL_LIGHT0, GL_POSITION, light0_pos);
    glLightfv(GL_LIGHT1, GL_POSITION, light1_pos);
    glLightfv(GL_LIGHT2, GL_POSITION, light2_pos);


    /* Position the lamp in the scene according to the "wander" settings */
    glTranslatef ((px - 0.5), (py - 0.5), (pz - 0.5));

    /* Rotate the object according to the "spin" settings */
    glRotatef (rx * 360, 1.0, 0.0, 0.0);
    glRotatef (ry * 360, 0.0, 1.0, 0.0);
    glRotatef (rz * 360, 0.0, 0.0, 1.0);

    /* Move the lamp up slightly: make 0,0 be at its vertical center. */
    switch (bp->style)
      {
      case CLASSIC: glTranslatef (0, 0, 0.33); break;
      case GIANT:   glTranslatef (0, 0, 0.33); break;
      case CONE:    glTranslatef (0, 0, 0.16); break;
      case ROCKET:  glTranslatef (0, 0, 0.30);
                    glScalef (0.85,0.85,0.85); break;
      default: abort(); break;
      }
  }

  animate_lava (mi);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  glCallList (bp->bottle_list);
  glCallList (bp->ball_list);
  glPopMatrix ();

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

  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_lavalite (ModeInfo *mi)
{
  lavalite_configuration *bp = &bps[MI_SCREEN(mi)];
  if (!bp->glx_context) return;
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
  if (bp->balls) free (bp->balls);
  if (bp->trackball) gltrackball_free (bp->trackball);
  if (bp->rot) free_rotator (bp->rot);
  if (bp->rot2) free_rotator (bp->rot2);
  if (glIsList(bp->bottle_list)) glDeleteLists(bp->bottle_list, 1);
  if (glIsList(bp->ball_list)) glDeleteLists(bp->ball_list, 1);
}

XSCREENSAVER_MODULE ("Lavalite", lavalite)

#endif /* USE_GL */
