/* menger, Copyright (c) 2001-2014 Jamie Zawinski <jwz@jwz.org>
 *         Copyright (c) 2002 Aurelien Jacobs <aurel@gnuage.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.
 *
 * Generates a 3D Menger Sponge gasket:
 *
 *                                ___+._______
 *                           __-""   --     __"""----._____
 *                    __.--"" -- ___--+---_____.     __  .+'|
 *              _.-'""  __    +:"__   | ._..+"" __    .+'   F
 *            J"--.____         __ """""+"         .+'  .J  F
 *            J        """""---.___       --   .+'"     F'  F
 *             L                   """""--...+'    .J       F
 *             L   F"9      --.            |   .   F'      J
 *             L   -_J      L_J      F"9   | ;'J    .+J .J J
 *             |                     L_J   | F.'  .'| J F' J
 *             |        |"""--.__          | '   |""  J    J
 *             J   ._   J ;;; |  "L        |   . |-___J    |
 *             J   L J  J ;-' |   L        | .'J |_  .'  . |
 *             J    ""  J    .---_L  F"9   | F.' | .'   FJ |
 *              L       J .-'  __ |  L_J   | '   :'     ' .+
 *              L       '--.___   |        |       .J   .'
 *              |  F"9         """'        |   .   F' .'
 *              |  -_J      F"9            | .'J    .'
 *              +__         -_J      F"9   | F.'  .'
 *                 """--___          L_J   | '  .'
 *                         """---___       |  .'
 *                                  ""---._|.'
 *
 *  The straightforward way to generate this object creates way more polygons
 *  than are needed, since there end up being many buried, interior faces.
 *  So during the recursive building of the object we store which face of
 *  each unitary cube we need to draw. Doing this reduces the polygon count
 *  by 40% - 60%.
 *
 *  Another optimization we could do to reduce the polygon count would be to
 *  merge adjascent coplanar squares together into rectangles.  This would
 *  result in the outer faces being composed of 1xN strips.  It's tricky to
 *  to find these adjascent faces in non-exponential time, though.
 *
 *  We could actually simulate large depths with a texture map -- if the
 *  depth is such that the smallest holes are only a few pixels across,
 *  just draw them as spots on the surface!  It would look the same.
 */

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


# define release_sponge 0

#include "xlockmore.h"
#include "colors.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_SPEED       "150"
#define DEF_MAX_DEPTH   "3"

typedef struct {
  GLXContext *glx_context;
  rotator *rot;
  trackball_state *trackball;
  Bool button_down_p;
  GLuint sponge_list0;            /* we store X, Y, and Z-facing surfaces */
  GLuint sponge_list1;            /* in their own lists, to make it easy  */
  GLuint sponge_list2;            /* to color them differently.           */

  unsigned long squares_fp;

  int current_depth;

  int ncolors;
  XColor *colors;
  int ccolor0;
  int ccolor1;
  int ccolor2;

  int draw_tick;

} sponge_configuration;

static sponge_configuration *sps = NULL;

static Bool do_spin;
static Bool do_wander;
static int speed;
static int max_depth;

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

static argtype vars[] = {
  {&do_spin,     "spin",     "Spin",     DEF_SPIN,      t_Bool},
  {&do_wander,   "wander",   "Wander",   DEF_WANDER,    t_Bool},
  {&speed,       "speed",    "Speed",    DEF_SPEED,     t_Int},
  {&max_depth,   "maxDepth", "MaxDepth", DEF_MAX_DEPTH, t_Int},
};

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


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


#define X0 0x01
#define X1 0x02
#define Y0 0x04
#define Y1 0x08
#define Z0 0x10
#define Z1 0x20

static int
cube (float x0, float x1, float y0, float y1, float z0, float z1,
      int faces, int wireframe)
{
  int n = 0;

  if (faces & X0)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (-1.0, 0.0, 0.0);
      glVertex3f (x0, y1, z0);
      glVertex3f (x0, y0, z0);
      glVertex3f (x0, y0, z1);
      glVertex3f (x0, y1, z1);
      glEnd ();
      n++;
    }
  if (faces & X1)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (1.0, 0.0, 0.0);
      glVertex3f (x1, y1, z1);
      glVertex3f (x1, y0, z1);
      glVertex3f (x1, y0, z0);
      glVertex3f (x1, y1, z0);
      glEnd ();
      n++;
    }
  if (faces & Y0)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (0.0, -1.0, 0.0);
      glVertex3f (x0, y0, z0);
      glVertex3f (x0, y0, z1);
      glVertex3f (x1, y0, z1);
      glVertex3f (x1, y0, z0);
      glEnd ();
      n++;
    }
  if (faces & Y1)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (0.0, 1.0, 0.0);
      glVertex3f (x0, y1, z0);
      glVertex3f (x0, y1, z1);
      glVertex3f (x1, y1, z1);
      glVertex3f (x1, y1, z0);
      glEnd ();
      n++;
    }
  if (faces & Z0)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (0.0, 0.0, -1.0);
      glVertex3f (x1, y1, z0);
      glVertex3f (x1, y0, z0);
      glVertex3f (x0, y0, z0);
      glVertex3f (x0, y1, z0);
      glEnd ();
      n++;
    }
  if (faces & Z1)
    {
      glBegin (wireframe ? GL_LINE_LOOP : GL_POLYGON);
      glNormal3f (0.0, 0.0, 1.0);
      glVertex3f (x0, y1, z1);
      glVertex3f (x0, y0, z1);
      glVertex3f (x1, y0, z1);
      glVertex3f (x1, y1, z1);
      glEnd ();
      n++;
    }

  return n;
}

static int
menger_recurs_1 (int level, float x0, float x1, float y0, float y1,
                 float z0, float z1, int faces, Bool wireframe, 
                 int orig, int forig)
{
  float xi, yi, zi;
  int f, x, y, z;
  int n = 0;

  if (orig)
    {
      if (wireframe)
        n += cube (x0, x1, y0, y1, z0, z1,
                   faces & (X0 | X1 | Y0 | Y1), wireframe);
    }

  if (level == 0)
    {
      if (!wireframe)
        n += cube (x0, x1, y0, y1, z0, z1, faces, wireframe);
    }
  else
    {
      xi = (x1 - x0) / 3;
      yi = (y1 - y0) / 3;
      zi = (z1 - z0) / 3;

      for (x = 0; x < 3; x++)
        for (y = 0; y < 3; y++)
          for (z = 0; z < 3; z++)
            {
              if ((x != 1 && y != 1)
                  || (y != 1 && z != 1)
                  || (x != 1 && z != 1))
                {
                  f = faces;

                  if (x == 1 || (x == 2 && (y != 1 && z != 1)))
                    f &= ~X0;
                  if (x == 1 || (x == 0 && (y != 1 && z != 1)))
                    f &= ~X1;
                  if (forig & X0 && x == 2 && (y == 1 || z == 1))
                    f |= X0;
                  if (forig & X1 && x == 0 && (y == 1 || z == 1))
                    f |= X1;

                  if (y == 1 || (y == 2 && (x != 1 && z != 1)))
                    f &= ~Y0;
                  if (y == 1 || (y == 0 && (x != 1 && z != 1)))
                    f &= ~Y1;
                  if (forig & Y0 && y == 2 && (x == 1 || z == 1))
                    f |= Y0;
                  if (forig & Y1 && y == 0 && (x == 1 || z == 1))
                    f |= Y1;

                  if (z == 1 || (z == 2 && (x != 1 && y != 1)))
                    f &= ~Z0;
                  if (z == 1 || (z == 0 && (x != 1 && y != 1)))
                    f &= ~Z1;
                  if (forig & Z0 && z == 2 && (x == 1 || y == 1))
                    f |= Z0;
                  if (forig & Z1 && z == 0 && (x == 1 || y == 1))
                    f |= Z1;

                  n += menger_recurs_1 (level-1,
                                        x0+x*xi, x0+(x+1)*xi,
                                        y0+y*yi, y0+(y+1)*yi,
                                        z0+z*zi, z0+(z+1)*zi, f, wireframe, 0,
                                        forig);
                }
              else if (wireframe && (x != 1 || y != 1 || z != 1))
                n += cube (x0+x*xi, x0+(x+1)*xi,
                           y0+y*yi, y0+(y+1)*yi,
                           z0+z*zi, z0+(z+1)*zi,
                           forig & (X0 | X1 | Y0 | Y1), wireframe);
            }
    }

  return n;
}

static int
menger_recurs (int level, float x0, float x1, float y0, float y1,
               float z0, float z1, int faces, Bool wireframe, 
               int orig)
{
  return menger_recurs_1 (level, x0, x1, y0, y1, z0, z1, faces, 
                          wireframe, orig, faces);
}


static void
build_sponge (sponge_configuration *sp, Bool wireframe, int level)
{
  glDeleteLists (sp->sponge_list0, 1);
  glNewList(sp->sponge_list0, GL_COMPILE);
  sp->squares_fp = menger_recurs (level, -1.5, 1.5, -1.5, 1.5, -1.5, 1.5,
                                  X0 | X1, wireframe,1);
  glEndList();

  glDeleteLists (sp->sponge_list1, 1);
  glNewList(sp->sponge_list1, GL_COMPILE);
  sp->squares_fp += menger_recurs (level, -1.5, 1.5, -1.5, 1.5, -1.5, 1.5,
                                   Y0 | Y1, wireframe,1);
  glEndList();

  glDeleteLists (sp->sponge_list2, 1);
  glNewList(sp->sponge_list2, GL_COMPILE);
  sp->squares_fp += menger_recurs (level, -1.5, 1.5, -1.5, 1.5, -1.5, 1.5,
                                   Z0 | Z1, wireframe,1);
  glEndList();
}


ENTRYPOINT Bool
sponge_handle_event (ModeInfo *mi, XEvent *event)
{
  sponge_configuration *sp = &sps[MI_SCREEN(mi)];

  if (gltrackball_event_handler (event, sp->trackball,
                                 MI_WIDTH (mi), MI_HEIGHT (mi),
                                 &sp->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)
        {
          sp->draw_tick = speed;
          sp->current_depth += (sp->current_depth > 0 ? 1 : -1);
          sp->current_depth--;
          return True;
        }
      else if (c == '-' || c == '_' ||
               keysym == XK_Down || keysym == XK_Left || keysym == XK_Prior)
        {
          sp->draw_tick = speed;
          sp->current_depth -= (sp->current_depth > 0 ? 1 : -1);
          sp->current_depth--;
          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:
      sp->draw_tick = speed;
      return True;
    }

  return False;
}



ENTRYPOINT void 
init_sponge (ModeInfo *mi)
{
  sponge_configuration *sp;
  int wire = MI_IS_WIREFRAME(mi);

  MI_INIT (mi, sps);

  sp = &sps[MI_SCREEN(mi)];

  if ((sp->glx_context = init_GL(mi)) != NULL) {
    reshape_sponge (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
  }

  if (!wire)
    {
      static const GLfloat pos0[4] = {-1.0, -1.0, 1.0, 0.1};
      static const GLfloat pos1[4] = { 1.0, -0.2, 0.2, 0.1};
      static const GLfloat dif0[4] = {1.0, 1.0, 1.0, 1.0};
      static const GLfloat dif1[4] = {1.0, 1.0, 1.0, 1.0};

      glLightfv(GL_LIGHT0, GL_POSITION, pos0);
      glLightfv(GL_LIGHT1, GL_POSITION, pos1);
      glLightfv(GL_LIGHT0, GL_DIFFUSE, dif0);
      glLightfv(GL_LIGHT1, GL_DIFFUSE, dif1);

      glEnable(GL_LIGHTING);
      glEnable(GL_LIGHT0);
      glEnable(GL_LIGHT1);

      glEnable(GL_DEPTH_TEST);
      glEnable(GL_NORMALIZE);

      glShadeModel(GL_SMOOTH);
    }

  {
    double spin_speed   = 1.0;
    double wander_speed = 0.03;
    sp->rot = make_rotator (do_spin ? spin_speed : 0,
                            do_spin ? spin_speed : 0,
                            do_spin ? spin_speed : 0,
                            1.0,
                            do_wander ? wander_speed : 0,
                            True);
    sp->trackball = gltrackball_init (True);
  }

  sp->ncolors = 128;
  sp->colors = (XColor *) calloc(sp->ncolors, sizeof(XColor));
  make_smooth_colormap (0, 0, 0,
                        sp->colors, &sp->ncolors,
                        False, 0, False);
  sp->ccolor0 = 0;
  sp->ccolor1 = sp->ncolors / 3;
  sp->ccolor2 = sp->ccolor1 * 2;

  sp->sponge_list0 = glGenLists (1);
  sp->sponge_list1 = glGenLists (1);
  sp->sponge_list2 = glGenLists (1);

  sp->draw_tick = 9999999;
}


ENTRYPOINT void
draw_sponge (ModeInfo *mi)
{
  sponge_configuration *sp = &sps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);

  GLfloat color0[4] = {0.0, 0.0, 0.0, 1.0};
  GLfloat color1[4] = {0.0, 0.0, 0.0, 1.0};
  GLfloat color2[4] = {0.0, 0.0, 0.0, 1.0};

  if (!sp->glx_context)
    return;

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

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();

  glScalef(1.1, 1.1, 1.1);

  {
    double x, y, z;
    get_position (sp->rot, &x, &y, &z, !sp->button_down_p);
    glTranslatef((x - 0.5) * 8,
                 (y - 0.5) * 6,
                 (z - 0.5) * 15);

    gltrackball_rotate (sp->trackball);

    get_rotation (sp->rot, &x, &y, &z, !sp->button_down_p);
    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);
  }

  color0[0] = sp->colors[sp->ccolor0].red   / 65536.0;
  color0[1] = sp->colors[sp->ccolor0].green / 65536.0;
  color0[2] = sp->colors[sp->ccolor0].blue  / 65536.0;

  color1[0] = sp->colors[sp->ccolor1].red   / 65536.0;
  color1[1] = sp->colors[sp->ccolor1].green / 65536.0;
  color1[2] = sp->colors[sp->ccolor1].blue  / 65536.0;

  color2[0] = sp->colors[sp->ccolor2].red   / 65536.0;
  color2[1] = sp->colors[sp->ccolor2].green / 65536.0;
  color2[2] = sp->colors[sp->ccolor2].blue  / 65536.0;


  sp->ccolor0++;
  sp->ccolor1++;
  sp->ccolor2++;
  if (sp->ccolor0 >= sp->ncolors) sp->ccolor0 = 0;
  if (sp->ccolor1 >= sp->ncolors) sp->ccolor1 = 0;
  if (sp->ccolor2 >= sp->ncolors) sp->ccolor2 = 0;

  if (sp->draw_tick++ >= speed)
    {
      sp->draw_tick = 0;
      if (sp->current_depth >= max_depth)
        sp->current_depth = -max_depth;
      sp->current_depth++;
      build_sponge (sp,
                    MI_IS_WIREFRAME(mi),
                    (sp->current_depth < 0
                     ? -sp->current_depth : sp->current_depth));

      mi->polygon_count = sp->squares_fp;  /* for FPS display */
      mi->recursion_depth = (sp->current_depth < 0
                             ? -sp->current_depth : sp->current_depth);
    }

  glScalef (2.0, 2.0, 2.0);

  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color0);
  glCallList (sp->sponge_list0);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color1);
  glCallList (sp->sponge_list1);
  glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, color2);
  glCallList (sp->sponge_list2);

  glPopMatrix ();

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

  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_sponge (ModeInfo *mi)
{
  sponge_configuration *sp = &sps[MI_SCREEN(mi)];
  if (!sp->glx_context) return;
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *sp->glx_context);
  if (sp->colors) free (sp->colors);
  if (sp->rot) free_rotator (sp->rot);
  if (sp->trackball) gltrackball_free (sp->trackball);
  if (glIsList(sp->sponge_list0)) glDeleteLists(sp->sponge_list0, 1);
  if (glIsList(sp->sponge_list1)) glDeleteLists(sp->sponge_list1, 1);
  if (glIsList(sp->sponge_list2)) glDeleteLists(sp->sponge_list2, 1);
}

XSCREENSAVER_MODULE_2 ("Menger", menger, sponge)

#endif /* USE_GL */
