/* cubestack, Copyright (c) 2016 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" \
			"*showFPS:      False       \n" \
			"*wireframe:    False       \n" \
			"*suppressRotationAnimation: True\n" \

# define release_cube 0

#include "xlockmore.h"
#include "colors.h"
#include "rotator.h"
#include "gltrackball.h"
#include <ctype.h>

#ifdef USE_GL /* whole file */


#define DEF_WANDER    "True"
#define DEF_SPEED     "1.0"
#define DEF_THICKNESS "0.13"
#define DEF_OPACITY   "0.7"

typedef struct {
  GLXContext *glx_context;
  rotator *rot;
  trackball_state *trackball;
  Bool button_down_p;
  GLfloat state;
  GLfloat r;
  int length;
  int ncolors;
  XColor *colors;
  int ccolor;
} cube_configuration;

static cube_configuration *bps = NULL;

static GLfloat speed;
static GLfloat thickness;
static GLfloat opacity;
static Bool do_wander;

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

static argtype vars[] = {
  {&do_wander, "wander", "Wander", DEF_WANDER, t_Bool},
  {&speed,     "speed",  "Speed",  DEF_SPEED,  t_Float},
  {&thickness, "thickness", "Thickness", DEF_THICKNESS, t_Float},
  {&opacity, "opacity", "Opacity", DEF_OPACITY, t_Float},
};

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


static int
draw_strut (ModeInfo *mi)
{
  int wire = MI_IS_WIREFRAME(mi);
  int polys = 0;
  GLfloat h;

  glPushMatrix();
  glFrontFace (GL_CCW);
  glNormal3f (0, 0, -1);
  glTranslatef (-0.5, -0.5, 0);
  glBegin (wire ? GL_LINE_LOOP : GL_TRIANGLE_FAN);
  glVertex3f (0, 0, 0);
  glVertex3f (1, 0, 0);
  glVertex3f (1 - thickness, thickness, 0);
  glVertex3f (thickness, thickness, 0);
  glEnd();
  polys += 2;

  h = 0.5 - thickness;
  if (h >= 0.25)
    {
      glBegin (wire ? GL_LINE_LOOP : GL_TRIANGLE_FAN);
      glVertex3f (0.5, 0.5, 0);
      glVertex3f (0.5 - thickness/2, 0.5 - thickness/2, 0);
      glVertex3f (0.5 - thickness/2, 0.5 - h/2, 0);
      glVertex3f (0.5 + thickness/2, 0.5 - h/2, 0);
      glVertex3f (0.5 + thickness/2, 0.5 - thickness/2, 0);
      glEnd();
      polys += 3;
    }

  glPopMatrix();

  return polys;
}


static int
draw_face (ModeInfo *mi)
{
  int i;
  int polys = 0;
  for (i = 0; i < 4; i++)
    {
      polys += draw_strut (mi);
      glRotatef (90, 0, 0, 1);
    }
  return polys;
}


static GLfloat
ease_fn (GLfloat r)
{
  return cos ((r/2 + 1) * M_PI) + 1; /* Smooth curve up, end at slope 1. */
}


static GLfloat
ease_ratio (GLfloat r)
{
  GLfloat ease = 0.5;
  if      (r <= 0)     return 0;
  else if (r >= 1)     return 1;
  else if (r <= ease)  return     ease * ease_fn (r / ease);
  else if (r > 1-ease) return 1 - ease * ease_fn ((1 - r) / ease);
  else                 return r;
}


static int
draw_cube_1 (ModeInfo *mi, GLfloat state, GLfloat color[4], Bool bottom_p)
{
  int polys = 0;
  int istate = state;
  GLfloat r = state - istate;
  GLfloat a = color[3];

  r = ease_ratio (r);

# define COLORIZE(R) \
      color[3] = a * R; \
      glMaterialfv (GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, color); \
      glColor4fv (color)

  if (bottom_p)
    {
      GLfloat r2 = (state < 0 ? 1 + state : 1);
      COLORIZE(r2);
      polys += draw_face (mi);		/* Bottom */
    }

  if (state >= 0)			/* Left */
    {
      GLfloat r2 = (istate == 0 ? r : 1);
      COLORIZE(r2);
      glPushMatrix();
      glTranslatef (-0.5, 0.5, 0);
      glRotatef (-r2 * 90, 0, 1, 0);
      glTranslatef (0.5, -0.5, 0);
      polys += draw_face (mi);
      glPopMatrix();
    }

  if (state >= 1)			/* Back */
    {
      GLfloat r2 = (istate == 1 ? r : 1);
      COLORIZE(r2);
      glPushMatrix();
      glTranslatef (-0.5, 0.5, 0);
      glRotatef ( 90, 0, 1, 0);
      glRotatef (-90, 0, 0, 1);
      glRotatef (-r2 * 90, 0, 1, 0);
      glTranslatef (0.5, -0.5, 0);
      polys += draw_face (mi);
      glPopMatrix();
    }

  if (state >= 2)			/* Right */
    {
      GLfloat r2 = (istate == 2 ? r : 1);
      COLORIZE(r2);
      glPushMatrix();
      glTranslatef (0.5, 0.5, 0);
      glRotatef ( 90, 0, 1, 0);
      glRotatef (-90, 0, 0, 1);
      glRotatef (-90, 0, 1, 0);
      glRotatef (-r2 * 90, 0, 1, 0);
      glTranslatef (-0.5, -0.5, 0);
      polys += draw_face (mi);
      glPopMatrix();
    }

  if (state >= 3)			/* Front */
    {
      GLfloat r2 = (istate == 3 ? r : 1);
      COLORIZE(r2);
      glPushMatrix();
      glTranslatef (0.5, 0.5, 0);
      glRotatef ( 90, 0, 1, 0);
      glRotatef (-90, 0, 0, 1);
      glRotatef (-180, 0, 1, 0);
      glTranslatef (-1, 0, 0);
      glRotatef (-r2 * 90, 0, 1, 0);
      glTranslatef (0.5, -0.5, 0);
      polys += draw_face (mi);
      glPopMatrix();
    }

  if (state >= 4)			/* Top */
    {
      GLfloat r2 = (istate == 4 ? r : 1);
      COLORIZE(r2);
      glPushMatrix();
      glTranslatef (0, 0, 1);
      glRotatef (-90, 0, 0, 1);
      glTranslatef (0.5, 0.5, 0);
      glRotatef (-90, 0, 1, 0);
      glRotatef (r2 * 90, 0, 1, 0);
      glTranslatef (-0.5, -0.5, 0);
      polys += draw_face (mi);
      glPopMatrix();
    }

  return polys;
}


static int
draw_cubes (ModeInfo *mi)
{
  cube_configuration *bp = &bps[MI_SCREEN(mi)];
  int polys = 0;
  GLfloat z = bp->state / 6;
  int i;
  GLfloat c[4];
  int c0 = bp->ccolor;
  GLfloat alpha = opacity;

  glPushMatrix();
  glTranslatef (0, 0, -1.5 - z);

  glTranslatef (0, 0, -bp->length);
  for (i = bp->length-1; i >= 0; i--)
    {
      int c1 = c0 - i - 1;
      if (c1 < 0) c1 += bp->ncolors;
      c[0] = bp->colors[c1].red   / 65536.0;
      c[1] = bp->colors[c1].green / 65536.0;
      c[2] = bp->colors[c1].blue  / 65536.0;
      c[3] = alpha;

      glTranslatef (0, 0, 1);
      polys += draw_cube_1 (mi, 5, c, i == bp->length - 1);
    }

  c[0] = bp->colors[c0].red   / 65536.0;
  c[1] = bp->colors[c0].green / 65536.0;
  c[2] = bp->colors[c0].blue  / 65536.0;
  c[3] = alpha;
  glTranslatef (0, 0, 1);
  polys += draw_cube_1 (mi, bp->state, c, bp->length == 0);

  glPopMatrix();

  return polys;
}


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


ENTRYPOINT Bool
cube_handle_event (ModeInfo *mi, XEvent *event)
{
  cube_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 == '\t')
        {
          bp->ncolors = 32;
          make_smooth_colormap (0, 0, 0,
                                bp->colors, &bp->ncolors,
                                False, 0, False);
          return True;
        }
    }


  return False;
}


ENTRYPOINT void 
init_cube (ModeInfo *mi)
{
  cube_configuration *bp;
  int wire = MI_IS_WIREFRAME(mi);

  MI_INIT (mi, bps);

  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  reshape_cube (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  if (!wire)
    {
      glDisable (GL_LIGHTING);
      glEnable(GL_DEPTH_TEST);
      glShadeModel (GL_SMOOTH);
      glEnable (GL_NORMALIZE);
      glDisable (GL_CULL_FACE);
      glEnable (GL_BLEND);
      glDisable (GL_DEPTH_TEST);
      glBlendFunc (GL_SRC_ALPHA, GL_ONE);
    }

  {
    double wander_speed = 0.005;
    bp->rot = make_rotator (0, 0, 0, 0,
                            do_wander ? wander_speed : 0,
                            False);
    bp->trackball = gltrackball_init (True);
  }

  if (thickness > 0.5)
    thickness = 0.5;
  if (thickness < 0.001)
    thickness = 0.001;

  bp->ncolors = 32;
  bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor));
  make_smooth_colormap (0, 0, 0,
                        bp->colors, &bp->ncolors,
                        False, 0, False);
  bp->state = -1;
}


ENTRYPOINT void
draw_cube (ModeInfo *mi)
{
  cube_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);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();

  {
    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) * 2);

    gltrackball_rotate (bp->trackball);
  }

  mi->polygon_count = 0;

  glScalef (6, 6, 6);
  glRotatef (-45, 1, 0, 0);
  glRotatef (20, 0, 0, 1);
  glRotatef (bp->r, 0, 0, 1);

  mi->polygon_count = draw_cubes (mi);
  glPopMatrix ();

  if (!bp->button_down_p)
    {
      int max = 6;
      bp->state += speed * 0.015;
      bp->r += speed * 0.05;
      while (bp->r > 360)
        bp->r -= 360;
      while (bp->state > max)
        {
          bp->state -= max;
          bp->length++;
          bp->ccolor++;
          if (bp->ccolor > bp->ncolors)
            bp->ccolor = 0;
        }

      if (bp->length > 20)
        bp->length = 20;
    }

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

  glXSwapBuffers(dpy, window);
}


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

XSCREENSAVER_MODULE_2 ("CubeStack", cubestack, cube)

#endif /* USE_GL */
