/* beats, Copyright (c) 2020 David Eccles (gringer) <hacking@gringene.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.
 */

/* Beats changes the position of objects in time with a
 * synchronisation signal (or more correctly, based on the time
 * elapsed since the last synchronisation point). By default, the
 * system clock is used for this signal, with synchronisation
 * happening every minute. The location of objects is entirely
 * dependant on this synchronisation signal; there is no multi-object
 * state that needs to be stored, although there may be some styling
 * state required.
 */

#define DEFAULTS	"*count:	30          \n" \
			"*delay:	30000       \n" \
			"*showFPS:      False       \n" \
			"*wireframe:    False       \n" \

# define release_beats 0

#include "xlockmore.h"
#include "colors.h"
#include "sphere.h"
#include "hsv.h"
#include <ctype.h>
#include <sys/time.h>

#ifdef USE_GL /* whole file */

#define DEF_CYCLE       "-1"
#define DEF_TICK        "True"
#define DEF_BLUR        "True"

#define SPHERE_SLICES 16  /* how densely to render spheres */
#define SPHERE_STACKS 16


typedef struct {
  GLXContext *glx_context;
  Bool button_down_p;

  GLuint beats_list;

  GLfloat pos;

  int ball_count; /* Number of balls */
  int preset_cycle; /* Cycle to show (-1 for random) */
  Bool use_tick; /* Add tick for clockwise / galaxy */
  Bool use_blur; /* Motion blur */
  int ncolors;
  XColor *colors;
  int ccolor;
  int color_shift;

} beats_configuration;

static beats_configuration *bps = NULL;

static int cycle_arg;
static Bool tick_arg;
static Bool blur_arg;

static XrmOptionDescRec opts[] = {
  { "-cycle",   ".cycle",   XrmoptionSepArg, 0 },
  { "-count",   ".count",   XrmoptionSepArg, 0 },
  { "-tick",    ".tick",     XrmoptionNoArg, "on" },
  { "+tick",    ".tick",     XrmoptionNoArg, "off" },
  { "-blur",    ".blur",     XrmoptionNoArg, "on" },
  { "+blur",    ".blur",     XrmoptionNoArg, "off" }
};

static argtype vars[] = {
  {&cycle_arg,   "cycle",   "Cycle",   DEF_CYCLE,   t_Int},
  {&tick_arg,   "tick",     "Tick",   DEF_TICK,   t_Bool},
  {&blur_arg,   "blur",     "Blur",   DEF_BLUR,   t_Bool}
};

static OptionStruct desc[] = {
  {"-count num", "number of balls"},
  {"-cycle num", "cycle / pattern type"},
  {"-/+tick", "enable/disable tick for clockwise and galaxy"},
  {"-/+blur", "enable/disable motion blur"}
};

ENTRYPOINT ModeSpecOpt beats_opts =
  {countof(opts), opts, countof(vars), vars, desc};

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

  glClear(GL_COLOR_BUFFER_BIT);
}


ENTRYPOINT Bool
beats_handle_event (ModeInfo *mi, XEvent *event)
{
  return True;
}

static Bool getFracColour (GLfloat* retVal, float posFrac, float s){
  /* top: red, right: yellow, bottom: [dark] green, left: blue */
  /* note: fixed point; align to 0.1 degree increments */
  int theta, h, v;
  unsigned short r,g,b;
  theta = ((int)(posFrac * 3600) % 3600 + 3600) % 3600;
  v = 100;
  if        ((theta >=   0) && (theta <  900)) {
    h = (theta * 600) / 900;
  } else if ((theta >=  900) && (theta < 1800)) {
    h = ((theta - 900) * 600) / 900 + 600;
    v = 100 - ((theta - 900) / 18);
  } else if ((theta >= 1800) && (theta < 2700)) {
    h = ((theta - 1800) * 1200) / 900 + 1200;
    v = ((theta - 1800) / 18) + 50;
  } else /* if ((theta >= 2700) && (theta < 3600))*/ {
    h = ((theta - 2700) * 1200) / 900 + 2400;
  }
  hsv_to_rgb((int)h / 10.0, s, v / 100.0, &r, &g, &b);
  retVal[0] = r / 65535.0;
  retVal[1] = g / 65535.0;
  retVal[2] = b / 65535.0;
  return True;
}


ENTRYPOINT void 
init_beats (ModeInfo *mi)
{
  beats_configuration *bp;
  int wire = MI_IS_WIREFRAME(mi);

  MI_INIT (mi, bps);
  bp = &bps[MI_SCREEN(mi)];

  bp->glx_context = init_GL(mi);

  reshape_beats (mi, MI_WIDTH(mi), MI_HEIGHT(mi));

  if (!wire)
    {
      GLfloat pos[4] = {1.0, 1.0, 1.0, 0.0};
      GLfloat amb[4] = {0.02, 0.02, 0.02, 1.0};
      GLfloat dif[4] = {1.0, 1.0, 1.0, 1.0};
      GLfloat spc[4] = {0.2, 0.2, 0.2, 0.2};

      glEnable(GL_LIGHTING);
      glEnable(GL_DEPTH_TEST);
      glEnable(GL_CULL_FACE);

      glLightfv(GL_LIGHT0, GL_POSITION, pos);
      glLightfv(GL_LIGHT0, GL_AMBIENT,  amb);
      glLightfv(GL_LIGHT0, GL_DIFFUSE,  dif);
      glLightfv(GL_LIGHT0, GL_SPECULAR, spc);

      glEnable(GL_LIGHT0);
    }

  if (cycle_arg > 3) cycle_arg = -1;

  bp->ball_count = MI_COUNT(mi);
  if (bp->ball_count < 2) bp->ball_count = 2;

  bp->preset_cycle = cycle_arg;
  bp->use_tick = tick_arg;
  bp->use_blur = blur_arg;
  
# ifdef HAVE_ANDROID
  bp->use_blur = False; /* Works on iOS but not Android */
# endif

  bp->ncolors = 128;
  bp->colors = (XColor *) calloc(bp->ncolors, sizeof(XColor));
  make_smooth_colormap (0, 0, 0,
                        bp->colors, &bp->ncolors,
                        False, 0, False);

  bp->beats_list = glGenLists(1);

  glNewList (bp->beats_list, GL_COMPILE);
  glScalef(0.71, 0.71, 0.71);
  unit_sphere (SPHERE_STACKS, SPHERE_SLICES, wire);
  glEndList ();

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}


ENTRYPOINT void
draw_beats (ModeInfo *mi)
{
  beats_configuration *bp = &bps[MI_SCREEN(mi)];
  Display *dpy = MI_DISPLAY(mi);
  Window window = MI_WINDOW(mi);
  unsigned num_objects = bp->ball_count, oi;
  struct timeval tv, tvOrig;
  struct tm *now;
  Bool sineWaveTick = bp->use_tick;
  Bool motionBlur = bp->use_blur;
  size_t cycle, dist;
  unsigned int tmS, tmM, tmH, tmD;
  unsigned int timeSeed;
  int timeDelta = 0;
  size_t blurOffset = 10; /* offset per blur frame, in milliseconds */
  size_t framesPerBlur = 20;  /* number of sub-frames to blur */
  size_t deltaLimit = (motionBlur) ? (blurOffset * framesPerBlur) : 1;
  float ballAlpha;
  float secFrac, minFrac, minProp, hourProp, halfDayProp,
    z, op, mp, m2m,
    theta, delta, blurFrac, oFP, pathLength;

  static const GLfloat bspec[4]  = {1.0, 1.0, 1.0, 1.0};
  static const GLfloat bshiny    = 92.0;

  GLfloat bcolor[4] = {0.85, 0.75, 0.75, 1.0};

  if (!bp->glx_context)
    return;
  gettimeofday (&tvOrig, NULL);
  
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
  
  glShadeModel(GL_SMOOTH);
  
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_ALPHA_TEST);
  glEnable(GL_NORMALIZE);
  glEnable(GL_CULL_FACE);
  glEnable(GL_BLEND);
  glEnable(GL_DEPTH_TEST);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

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

  {
    GLfloat s = (MI_WIDTH(mi) < MI_HEIGHT(mi)
                 ? MI_WIDTH(mi) / (GLfloat) MI_HEIGHT(mi)
                 : 1);
    glScalef (s, s, s);
  }

  /* timeDelta is in milliseconds */
  for(timeDelta = 0; timeDelta <= deltaLimit; timeDelta += blurOffset){
    if(timeDelta < blurOffset){
      /* glEnable(GL_DEPTH_TEST); */
      ballAlpha = 1.0;
    } else {
      /* glDisable(GL_DEPTH_TEST); */
      ballAlpha = 1.0 / framesPerBlur;
    }
    blurFrac = sin((1 - (float) timeDelta / deltaLimit) * M_PI_2) * ballAlpha;
    tv = tvOrig;
    now = localtime (&tv.tv_sec); /* This seems to be needed for seconds */
    tmS = now->tm_sec;
    tmM = now->tm_min;
    tmH = now->tm_hour;
    tmD = now->tm_yday;
    secFrac = ((tv.tv_usec % 1000000) - (timeDelta * 1000)) / (1e6);
    if(secFrac < 0){
      secFrac += 1;
      tmS--;
      if(tmS < 0){
	tmS += 60;
	tmM--;
      }
      if(tmM < 0){
	tmM += 60;
	tmH--;
      }
      if(tmH < 0){
	tmH += 24;
	tmD--;
      }
      if(tmD < 0){
	/* note: this won't be accurate for leap years, but the rare
	   event logic is complex enough */
	tmD += 365;
      }
    }
    /* pseudo-random generator based on current minute */
    timeSeed = (((tmM+1) * (tmM+1) * ((tmH+1) * 37) *
		 ((tmD+1) * 1151) * 1233599) % 653);
    cycle = timeSeed % 4;
    if(bp->preset_cycle != -1){
      cycle = bp->preset_cycle;
    }
    if(sineWaveTick && (cycle == 0 || cycle == 3)){
      Bool doTick = (timeSeed % 2 == 0);
      if(doTick){ /* choose to tick randomly */
	/* sine-wave 'tick' motion, converts linear 0..1 to 
	   pause/fast/pause 0..1 */
	secFrac = (1.0 - sin((0.5-secFrac) * M_PI))/2.0;
      }
    }
    minFrac = tmS / 60.0;
    /* now we have enough information to calculate our goal statistic,
       minProp: the position in the synchronisation cycle of one
       minute */
    minProp = (minFrac - trunc(minFrac)) + (secFrac / 60);
    m2m = minProp * 2 * M_PI;

    /* change colour based on the minute and hour */
    hourProp = tmM / 60.0 + minProp / 60.0;
    hourProp = hourProp - trunc(hourProp);

    halfDayProp = tmH / 12.0 + hourProp / 12.0;
    halfDayProp = halfDayProp - trunc(halfDayProp);

    mi->polygon_count = 0;

    for(oi = 0; oi < num_objects; oi++){
      glPushMatrix ();
      glScalef(1.1, 1.1, 1.1);

      /* Object Fraction Position - 0..1 depending on native Z order */
      oFP = oi * 1.0 / (num_objects - 1);

      /* set Z distance between [-3.5 .. 0.5] (common to all cycles) */
      z = (oFP) * 4.0 - 3.5;

      /* set colour (common to all cycles) */
      if(oFP < (1 / 3.0)){ /* "second" objects */
	getFracColour(bcolor, minProp, 1.0);
      } else if(oFP < (2 / 3.0)) { /* "minute" objects */
	getFracColour(bcolor, hourProp, 1.0);
      } else { /* "hour" objects */
	getFracColour(bcolor, halfDayProp, 1.0);
      }
    
      /* set x/y location */
      if(cycle == 0){
	/* clockwise */
	glRotatef(-minProp * 360 * (oi + 1), 0, 0, 1);
	glTranslatef(0, 5, 0);
      } else if(cycle == 1){
	/* rain dance */
	float y = 10 * cos(m2m * (oi + 1.0))/2;
	/* rotate around Y axis */
	glTranslatef(0, 0, -20);
	glRotatef(minProp * 360, 0, 1, 0);
	glTranslatef(0, y, 20);
      } else if(cycle == 2){
	/* metronome */
	theta = sin(-m2m * (oi + 1.0)) * 90;
	/* rotate around z axis at (-5, 0, 0) */
	glTranslatef(0, -5, 0);
	glRotatef(theta, 0, 0, 1);
	glTranslatef(0, 10, 0);
      } else if (cycle == 3){
	/* galaxy */
	mp = (num_objects - 1.0) / 2;
	op = mp - oi;
	dist = (int)(fabs(op)+0.5); /* dist from centre */
	/* make sure each object travels an integer number of loops in
	   a path through one cycle */
	pathLength = (int)((60.0 / dist) + 0.5) * 720.0;
	delta = pathLength / 2;
	theta = -minProp * delta - 180;
	/* rotate around X axis after translating (0,-5,0) */
	glTranslatef(0, 0, -20);
	glRotatef(minProp * 360 - 180, 1, 0, 0);
	glTranslatef(0, 0, 20);
	glTranslatef(0, -5, 0);
	/* rotate around Y axis */
	glTranslatef(0, 0, -20);
	glRotatef(theta, 0, 1, 0);
	glTranslatef(0, 0, 20);
      }

      /* spread out based on Z position */
      glTranslatef(0, 0, (z - 0.5) * 10);

      /* set up colours */
      glMaterialfv (GL_FRONT, GL_SPECULAR,            bspec);
      glMateriali  (GL_FRONT, GL_SHININESS,           bshiny);
      if(motionBlur){
	bcolor[3] = (timeDelta == 0) ? 1.0 : blurFrac; /* was ballAlpha */
      }
      glMaterialfv (GL_FRONT, GL_AMBIENT_AND_DIFFUSE, bcolor);
      glCallList (bp->beats_list); /* draw sphere */
      mi->polygon_count += (SPHERE_SLICES * SPHERE_STACKS);

      glPopMatrix();
    }
  }
  glPopMatrix();
  if (mi->fps_p) do_fps (mi);
  glFinish();
  glXSwapBuffers(dpy, window);
}


ENTRYPOINT void
free_beats (ModeInfo *mi)
{
  beats_configuration *bp = &bps[MI_SCREEN(mi)];
  if (!bp->glx_context) return;
  glXMakeCurrent(MI_DISPLAY(mi), MI_WINDOW(mi), *bp->glx_context);
  if (bp->colors) free (bp->colors);
  if (glIsList(bp->beats_list)) glDeleteLists(bp->beats_list, 1);
}

XSCREENSAVER_MODULE ("Beats", beats)

#endif /* USE_GL */
