/* razzledazzle, Copyright (c) 2018-2020 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" \
			"*ncolors:      2           \n" \
			"*suppressRotationAnimation: True\n" \

# define release_dazzle 0

#include "xlockmore.h"
#include "colors.h"
#include "normals.h"
#include "gllist.h"
#include <ctype.h>

#ifdef USE_GL /* whole file */

#define DEF_SPEED       "1.0"
#define DEF_DENSITY     "5.0"
#define DEF_THICKNESS   "0.1"
#define DEF_MODE        "Random"

#define BELLRAND(n) ((frand((n)) + frand((n)) + frand((n))) / 3)
#undef RANDSIGN
#define RANDSIGN() ((random() & 1) ? 1 : -1)

extern const struct gllist
  *ships_ship1, *ships_ship2, *ships_ship3, *ships_ship4,
  *ships_ship5, *ships_ship6, *ships_ship7, *ships_ship8;

static const struct gllist * const *all_ships[] = {
  &ships_ship1, &ships_ship2, &ships_ship3, &ships_ship4,
  &ships_ship5, &ships_ship6, &ships_ship7, &ships_ship8,
};


typedef enum { LEFT, RIGHT, UP, DOWN } direction;

typedef struct node node;

struct node {
  long gx, gy;
  GLfloat x, y;
  GLfloat dx, dy;
  int nstripes;
  Bool horiz_p;
  Bool drawn_p;
  GLfloat color1[4], color2[4];
};

typedef struct {
  GLXContext *glx_context;
  Bool button_down_p;
  GLfloat xoff, yoff, dx, dy;
  node *nodes;
  node *dragging;
  int drag_x, drag_y;
  int ncolors;
  XColor *colors;
  GLuint *dlists;
  enum { SHIPS, FLAT, RANDOM } mode;
  int which_ship;
  long frames;
} dazzle_configuration;

static dazzle_configuration *bps = NULL;

static GLfloat speed, thickness, density;
static char *mode_arg;

static XrmOptionDescRec opts[] = {
  { "-speed",     ".speed",     XrmoptionSepArg, 0 },
  { "-thickness", ".thickness", XrmoptionSepArg, 0 },
  { "-density",   ".density",   XrmoptionSepArg, 0 },
  { "-mode",      ".mode",      XrmoptionSepArg,  0  },
  { "-ships",     ".mode",      XrmoptionNoArg,  "ships"  },
  { "-flat",      ".mode",      XrmoptionNoArg,  "flat"   },
};

static argtype vars[] = {
  {&speed,     "speed",     "Speed",     DEF_SPEED,     t_Float},
  {&thickness, "thickness", "Thickness", DEF_THICKNESS, t_Float},
  {&density,   "density",   "Density",   DEF_DENSITY,   t_Float},
  {&mode_arg,  "mode",      "Mode",      DEF_MODE,      t_String},
};

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



static void
draw_grid (ModeInfo *mi, int gx, int gy)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];
  Bool wire = MI_IS_WIREFRAME(mi);
  long x, y;
  long wh = density * 2;

  if (wire)
    glColor3f (1, 1, 1);

  if (!wire)
    glBegin (GL_QUADS);

  for (y = 0; y < wh; y++)
    for (x = 0; x < wh; x++)
      {
        node *n0 = &bp->nodes[(y     % wh) * wh + (x     % wh)];
        node *n1 = &bp->nodes[(y     % wh) * wh + ((x+1) % wh)];
        node *n2 = &bp->nodes[((y+1) % wh) * wh + ((x+1) % wh)];
        node *n3 = &bp->nodes[((y+1) % wh) * wh + (x     % wh)];
        int nstripes, i;
        Bool horiz_p, visible;
        GLfloat x0, y0, x1, y1, x2, y2, x3, y3;
        GLfloat xoff = (x < wh-1 ? 0 : wh);
        GLfloat yoff = (y < wh-1 ? 0 : wh);
        GLfloat bx = fmod ((double) bp->xoff, 2.0);
        GLfloat by = fmod ((double) bp->yoff, 2.0);

        bx += gx*2;
        by += gy*2;

        if (wire)
          {
            GLfloat a;
            glColor3f (0, 0, 1);
            glBegin (GL_LINE_LOOP);
            for (a = 0; a < 360; a += 10)
              glVertex3f ((n0->x / density-1) + 0.05 * cos(a * M_PI/180) + bx,
                          (n0->y / density-1) + 0.05 * sin(a * M_PI/180) + by,
                          0);
            glEnd();
          }

        x0 = n0->x        / density - 1 + bx;
        y0 = n0->y        / density - 1 + by;

        x1 = (n1->x+xoff) / density - 1 + bx;
        y1 = n1->y        / density - 1 + by;
        x2 = (n2->x+xoff) / density - 1 + bx;
        y2 = (n2->y+yoff) / density - 1 + by;
        x3 = n3->x        / density - 1 + bx;
        y3 = (n3->y+yoff) / density - 1 + by;

        if (wire)
          {
            if (gx == 0 && gy == 0)
              {
                glLineWidth (4);
                glColor3f(1, 0, 0);
              }
            else
              glColor3f(0.5, 0, 0.5);
            if (wire) glBegin (GL_LINE_LOOP);
            glVertex3f (x0, y0, 0);
            glVertex3f (x1, y1, 0);
            glVertex3f (x2, y2, 0);
            glVertex3f (x3, y3, 0);
            mi->polygon_count++;
            if (wire) glEnd();
            glLineWidth (1);
          }

        /* This isn't quite right: just because all corners are off screen
           doesn't mean the quad isn't visible. We need to intersect the
           edges with the screen rectangle.
         */
        {
          GLfloat max = 0.75;
          visible = ((x0 >= -max && y0 >= -max && x0 <= max && y0  <= max) ||
                     (x1 >= -max && y1 >= -max && x1 <= max && y1  <= max) ||
                     (x2 >= -max && y2 >= -max && x2 <= max && y2  <= max) ||
                     (x3 >= -max && y3 >= -max && x3 <= max && y3  <= max));
        }

        if (!visible) continue;

        if (visible)
          n0->drawn_p = True;

        nstripes = n0->nstripes;
        horiz_p  = n0->horiz_p;

        for (i = 0; i < nstripes; i++)
          {
            GLfloat ss  = (GLfloat) i     / nstripes;
            GLfloat ss1 = (GLfloat) (i+1) / nstripes;
            if (i & 1)
              glColor4fv (n0->color1);
            else if (wire)
              continue;
            else
              glColor4fv (n0->color2);

            if (horiz_p)
              {
                x0 = n0->x        + (n3->x        - n0->x)        * ss;
                y0 = n0->y        + ((n3->y+yoff) - n0->y)        * ss;
                x1 = (n1->x+xoff) + ((n2->x+xoff) - (n1->x+xoff)) * ss;
                y1 = n1->y        + ((n2->y+yoff) - n1->y)        * ss;

                x2 = (n1->x+xoff) + ((n2->x+xoff) - (n1->x+xoff)) * ss1;
                y2 = n1->y        + ((n2->y+yoff) - n1->y)        * ss1;
                x3 = n0->x        + (n3->x        - n0->x)        * ss1;
                y3 = n0->y        + ((n3->y+yoff) - n0->y)        * ss1;
              }
            else
              {
                x0 = n0->x        + ((n1->x+xoff) - n0->x)        * ss;
                y0 = n0->y        + (n1->y        - n0->y)        * ss;
                x1 = n3->x        + ((n2->x+xoff) - n3->x)        * ss;
                y1 = (n3->y+yoff) + ((n2->y+yoff) - (n3->y+yoff)) * ss;

                x2 = n3->x        + ((n2->x+xoff) - n3->x)        * ss1;
                y2 = (n3->y+yoff) + ((n2->y+yoff) - (n3->y+yoff)) * ss1;
                x3 = n0->x        + ((n1->x+xoff) - n0->x)        * ss1;
                y3 = n0->y        + (n1->y        - n0->y)        * ss1;
              }

            if (wire) glBegin (GL_LINES);
            glVertex3f (x0 / density - 1 + bx, y0 / density - 1 + by, 0);
            glVertex3f (x1 / density - 1 + bx, y1 / density - 1 + by, 0);
            glVertex3f (x2 / density - 1 + bx, y2 / density - 1 + by, 0);
            glVertex3f (x3 / density - 1 + bx, y3 / density - 1 + by, 0);
            mi->polygon_count++;
            if (wire) glEnd();
          }
      }

  if (!wire)
    glEnd();
}


static void
move_grid (ModeInfo *mi)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];
  long x, y;
  long wh = density * 2;
  Bool wire = MI_IS_WIREFRAME(mi);
  GLfloat max = 1.0 / density * 3;

  if (bp->button_down_p)
    return;

  bp->xoff += bp->dx;
  bp->yoff += bp->dy;

  if (! (random() % 50))
    {
      bp->dx += frand(0.0002) * RANDSIGN() * speed;
      bp->dy += frand(0.0002) * RANDSIGN() * speed;
    }

  if (bp->dx > 0.003 * speed) bp->dx = 0.003 * speed;
  if (bp->dy > 0.003 * speed) bp->dy = 0.003 * speed;

  for (y = 0; y < wh; y++)
    for (x = 0; x < wh; x++)
      {
        node *n = &bp->nodes[y * wh + x];
        GLfloat x2 = n->x + n->dx;
        GLfloat y2 = n->y + n->dy;

        if (x2 < n->gx + max && x2 >= n->gx - max &&
            y2 < n->gy + max && y2 >= n->gy - max)
          {
            n->x = x2;
            n->y = y2;
          }

        if (! (random() % 50))
          {
            n->dx += frand(0.0005) * RANDSIGN() * speed;
            n->dy += frand(0.0005) * RANDSIGN() * speed;
          }

        /* If this quad was not drawn, it's ok to re-randomize stripes, */
        if (! n->drawn_p)
          {
            int i = random() % bp->ncolors;
            int j = (i + bp->ncolors / 2) % bp->ncolors;
            GLfloat cscale = 0.3;

            n->color1[0] = bp->colors[i].red   / 65536.0;
            n->color1[1] = bp->colors[i].green / 65536.0;
            n->color1[2] = bp->colors[i].blue  / 65536.0;
            n->color1[3] = 1.0;

            n->color2[0] = bp->colors[j].red   / 65536.0;
            n->color2[1] = bp->colors[j].green / 65536.0;
            n->color2[2] = bp->colors[j].blue  / 65536.0;
            n->color2[3] = 1.0;

            if (! wire)
              {
                n->color1[0] = cscale * n->color1[0] + 1 - cscale;
                n->color1[1] = cscale * n->color1[1] + 1 - cscale;
                n->color1[2] = cscale * n->color1[2] + 1 - cscale;
                n->color2[0] = cscale * n->color2[0];
                n->color2[1] = cscale * n->color2[1];
                n->color2[2] = cscale * n->color2[2];
              }

            n->horiz_p  = random() & 1;
            n->nstripes = 2 + (int) (BELLRAND(1.0 / thickness));
          }
        n->drawn_p = False;
      }
}


/* Window management, etc
 */
ENTRYPOINT void
reshape_dazzle (ModeInfo *mi, int width, int height)
{
  glViewport (0, 0, (GLint) width, (GLint) height);

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

  if (width > height * 5) {   /* tiny window: show middle */
    GLfloat s = (GLfloat)height/width;
    glOrtho (0, 1, 0.5-s, 0.5+s, -1, 1);
  }

# ifdef HAVE_MOBILE	/* So much WTF */
  {
    int rot = current_device_rotation();

    glTranslatef (0.5, 0.5, 0);

    if (rot == 180 || rot == -180) {
      glTranslatef (1, 1, 0);
    } else if (rot == 90 || rot == -270) {
      glRotatef (180, 0, 0, 1);
      glTranslatef (0, 1, 0);
    } else if (rot == -90 || rot == 270) {
      glRotatef (180, 0, 0, 1);
      glTranslatef (1, 0, 0);
    }

    glTranslatef(-0.5, -0.5, 0);
  }
# endif

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  glClear(GL_COLOR_BUFFER_BIT);
}


static void
dazzle_randomize (ModeInfo *mi)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];
  long x, y;
  long wh = density * 2;

  bp->ncolors = MI_NCOLORS(mi) - 1;
  if (bp->ncolors < 1) bp->ncolors = 1;
  if (bp->colors) free (bp->colors);
  bp->colors = (XColor *) calloc (bp->ncolors, sizeof(XColor));
  if (bp->ncolors < 3)
    make_random_colormap (0, 0, 0, bp->colors, &bp->ncolors,
                          True, False, 0, False);
  else
    make_smooth_colormap (0, 0, 0,
                          bp->colors, &bp->ncolors,
                          False, False, False);
  if (bp->ncolors < 1) abort();

  bp->dragging = 0;
  if (bp->nodes) free (bp->nodes);

  bp->nodes = (node *) calloc (wh * wh, sizeof (node));
  for (y = 0; y < wh; y++)
    for (x = 0; x < wh; x++)
      {
        node *n = &bp->nodes[wh * y + x];
        n->gx = n->x = x;
        n->gy = n->y = y;
      }

  bp->dx = bp->dy = 0;
  bp->xoff = bp->yoff = 0;
  for (x = 0; x < 1000; x++)
    move_grid (mi);

  bp->dx = frand(0.0005) * RANDSIGN() * speed;
  bp->dy = frand(0.0005) * RANDSIGN() * speed;

  if (bp->mode == SHIPS || bp->mode == RANDOM)
    {
      bp->which_ship = random() % countof(all_ships);
      if (bp->mode == RANDOM && !(random() % 3))
        bp->which_ship = -1;
    }

  if (bp->which_ship != -1)
    {
      bp->dx /= 10;
      bp->dy /= 10;
    }
}


ENTRYPOINT Bool
dazzle_handle_event (ModeInfo *mi, XEvent *event)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];
  Bool wire = MI_IS_WIREFRAME(mi);
  GLfloat bx = fmod ((double) bp->xoff, 2.0);
  GLfloat by = fmod ((double) bp->yoff, 2.0);
  long wh = density * 2;

  if (event->xany.type == ButtonPress)
    {
      GLfloat x = (GLfloat) event->xbutton.x / MI_WIDTH (mi)  - 0.5;
      GLfloat y = (GLfloat) event->xbutton.y / MI_HEIGHT (mi) - 0.5;
      node *nn = 0;
      int xoff = 0, yoff = 0;
      GLfloat d2 = 999999;
      long x0, y0, x1, y1;

      if (wire) x /= 0.2, y /= 0.2;

      for (y0 = -1; y0 <= 1; y0++)
        for (x0 = -1; x0 <= 1; x0++)
          for (y1 = 0; y1 < wh; y1++)
            for (x1 = 0; x1 < wh; x1++)
              {
                node *n0 = &bp->nodes[(y1 % wh) * wh + (x1 % wh)];
                double dist2;
                GLfloat x2 = n0->x / density - 1 + bx + x0*2;
                GLfloat y2 = n0->y / density - 1 + by + y0*2;

                dist2 = (x - x2) * (x - x2) + (y - y2) * (y - y2);
                if (dist2 < d2)
                  {
                    d2 = dist2;
                    nn = n0;
                    xoff = x0;
                    yoff = y0;
                  }
              }

      bp->button_down_p = True;
      bp->dragging = nn;
      bp->drag_x = xoff;
      bp->drag_y = yoff;
      return True;
    }
  else if (event->xany.type == ButtonRelease)
    {
      bp->dragging = 0;
      bp->button_down_p = False;
      return True;
    }
  else if (event->xany.type == MotionNotify && bp->dragging)
    {
      GLfloat x = (GLfloat) event->xmotion.x / MI_WIDTH (mi)  - 0.5;
      GLfloat y = (GLfloat) event->xmotion.y / MI_HEIGHT (mi) - 0.5;
      if (wire) x /= 0.2, y /= 0.2;
      x -= bx;
      y -= by;
      x -= bp->drag_x * 2;
      y -= bp->drag_y * 2;
      bp->dragging->x = x * density + density;
      bp->dragging->y = y * density + density;
      bp->dragging->dx = bp->dragging->dy = 0;
      return True;
    }
  else if (screenhack_event_helper (MI_DISPLAY(mi), MI_WINDOW(mi), event))
    {
      dazzle_randomize (mi);
      return True;
    }

  return False;
}


ENTRYPOINT void 
init_dazzle (ModeInfo *mi)
{
  dazzle_configuration *bp;

  MI_INIT (mi, bps);

  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  if (!mode_arg || !*mode_arg || !strcasecmp(mode_arg, "random"))
    bp->mode = RANDOM;
  else if (!strcasecmp(mode_arg, "ship") || !strcasecmp(mode_arg, "ships"))
    bp->mode = SHIPS;
  else if (!strcasecmp(mode_arg, "flat"))
    bp->mode = FLAT;
  else
    {
      fprintf (stderr, "%s: mode must be ship, flat or random, not %s\n",
               progname, mode_arg);
      exit (1);
    }

  bp->which_ship = -1;
  if (bp->mode == SHIPS || bp->mode == RANDOM)
    {
      int i;
      bp->dlists = (GLuint *) calloc (countof(all_ships)+1, sizeof(GLuint));
      for (i = 0; i < countof(all_ships); i++)
        {
          const struct gllist *gll = *all_ships[i];

          bp->dlists[i] = glGenLists (1);
          glNewList (bp->dlists[i], GL_COMPILE);

          glMatrixMode(GL_MODELVIEW);
          glPushMatrix();
          glMatrixMode(GL_TEXTURE);
          glPushMatrix();
          glMatrixMode(GL_MODELVIEW);

          if (random() & 1)
            {
              glScalef (-1, 1, 1);
              glTranslatef (-1, 0, 0);
            }
          renderList (gll, MI_IS_WIREFRAME(mi));

          glMatrixMode(GL_TEXTURE);
          glPopMatrix();
          glMatrixMode(GL_MODELVIEW);
          glPopMatrix();
          glEndList ();
        }
    }

  dazzle_randomize (mi);
  reshape_dazzle (mi, MI_WIDTH(mi), MI_HEIGHT(mi));
}


ENTRYPOINT void
draw_dazzle (ModeInfo *mi)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];
  Bool wire = MI_IS_WIREFRAME(mi);
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  int x, y;

  if (!bp->glx_context)
    return;

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

  glShadeModel(GL_SMOOTH);
  glDisable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  glDisable(GL_CULL_FACE);

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glPushMatrix ();
  mi->polygon_count = 0;

  glTranslatef (0.5, 0.5, 0);

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

  move_grid (mi);

  for (y = -1; y <= 1; y++)
    for (x = -1; x <= 1; x++)
      draw_grid (mi, x, y);

  if (bp->which_ship != -1)
    {
# ifdef HAVE_IPHONE
      int rot = current_device_rotation();
# endif

      if (wire)
        glColor3f (1, 0, 0);
      else
        {
          glColor3f (0, 0, 0);

          /* Draw into the depth buffer but not the frame buffer */
          glColorMask (GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
          glClear (GL_DEPTH_BUFFER_BIT);
          glEnable (GL_DEPTH_TEST);
        }

# ifdef HAVE_IPHONE
      glRotatef (90, 0, 0, 1);
      if (rot == 90 || rot == -270)
        glRotatef (180, 0, 0, 1);
# endif

      glPushMatrix();
      glRotatef (90, 1, 0, 0);
      glScalef (0.9, 0.9, 0.9);
      glTranslatef (-0.5, 0, -0.2);

# ifdef HAVE_IPHONE
      if (rot == 0 || rot == 180 || rot == -180)
        glScalef (1, 1, (GLfloat) MI_HEIGHT(mi) / MI_WIDTH(mi));
      else
# endif
        glScalef (1, 1, (GLfloat) MI_WIDTH(mi) / MI_HEIGHT(mi));

      /* Wave boat horizontally and vertically */
      glTranslatef (cos ((double) bp->frames / 80 * M_PI * speed) / 200,
                    0,
                    cos ((double) bp->frames / 60 * M_PI * speed) / 300);

      glCallList (bp->dlists[bp->which_ship]);
      mi->polygon_count += (*all_ships[bp->which_ship])->points / 3;
      glPopMatrix();

      /* Wave horizon vertically */
      glTranslatef (0,
                    cos ((double) bp->frames / 120 * M_PI * speed) / 200,
                    0);

      if (! wire)
        {
          glColorMask (GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

          /* Black out everything that isn't a ship. */
# if 0
          glBegin (GL_QUADS);
          glVertex3f (-1, -1, 0);
          glVertex3f (-1,  1, 0);
          glVertex3f ( 1,  1, 0);
          glVertex3f ( 1, -1, 0);
          glEnd();
# else
          {
            GLfloat horizon = 0.15;

            glColor3f (0.7, 0.7, 1.0);
            glBegin (GL_QUADS);
            glVertex3f (-1, -1, 0);
            glVertex3f (-1,  horizon, 0);
            glVertex3f ( 1,  horizon, 0);
            glVertex3f ( 1, -1, 0);
            glEnd();

            glColor3f (0.0, 0.05, 0.2);
            glBegin (GL_QUADS);
            glVertex3f (-1, horizon, 0);
            glVertex3f (-1, 1, 0);
            glVertex3f ( 1, 1, 0);
            glVertex3f ( 1, horizon, 0);
            glEnd();
          }
# endif

          glDisable (GL_DEPTH_TEST);
        }
    }

  if (wire)
    {
      glColor3f(0,1,1);
      glLineWidth(4);
      glBegin(GL_LINE_LOOP);
      glVertex3f(-0.5, -0.5, 0);
      glVertex3f(-0.5,  0.5, 0);
      glVertex3f( 0.5,  0.5, 0);
      glVertex3f( 0.5, -0.5, 0);
      glEnd();
      glLineWidth(1);
    }

  glPopMatrix ();

  bp->frames++;
  if (mi->fps_p) do_fps (mi);
  glFinish();

  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_dazzle (ModeInfo *mi)
{
  dazzle_configuration *bp = &bps[MI_SCREEN(mi)];

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

  if (bp->nodes) free (bp->nodes);
  if (bp->colors) free (bp->colors);
  if (bp->dlists)
    {
      int i;
      for (i = 0; i < countof(all_ships); i++)
        glDeleteLists (bp->dlists[i], 1);
      free (bp->dlists);
    }
}

XSCREENSAVER_MODULE_2 ("RazzleDazzle", razzledazzle, dazzle)

#endif /* USE_GL */
