/* quasicrystal, Copyright (c) 2013 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.
 *
 * Overlapping sine waves create interesting plane-tiling interference
 * patterns.  Created by jwz, Jul 2013.  Inspired by
 * http://mainisusuallyafunction.blogspot.com/2011/10/quasicrystals-as-sums-of-waves-in-plane.html
 */


#define DEFAULTS	"*delay:	30000       \n" \
			"*spin:         True        \n" \
			"*wander:       True        \n" \
			"*symmetric:    True        \n" \
			"*count:        17          \n" \
			"*contrast:     30          \n" \
			"*showFPS:      False       \n" \
			"*wireframe:    False       \n" \
			"*suppressRotationAnimation: True\n" \

# define release_quasicrystal 0

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

#ifdef USE_GL /* whole file */

#define DEF_SPEED  "1.0"

typedef struct {
  rotator *rot, *rot2;
  GLuint texid;
} plane;

typedef struct {
  GLXContext *glx_context;
  Bool button_down_p;
  Bool symmetric_p;
  GLfloat contrast;
  int count;
  int ncolors, ccolor;
  XColor *colors;
  plane *planes;
  int mousex, mousey;

} quasicrystal_configuration;

static quasicrystal_configuration *bps = NULL;

static GLfloat speed;

static XrmOptionDescRec opts[] = {
  { "-spin",         ".spin",      XrmoptionNoArg, "True"  },
  { "+spin",         ".spin",      XrmoptionNoArg, "False" },
  { "-wander",       ".wander",    XrmoptionNoArg, "True"  },
  { "+wander",       ".wander",    XrmoptionNoArg, "False" },
  { "-symmetry",     ".symmetric", XrmoptionNoArg, "True"   },
  { "-symmetric",    ".symmetric", XrmoptionNoArg, "True"   },
  { "-no-symmetry",  ".symmetric", XrmoptionNoArg, "False"  },
  { "-nonsymmetric", ".symmetric", XrmoptionNoArg, "False"  },
  { "-speed",        ".speed",     XrmoptionSepArg, 0 },
  { "-contrast",     ".contrast",  XrmoptionSepArg, 0 },
};

static argtype vars[] = {
  {&speed,     "speed",  "Speed",  DEF_SPEED,  t_Float},
};

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



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

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

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho (0, 1, 1, 0, -1, 1);

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  glTranslatef (0.5, 0.5, 0);
  glScalef (h, 1, 1);
  if (width > height)
    glScalef (1/h, 1/h, 1);
  glTranslatef (-0.5, -0.5, 0);
  glClear(GL_COLOR_BUFFER_BIT);
}


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

  if (event->xany.type == ButtonPress &&
      event->xbutton.button == Button1)
    {
      bp->button_down_p = True;
      bp->mousex = event->xbutton.x;
      bp->mousey = event->xbutton.y;
      return True;
    }
  else if (event->xany.type == ButtonRelease &&
           event->xbutton.button == Button1)
    {
      bp->button_down_p = False;
      return True;
    }
  else if (event->xany.type == ButtonPress &&	/* wheel up or right */
           (event->xbutton.button == Button4 ||
            event->xbutton.button == Button7))
    {
    UP:
      if (bp->contrast <= 0) return False;
      bp->contrast--;
      return True;
    }
  else if (event->xany.type == ButtonPress &&	/* wheel down or left */
           (event->xbutton.button == Button5 ||
            event->xbutton.button == Button6))
    {
    DOWN:
      if (bp->contrast >= 100) return False;
      bp->contrast++;
      return True;
    }
  else if (event->xany.type == MotionNotify &&
           bp->button_down_p)
    {
      /* Dragging up and down tweaks contrast */

      int dx = event->xmotion.x - bp->mousex;
      int dy = event->xmotion.y - bp->mousey;

      if (abs(dy) > abs(dx))
        {
          bp->contrast += dy / 40.0;
          if (bp->contrast < 0)   bp->contrast = 0;
          if (bp->contrast > 100) bp->contrast = 100;
        }

      bp->mousex = event->xmotion.x;
      bp->mousey = event->xmotion.y;
      return True;
    }
  else
    {
      if (event->xany.type == KeyPress)
        {
          KeySym keysym;
          char c = 0;
          XLookupString (&event->xkey, &c, 1, &keysym, 0);
          if (c == '<' || c == ',' || c == '-' || c == '_' ||
              keysym == XK_Left || keysym == XK_Up || keysym == XK_Prior)
            goto UP;
          else if (c == '>' || c == '.' || c == '=' || c == '+' ||
                   keysym == XK_Right || keysym == XK_Down ||
                   keysym == XK_Next)
            goto DOWN;
        }

      return screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event);
    }

  return False;
}



ENTRYPOINT void 
init_quasicrystal (ModeInfo *mi)
{
  quasicrystal_configuration *bp;
  int wire = MI_IS_WIREFRAME(mi);
  unsigned char *tex_data = 0;
  int tex_width;
  int i;

  MI_INIT (mi, bps);

  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  reshape_quasicrystal (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  glDisable (GL_DEPTH_TEST);
  glEnable (GL_CULL_FACE);

  bp->count = MI_COUNT(mi);
  if (bp->count < 1) bp->count = 1;

  if (! wire)
    {
      unsigned char *o;

      tex_width = 4096;
      glGetIntegerv (GL_MAX_TEXTURE_SIZE, &tex_width);
      if (tex_width > 4096) tex_width = 4096;

      tex_data = (unsigned char *) calloc (4, tex_width);
      o = tex_data;
      for (i = 0; i < tex_width; i++)
        {
          unsigned char y = 255 * (1 + sin (i * M_PI * 2 / tex_width)) / 2;
          *o++ = y;
          *o++ = y;
          *o++ = y;
          *o++ = 255;
        }
    }

  bp->symmetric_p =
    get_boolean_resource (MI_DISPLAY (mi), "symmetric", "Symmetric");

  bp->contrast = get_float_resource (MI_DISPLAY (mi), "contrast", "Contrast");
  if (bp->contrast < 0 || bp->contrast > 100) 
    {
      fprintf (stderr, "%s: contrast must be between 0 and 100%%.\n", progname);
      bp->contrast = 0;
    }

  {
    Bool spinp   = get_boolean_resource (MI_DISPLAY (mi), "spin", "Spin");
    Bool wanderp = get_boolean_resource (MI_DISPLAY (mi), "wander", "Wander");
    double spin_speed   = 0.01;
    double wander_speed = 0.0001;
    double spin_accel   = 10.0;
    double scale_speed  = 0.005;

    bp->planes = (plane *) calloc (sizeof (*bp->planes), bp->count);

    bp->ncolors = 256;  /* ncolors affects color-cycling speed */
    bp->colors = (XColor *) calloc (bp->ncolors, sizeof(XColor));
    make_smooth_colormap (0, 0, 0, bp->colors, &bp->ncolors,
                          False, 0, False);
    bp->ccolor = 0;

    for (i = 0;  i < bp->count; i++)
      {
        plane *p = &bp->planes[i];
        p->rot = make_rotator (0, 0,
                               spinp ? spin_speed : 0,
                               spin_accel,
                               wanderp ? wander_speed : 0,
                               True);
        p->rot2 = make_rotator (0, 0,
                                0, 0,
                               scale_speed,
                               True);
        if (! wire)
          {
            clear_gl_error();

            glGenTextures (1, &p->texid);
            glBindTexture (GL_TEXTURE_1D, p->texid);
            glPixelStorei (GL_UNPACK_ALIGNMENT, 1);
            glTexImage1D (GL_TEXTURE_1D, 0, GL_RGBA,
                          tex_width, 0,
                          GL_RGBA, GL_UNSIGNED_BYTE, tex_data);
            check_gl_error("texture");

            glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_REPEAT);
            glTexParameterf(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_REPEAT);

            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

            glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
          }
      }
  }

  if (tex_data) free (tex_data);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}


ENTRYPOINT void
draw_quasicrystal (ModeInfo *mi)
{
  quasicrystal_configuration *bp = &bps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  int wire = MI_IS_WIREFRAME(mi);
  double r=0, ps=0;
  int i;

  if (!bp->glx_context)
    return;

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

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  mi->polygon_count = 0;

  glShadeModel(GL_FLAT);
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);
  glDisable (GL_LIGHTING);
  if (!wire)
    {
      glEnable (GL_TEXTURE_1D);
# ifdef HAVE_JWZGLES
      glEnable (GL_TEXTURE_2D);  /* jwzgles needs this, bleh. */
# endif
    }

  glEnable (GL_BLEND);
  glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glPushMatrix ();
  glTranslatef (0.5, 0.5, 0);
  glScalef (3, 3, 3);

  if (wire) glScalef (0.2, 0.2, 0.2);

  for (i = 0;  i < bp->count; i++)
    {
      plane *p = &bp->planes[i];
      double x, y, z;
      double scale = (wire ? 10 : 700.0 / bp->count);
      double pscale;

      glPushMatrix();

      get_position (p->rot, &x, &y, &z, !bp->button_down_p);
      glTranslatef((x - 0.5) * 0.3333,
                   (y - 0.5) * 0.3333,
                   0);

      /* With -symmetry, keep the planes' scales in sync.
         Otherwise, they scale independently.
       */
      if (bp->symmetric_p && i > 0)
        pscale = ps;
      else
        {
          get_position (p->rot2, &x, &y, &z, !bp->button_down_p);
          pscale = 1 + (4 * z);
          ps = pscale;
        }

      scale *= pscale;


      /* With -symmetry, evenly distribute the planes' rotation.
         Otherwise, they rotate independently.
       */
      if (bp->symmetric_p && i > 0)
        z = r + (i * M_PI * 2 / bp->count);
      else
        {
          get_rotation (p->rot, &x, &y, &z, !bp->button_down_p);
          r = z;
        }


      glRotatef (z * 360, 0, 0, 1);
      glTranslatef (-0.5, -0.5, 0);

      glColor4f (1, 1, 1, (wire ? 0.5 : 1.0 / bp->count));

      if (!wire)
        glBindTexture (GL_TEXTURE_1D, p->texid);

      glBegin (wire ? GL_LINE_LOOP : GL_QUADS);
      glNormal3f (0, 0, 1);
      glTexCoord2f (-scale/2,  scale/2); glVertex3f (0, 1, 0);
      glTexCoord2f ( scale/2,  scale/2); glVertex3f (1, 1, 0);
      glTexCoord2f ( scale/2, -scale/2); glVertex3f (1, 0, 0);
      glTexCoord2f (-scale/2, -scale/2); glVertex3f (0, 0, 0);
      glEnd();

      if (wire)
        {
          float j;
          glDisable (GL_TEXTURE_1D);
          glColor4f (1, 1, 1, 1.0 / bp->count);
          for (j = 0; j < 1; j += (1 / scale))
            {
              glBegin (GL_LINES);
              glVertex3f (j, 0, 0);
              glVertex3f (j, 1, 0);
              mi->polygon_count++;
              glEnd();
            }
        }

      glPopMatrix();

      mi->polygon_count++;
    }

  /* Colorize the grayscale image. */
  {
    GLfloat c[4];
    c[0] = bp->colors[bp->ccolor].red   / 65536.0;
    c[1] = bp->colors[bp->ccolor].green / 65536.0;
    c[2] = bp->colors[bp->ccolor].blue  / 65536.0;
    c[3] = 1;

    /* Brighten the colors. */
    c[0] = (0.6666 + c[0]/3);
    c[1] = (0.6666 + c[1]/3);
    c[2] = (0.6666 + c[2]/3);

    glBlendFunc (GL_DST_COLOR, GL_SRC_COLOR);
    glDisable (GL_TEXTURE_1D);
    glColor4fv (c);
    glTranslatef (-0.5, -0.5, 0);
    glBegin (GL_QUADS);
    glVertex3f (0, 1, 0);
    glVertex3f (1, 1, 0);
    glVertex3f (1, 0, 0);
    glVertex3f (0, 0, 0);
    glEnd();
    mi->polygon_count++;
  }

  /* Clip the colors to simulate contrast. */

  if (bp->contrast > 0)
    {
      /* If c > 0, map 0 - 100 to 0.5 - 1.0, and use (s & ~d) */
      GLfloat c = 1 - (bp->contrast / 2 / 100.0);
      glDisable (GL_TEXTURE_1D);
      glDisable (GL_BLEND);
      glEnable (GL_COLOR_LOGIC_OP);
      glLogicOp (GL_AND_REVERSE);
      glColor4f (c, c, c, 1);
      glBegin (GL_QUADS);
      glVertex3f (0, 1, 0);
      glVertex3f (1, 1, 0);
      glVertex3f (1, 0, 0);
      glVertex3f (0, 0, 0);
      glEnd();
      mi->polygon_count++;
      glDisable (GL_COLOR_LOGIC_OP);
    }

  /* Rotate colors. */
  bp->ccolor++;
  if (bp->ccolor >= bp->ncolors)
    bp->ccolor = 0;


  glPopMatrix ();

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

  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_quasicrystal (ModeInfo *mi)
{
  quasicrystal_configuration *bp = &bps[MI_SCREEN(mi)];
  int i;
  if (!bp->glx_context) return;
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
  for (i = 0; i < bp->count; i++) {
    free_rotator (bp->planes[i].rot);
    free_rotator (bp->planes[i].rot2);
    if (bp->planes[i].texid) glDeleteTextures (1, &bp->planes[i].texid);
  }    
  if (bp->planes) free (bp->planes);
  if (bp->colors) free (bp->colors);

}

XSCREENSAVER_MODULE ("QuasiCrystal", quasicrystal)

#endif /* USE_GL */
