#define _GNU_SOURCE
/* needed because objects.h undefines _GNU_SOURCE before
   presenterlib-c.h includes math.h */
#include <features.h>
#include <math.h>
#include "objects.h"
#undef TRUE
#undef FALSE
#include "presenterlib-c.h"

/* Variables */

struct trigger_task_args trigger_task_args;
double max_time = 0;
pthread_t renderer_thread, trigger_thread;
int debugging = FALSE;
int error = FALSE;
int festival_initialized = FALSE;
volatile int renderer_stopped = TRUE;
volatile int first_trigger = FALSE;
volatile int volume = FALSE;
volatile unsigned int volumes = 0;
volatile unsigned int counter = 0;

/* Scheme->C */

TSCP bool_tscp(int bool) {
  if (bool) return TRUEVALUE;
  else return FALSEVALUE;
}

int tscp_bool(TSCP bool) {
  if (bool==TRUEVALUE) return 1;
  if (bool==FALSEVALUE) return 0;
  sc_error("TSCP_BOOL", "~s is not a boolean", CONS(bool, EMPTYLIST));
  exit(EXIT_FAILURE);
}

/* Fix Scheme->C signed integers */

TSCP sc_int_tscp(int n) {
  if (n<=MAXTSCPINT&&n>=MINTSCPINT) return C_FIXED(n);
  return MAKEFLOAT((double)n);
}

int sc_tscp_int(TSCP p) {
  switch TSCPTAG(p) {
    case FIXNUMTAG:
      return FIXED_C(p);
      break;
    case EXTENDEDTAG:
      if (TX_U(p)->extendedobj.tag==DOUBLEFLOATTAG) {
        return (S2CINT)FLOAT_VALUE(p);
      }
      break;
    }
  sc_error("TSCP_S2CINT", "Argument cannot be converted to C int",
	   EMPTYLIST);
  return 0;                     /* silences warning */
}

/* stuff */

void *presenter_malloc(size_t size) {
  void *p = malloc(size);
  if (p==NULL) presenter_error("Out of memory");
  return p;
}

double current_time(void) {
  struct timeval time;
  if (gettimeofday(&time, NULL)!=0) presenter_error("gettimeofday failed");
  /* needs work: Will it convert division into multiplication? */
  return ((double)time.tv_sec)+((double)time.tv_usec)/1e6;
}

/* ffmpeg */

int ffmpeg_first_video_stream(struct ffmpeg_video *ffmpeg_video) {
  if (av_find_stream_info(ffmpeg_video->pFormatCtx)<0) {
    presenter_error("Can't get video stream information");
  }
  /* upper bound */
  for (unsigned int i = 0; i<ffmpeg_video->pFormatCtx->nb_streams; i++) {
    if (ffmpeg_video->pFormatCtx->streams[i]->codec->codec_type==
	AVMEDIA_TYPE_VIDEO) {
      return i;
    }
  }
  presenter_error("Can't find first video stream");
}

AVCodecContext *ffmpeg_get_codec(struct ffmpeg_video *ffmpeg_video) {
  AVCodecContext *pCodecCtx =
    ffmpeg_video->pFormatCtx->streams[ffmpeg_video->videoStream]->codec;
  AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
  if (pCodec==NULL) presenter_error("Unsupported codec!");
  if (avcodec_open(pCodecCtx, pCodec)<0) presenter_error("Can't open codec!");
  return pCodecCtx;
}

int ffmpeg_next_frame(struct ffmpeg_video *ffmpeg_video) {
  av_free_packet(&ffmpeg_video->packet);
  int frameFinished;
  int nextFrameValid =
    av_read_frame(ffmpeg_video->pFormatCtx, &ffmpeg_video->packet)>=0;
  if (nextFrameValid&&
      ffmpeg_video->packet.stream_index==ffmpeg_video->videoStream) {
    avcodec_decode_video2(ffmpeg_video->pCodecCtx,
			  ffmpeg_video->pFrame,
			  &frameFinished,
			  &ffmpeg_video->packet);
    if (frameFinished) ffmpeg_video->frame++;
    else ffmpeg_next_frame(ffmpeg_video);
  } else if (nextFrameValid) {
    ffmpeg_next_frame(ffmpeg_video);
  } else if (!ffmpeg_video->videoFinished&&!nextFrameValid) {
    /* This is required because ffmpeg hangs on to many frames internally */
    AVPacket packet;
    packet.data = 0;
    packet.size = 0;
    avcodec_decode_video2(ffmpeg_video->pCodecCtx,
			  ffmpeg_video->pFrame,
			  &frameFinished,
			  &packet);
    if (frameFinished) ffmpeg_video->frame++;
    else ffmpeg_video->videoFinished = 1;
  }
  return !ffmpeg_video->videoFinished;
}

char ffmpeg_video_finished(struct ffmpeg_video *ffmpeg_video) {
  return ffmpeg_video->videoFinished==1;
}

void ffmpeg_close_and_free_video(struct ffmpeg_video *ffmpeg_video) {
  av_free(ffmpeg_video->buffer);
  av_free(ffmpeg_video->pFrameBGRA);
  av_free(ffmpeg_video->pFrame);
  avcodec_close(ffmpeg_video->pCodecCtx);
  av_close_input_file(ffmpeg_video->pFormatCtx);
  av_free_packet(&ffmpeg_video->packet);
  ffmpeg_video->videoFinished = 1;
  free(ffmpeg_video);
}

struct ffmpeg_video *ffmpeg_open_video(char *filename) {
  struct ffmpeg_video *ffmpeg_video =
    (struct ffmpeg_video *)presenter_malloc(sizeof(struct ffmpeg_video));
  /* needs work: bzero is deprecated, should use memset */
  bzero(ffmpeg_video, sizeof(struct ffmpeg_video));
  av_register_all();
  if (avformat_open_input(&ffmpeg_video->pFormatCtx, filename, NULL, NULL)!=0) {
    presenter_error("Can't open video %s", filename);
  }
  ffmpeg_video->videoStream = ffmpeg_first_video_stream(ffmpeg_video);
  ffmpeg_video->pCodecCtx = ffmpeg_get_codec(ffmpeg_video);
  ffmpeg_video->pFrame = avcodec_alloc_frame();
  ffmpeg_video->pFrameBGRA = avcodec_alloc_frame();
  if (!ffmpeg_video->pFrameBGRA||!ffmpeg_video->pFrame) {
    presenter_error("Can't allocate frame");
  }
  ffmpeg_video->buffer =
    (uint8_t *)av_malloc(avpicture_get_size(PIX_FMT_BGRA,
					    ffmpeg_video->pCodecCtx->width,
					    ffmpeg_video->pCodecCtx->height)*
			 sizeof(uint8_t));
  avpicture_fill((AVPicture *)ffmpeg_video->pFrameBGRA,
		 ffmpeg_video->buffer, PIX_FMT_BGRA,
		 ffmpeg_video->pCodecCtx->width,
		 ffmpeg_video->pCodecCtx->height);
  ffmpeg_video->img_convert_ctx =
    sws_getContext(ffmpeg_video->pCodecCtx->width,
		   ffmpeg_video->pCodecCtx->height,
		   ffmpeg_video->pCodecCtx->pix_fmt,
		   ffmpeg_video->pCodecCtx->width,
		   ffmpeg_video->pCodecCtx->height,
		   PIX_FMT_BGRA, SWS_BICUBIC,
		   NULL, NULL, NULL);
  ffmpeg_video->videoFinished = 0;
  ffmpeg_video->frame = 0;
  av_init_packet(&ffmpeg_video->packet);
  ffmpeg_next_frame(ffmpeg_video);
  return ffmpeg_video;
}

uint8_t *ffmpeg_get_frame(struct ffmpeg_video *ffmpeg_video) {
  uint8_t *data =
    (uint8_t *)
    presenter_malloc(avpicture_get_size(PIX_FMT_BGRA,
					ffmpeg_video->pCodecCtx->width,
					ffmpeg_video->pCodecCtx->height)*
		     sizeof(uint8_t));
  sws_scale(ffmpeg_video->img_convert_ctx,
	    (const uint8_t *const *)ffmpeg_video->pFrame->data,
	    ffmpeg_video->pFrame->linesize, 0,
	    ffmpeg_video->pCodecCtx->height,
	    ffmpeg_video->pFrameBGRA->data, ffmpeg_video->pFrameBGRA->linesize);
  memcpy(data, ffmpeg_video->buffer,
	 avpicture_get_size(PIX_FMT_BGRA,
			    ffmpeg_video->pCodecCtx->width,
			    ffmpeg_video->pCodecCtx->height)*sizeof(uint8_t));
  return data;
}

unsigned int ffmpeg_video_width(struct ffmpeg_video *ffmpeg_video) {
  return ffmpeg_video->pCodecCtx->width;
}

unsigned int ffmpeg_video_height(struct ffmpeg_video *ffmpeg_video) {
  return ffmpeg_video->pCodecCtx->height;
}

double ffmpeg_video_frame_rate(struct ffmpeg_video *ffmpeg_video) {
  /* hints from
     http://ffmpeg.org/pipermail/ffmpeg-cvslog/2011-August/039936.html */
  int i = ffmpeg_video->videoStream;
  int rfps = ffmpeg_video->pFormatCtx->streams[i]->r_frame_rate.num;
  int rfps_base = ffmpeg_video->pFormatCtx->streams[i]->r_frame_rate.den;
  return (double)rfps/rfps_base;
}

Imlib_Image ffmpeg_get_frame_as_imlib(struct ffmpeg_video *ffmpeg_video) {
  sws_scale(ffmpeg_video->img_convert_ctx,
	    (const uint8_t *const *)ffmpeg_video->pFrame->data,
	    ffmpeg_video->pFrame->linesize, 0,
	    ffmpeg_video->pCodecCtx->height,
	    ffmpeg_video->pFrameBGRA->data, ffmpeg_video->pFrameBGRA->linesize);
  Imlib_Image image =
    imlib_create_image_using_copied_data(ffmpeg_video->pCodecCtx->width,
					 ffmpeg_video->pCodecCtx->height,
					 (uint32_t *)ffmpeg_video->buffer);
  return image;
}

/* This is the unsafe version of the above. You must not advance the
   frame or free the ffmpeg video until you are done with the imlib image.
   It's faster because it doesn't copy the data. */

Imlib_Image ffmpeg_get_frame_as_imlib_unsafe
(struct ffmpeg_video *ffmpeg_video) {
  sws_scale(ffmpeg_video->img_convert_ctx,
	    (const uint8_t *const *)ffmpeg_video->pFrame->data,
	    ffmpeg_video->pFrame->linesize, 0,
	    ffmpeg_video->pCodecCtx->height,
	    ffmpeg_video->pFrameBGRA->data, ffmpeg_video->pFrameBGRA->linesize);
  Imlib_Image image =
    imlib_create_image_using_data(ffmpeg_video->pCodecCtx->width,
                                  ffmpeg_video->pCodecCtx->height,
                                  (uint32_t *)ffmpeg_video->buffer);
  return image;
}

/* renderer thread */

void wakeup_gui(Display *display, Window wakeup_target) {
  XEvent event;
  event.type = KeyPress;
  event.xkey.keycode = XKeysymToKeycode(display, XK_Escape);
  event.xkey.state = 0;
  XSendEvent(display, wakeup_target, 0, 0, &event);
  XFlush(display);
}

// A run (called sequence) is a sequence of timepoints.
// A timepoint (called commands) is a sequence of commands.
// There is a single log entry for each timepoint.
// The timepoints_processed entry in in renderer_result tells the number of
//   log entries.
// Each log entry is a start_timestamp, a double returned by gettimeofday, and
//   a volume, which is an int indicated the volume index covered by that
//   timepoint. Conceivably, there could be multiple volumes covered by a
//   timepoint, in which case that would be the last one.
// Every timepoint can have at most one ADVANCE. This specifies the maximum
//   amount of time for the timepoint. If none is specified, there is no max.
//   If multiple are specified, the last one counts. By convention, ADVANCE is
//   the first command of the timepoint.
//   Currently only used in standard-timepoint.
// SLEEP sleeps the specified amount of time but not past the maximum time
//   for a timepoint specified by ADVANCE. Usually, SLEEP would be the last
//   command of the timepoint.
//   Currently only used in standard-timepoint.
// FESTIVAL issues a festival command.
// FILL_RECTANGLE, IMAGE, TEXT, and SHOW_VIDEO_FRAME, draw on the framebuffer.
// RENDER renders.
// WAKE_GUI wakes up the GUI. It is currently not used. It is unclear why it
//   is needed (and why one needs to wakup the GUI at the end of the run). The
//   GUI wakeup mechanism sends a TAB (c-I) but there is no command for that.
//   The GUI should be running anyway. All that will happen is that there will
//   be a beep and perhaps the redraw-display method will run that will do a
//   stop-and-log if renderer_stopped.
// LOAD_VIDEO just specifies which video to run. Each video specified by
//   LOAD_VIDEO keeps track of its current frame. You could presumably
//   interleave frame from different videos and a given video could appear in
//   two different LOAD_VIDEO commands. You presumably don't want this to be in
//   a LOOP.
// ADVANCE_VIDEO_FRAME advances the video frame.
// Every timepoint can have at most one LOOP. This specifies the number of
//   iterations for the timepoint. If none is specified, there is no iteration.
//   If multiple are specified, the maximum one counts. Each iteration must
//   meet the maximum amount of time for the timepoint. The timepoint is
//   executed the number of iterations plus one. LOOP is like Fortran DO, it
//   always does at least one iteration, even if iterations is negative.
// START_VOLUME just clears a flag. This flag is set by the trigger task
//   after it gets slices_per_tr triggers.
// WAIT_FOR_VOLUME waits for the flag and clears the flag. Presumably, you
//   would do this either after START_VOLUME or STOP_ON_VOLUME_AND_CLEAR so you
//   would wait for the trigger task to set the flag. Currently this is the
//   last step of play2 and tr-timepoint, used only by fixation.
// STOP_ON_VOLUME_WITHOUT_CLEARING ends the timepoint without further iteration
//   if the flag is set. Doesn't check the maximum amount of time for the
//   timepoint.
// STOP_ON_VOLUME_AND_CLEAR ends the timepoint without further iteration
//   if the flag is set. Also clears the flag. Doesn't check the maximum amount
//   of time for the timepoint.

/* return TRUE if we should rerun for another iteration */
int execute_commands(struct renderer_commands *commands,
		     struct ffmpeg_video **videos,
		     double start_timestamp,
		     Display *display,
		     unsigned int trs,
		     double tr,
		     int countdown,
		     Window message_pane,
		     GC gc,
		     int message_pane_y,
		     Window wakeup_target,
		     Imlib_Image framebuffer,
		     int number_of_targets,
		     struct renderer_target *targets,
		     int timepoint,
		     unsigned int iteration) {
  int command = 0;
  double advance_time = INFINITY;
  int another_iteration = FALSE;
  int width = targets[0].width;
  int height = targets[0].height;
  while (commands) {
    if (debugging) {
      printf("command(%d) %d @ %u,%d\n",
	     command, commands->type, volumes, volume);
    }
    switch (commands->type) {
    case ADVANCE:
      advance_time = commands->data.advance->s;
      break;
    case SLEEP:
      while (current_time()-start_timestamp<
	     /* a bit of slack time, if we make this exact we'll
		overshoot at the end of the function */
	     MIN(commands->data.sleep->s, advance_time)-0.01) {
        usleep(10);
      }
      break;
    case FESTIVAL:
      c_festival_eval_command(commands->data.festival->text);
      break;
    case FILL_RECTANGLE: {
      struct fill_rectangle *t = commands->data.fill_rectangle;
      imlib_context_set_image(framebuffer);
      imlib_context_set_color(t->r, t->g, t->b, t->a);
      imlib_image_fill_rectangle
	(t->x*width, t->y*height, t->width*width, t->height*height);
    }
      break;
    case IMAGE: {
      struct image *i = commands->data.image;
      imlib_context_set_image(i->image);
      int image_width = imlib_image_get_width();
      int image_height = imlib_image_get_height();
      imlib_context_set_image(framebuffer);
      imlib_blend_image_onto_image(i->image, 0, 0, 0,
				   image_width,
				   image_height,
                                   i->x*width,
				   i->y*height,
                                   i->width*width,
				   i->height*height);
    }
      break;
    case TEXT: {
      struct text *t = commands->data.text;
      Imlib_Font font = imlib_load_font(t->font);
      if (!font) {
        presenter_error("(%d,%d) can't load requested font %s",
			timepoint, command, t->font);
      }
      imlib_context_set_image(framebuffer);
      imlib_context_set_font(font);
      imlib_context_set_color(t->r, t->g, t->b, t->a);
      imlib_context_set_direction(t->direction);
      imlib_context_set_angle(t->angle);
      imlib_text_draw(t->x*width, t->y*height, t->text);
      imlib_free_font();
    }
      break;
    case RENDER:
      for (unsigned int i = 0; i<number_of_targets; i++) {
        struct renderer_target target = targets[i];
        imlib_context_set_image(framebuffer);
        Imlib_Image image;
        if (target.width!=width||target.height!=height) {
          image = imlib_create_cropped_scaled_image
	    (0, 0, width, height, target.width, target.height);
        }
        else image = framebuffer;
        imlib_context_set_image(image);
        imlib_context_set_drawable(target.window);
        imlib_render_image_on_drawable(target.x, target.y);
        if (target.width!=width||target.height!= height) {
          imlib_free_image_and_decache();
	}
      }
      XFlush(display);
      break;
    case WAKE_GUI:
      wakeup_gui(display, wakeup_target);
      break;
    case LOAD_VIDEO:
      videos[commands->data.load_video->id] =
	commands->data.load_video->ffmpeg_video;
      break;
    case SHOW_VIDEO_FRAME: {
      struct show_video_frame *s = commands->data.show_video_frame;
      Imlib_Image image = ffmpeg_get_frame_as_imlib_unsafe(videos[s->id]);
      imlib_context_set_image(image);
      int image_width = imlib_image_get_width();
      int image_height = imlib_image_get_height();
      imlib_context_set_image(framebuffer);
      imlib_blend_image_onto_image(image, 0, 0, 0,
				   image_width,
				   image_height,
                                   s->x*width,
				   s->y*height,
                                   s->width*width,
				   s->height*height);
      imlib_context_set_image(image);
      /* This is almost free because it only frees the surrounding
         structure, not the image, as that's still property of ffmpeg. */
      imlib_free_image_and_decache();
    }
      break;
    case ADVANCE_VIDEO_FRAME:
      ffmpeg_next_frame(videos[commands->data.advance_video_frame->id]);
      break;
    case LOOP:
      another_iteration =
	another_iteration||(iteration<commands->data.loop->iterations);
      break;
    case START_VOLUME:
      volume = FALSE;
      break;
    case WAIT_FOR_VOLUME:
      while (!volume) usleep(10);
      volume = FALSE;
      break;
    case STOP_ON_VOLUME_WITHOUT_CLEARING:
      /* stop iteration and move on */
      if (volume) return FALSE;
      break;
    case STOP_ON_VOLUME_AND_CLEAR:
      if (volume) {
        volume = FALSE;
        /* stop iteration and move on */
        return FALSE;
      }
      break;
    default:
      presenter_error("(%d,%d) unknown or unimplemented command %u %p\n",
                      timepoint, command, commands->type, commands->data);
      break;
    }
    command++;
    commands = commands->next;
  }
  double time = current_time()-start_timestamp;
  if (time>advance_time) {
    if (error) {
      presenter_error("(%d,%d) can't keep up, had %lf s but took %lf s",
		      timepoint, command, advance_time, time);
    }
    printf("(%d,%d) can't keep up, had %lf s but took %lf s",
	   timepoint, command, advance_time, time);
  }
  else {
    if (debugging) {
      printf("(%d,%d) %lf s and took %lf s\n",
	     timepoint, command, advance_time, time);
      max_time = MAX(max_time, time);
    }
  }
  if (countdown) {
    int seconds_to_go = floor((trs-volumes)*tr);
    int minutes_to_go = floor(((double)(seconds_to_go))/60.0);
    seconds_to_go = seconds_to_go-60*minutes_to_go;
    char message[80];
    if (seconds_to_go<10) {
      sprintf(&message[0], "%d:0%d", minutes_to_go, seconds_to_go);
    }
    else {
      sprintf(&message[0], "%d:%d", minutes_to_go, seconds_to_go);
    }
    XClearWindow(display, message_pane);
    XDrawString(display,
		message_pane,
		gc,
		5,
		message_pane_y,
		&message[0],
		strlen(message));
    XFlush(display);
  }
  return another_iteration;
}

void *renderer_task(void *args) {
  if (!festival_initialized) {
    c_festival_initialize();
    festival_initialized = TRUE;
  }
  if (debugging) printf("starting renderer\n");
  struct renderer_task_args *renderer_task_args =
    (struct renderer_task_args *)args;
  struct renderer_sequence *sequence = renderer_task_args->sequence;
  Display *display = XOpenDisplay("");
  if (display==NULL) presenter_error("display is NULL in renderer_task");
  int screen = DefaultScreen(display);
  struct renderer_target *targets = renderer_task_args->targets;
  Imlib_Image framebuffer =
    imlib_create_image(targets[0].width, targets[0].height);
  struct ffmpeg_video *videos[VIDEO_IDS];
  /* We can't reliably pass *roman-gc* from Scheme probably due to memory
     consistency. */
  Font font;
  GC gc;
  if (renderer_task_args->countdown) {
    XGCValues values;
    /* We don't check the return values of the next five. */
    font = XLoadFont(display, "9x15");
    gc = XCreateGC(display, renderer_task_args->message_pane, 0, &values);
    XSetForeground(display, gc, BlackPixel(display, screen));
    XSetBackground(display, gc, WhitePixel(display, screen));
    XSetFont(display, gc, font);
  }
  for (int i = 0; i<VIDEO_IDS; i++) videos[i] = NULL;
  imlib_context_disconnect_display();
  imlib_context_set_display(display);
  imlib_context_set_visual(XDefaultVisual(display, screen));
  imlib_context_set_colormap(XDefaultColormap(display, screen));
  /* 100MB of imlib font cache, just to make sure there's no lag */
  imlib_set_font_cache_size(100*1024*1024);
  struct renderer_result *renderer_result =
    presenter_malloc(sizeof(struct renderer_result));
  renderer_result->log =
    presenter_malloc(renderer_task_args->sequence_length*
		     sizeof(struct renderer_log));
  first_trigger = FALSE;
  volume = FALSE;
  volumes = 0;
  counter = 0;
  renderer_stopped = FALSE;
  if (debugging) printf("waiting for trigger\n");
  while (!first_trigger) usleep(100);
  int timepoint = 0;
  unsigned int iteration = 0;
  while (TRUE) {
    if (renderer_stopped) {
      renderer_result->stop_reason = RENDERER_WAS_STOPPED;
      renderer_result->timepoints_processed = timepoint;
      break;
    }
    if (!sequence) {
      renderer_result->stop_reason = RENDERER_FINISHED_SEQUENCE;
      renderer_result->timepoints_processed = timepoint;
      break;
    }
    renderer_result->log[timepoint].start_timestamp = current_time();
    renderer_result->log[timepoint].volumes = volumes;
    renderer_result->log[timepoint].counter = counter;
    renderer_result->log[timepoint].iteration = iteration;
    renderer_result->log[timepoint].type = sequence->commands->type;
    if (execute_commands(sequence->commands,
			 &videos[0],
			 renderer_result->log[timepoint].start_timestamp,
			 display,
			 renderer_task_args->trs,
			 renderer_task_args->tr,
			 renderer_task_args->countdown,
			 renderer_task_args->message_pane,
			 gc,
			 renderer_task_args->message_pane_y,
			 renderer_task_args->wakeup_target,
			 framebuffer,
			 renderer_task_args->number_of_targets,
			 targets,
			 timepoint,
			 iteration)) {
      iteration++;
    }
    else {
      iteration = 0;
      sequence = sequence->next;
    }
    timepoint++;
  }
  if (renderer_task_args->countdown) {
    /* We don't check the return values of the next two. */
    XFreeGC(display, gc);
    XUnloadFont(display, font);
  }
  imlib_context_set_image(framebuffer);
  imlib_free_image_and_decache();
  renderer_stopped = TRUE;
  wakeup_gui(display, renderer_task_args->wakeup_target);
  if (debugging) printf("stopping renderer, longest iteration %lf\n", max_time);
  return (void *)renderer_result;
}

void start_renderer_thread(struct renderer_task_args *renderer_task_args) {
  if (!renderer_stopped) {
    presenter_error("Can't start two renderers at once");
  }
  pthread_attr_t attributes;
  pthread_attr_init(&attributes);
  pthread_attr_setstacksize(&attributes, 10*1024*1024); /* hardwired 10MB */
  if (pthread_create
      (&renderer_thread, &attributes, renderer_task, renderer_task_args)) {
    presenter_error("Can't start renderer thread");
  }
}

struct renderer_result *stop_renderer_thread(void) {
  renderer_stopped = TRUE;
  struct renderer_result *renderer_result;
  if (pthread_join(renderer_thread, (void *)&renderer_result)) {
    presenter_error("Can't stop renderer thread");
  }
  return renderer_result;
}

void free_commands(struct renderer_commands *commands) {
  while (commands!=NULL) {
    struct renderer_commands *next = commands->next;
    switch (commands->type) {
    case ADVANCE:
      free(commands->data.advance);
      break;
    case SLEEP:
      free(commands->data.sleep);
      break;
    case FESTIVAL:
      free(commands->data.festival);
      break;
    case FILL_RECTANGLE:
      free(commands->data.fill_rectangle);
      break;
    case IMAGE:
      free(commands->data.image);
      break;
    case TEXT:
      free(commands->data.text);
      break;
    case RENDER:
      break;
    case WAKE_GUI:
      break;
    case LOAD_VIDEO:
      ffmpeg_close_and_free_video(commands->data.load_video->ffmpeg_video);
      free(commands->data.load_video);
      break;
    case SHOW_VIDEO_FRAME:
      free(commands->data.show_video_frame);
      break;
    case ADVANCE_VIDEO_FRAME:
      free(commands->data.advance_video_frame);
      break;
    case LOOP:
      free(commands->data.loop);
      break;
    case START_VOLUME:
      break;
    case WAIT_FOR_VOLUME:
      break;
    case STOP_ON_VOLUME_WITHOUT_CLEARING:
      break;
    case STOP_ON_VOLUME_AND_CLEAR:
      break;
    default:
      presenter_error("unknown or unimplemented command %u %p\n",
		      commands->type,
		      commands->data);
      break;
    }
    free(commands);
    commands = next;
  }
}

void free_sequence(struct renderer_sequence *sequence) {
  while (sequence!=NULL) {
    struct renderer_sequence *next = sequence->next;
    free_commands(sequence->commands);
    free(sequence);
    sequence = next;
  }
}

void free_renderer_task_args(struct renderer_task_args *renderer_task_args) {
  free(renderer_task_args->targets);
  free_sequence(renderer_task_args->sequence);
  free(renderer_task_args);
}

/* trigger thread */

/* can't use AnyModifier, it's too often unavailable */

void grab_key(Display *display, Window root, int keycode) {
  XGrabKey(display, keycode, 0, root, False, GrabModeAsync, GrabModeAsync);
  XGrabKey(display, keycode, ControlMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, ShiftMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, ControlMask|ShiftMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, LockMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, ControlMask|LockMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, ShiftMask|LockMask, root, False, GrabModeAsync,
	   GrabModeAsync);
  XGrabKey(display, keycode, ControlMask|ShiftMask|LockMask, root, False,
	   GrabModeAsync, GrabModeAsync);
}

void un_grab_key(Display *display, Window root, int keycode) {
  XUngrabKey(display, keycode, 0, root);
  XUngrabKey(display, keycode, ControlMask, root);
  XUngrabKey(display, keycode, ShiftMask, root);
  XUngrabKey(display, keycode, ControlMask|ShiftMask, root);
  XUngrabKey(display, keycode, LockMask, root);
  XUngrabKey(display, keycode, ControlMask|LockMask, root);
  XUngrabKey(display, keycode, ShiftMask|LockMask, root);
  XUngrabKey(display, keycode, ControlMask|ShiftMask|LockMask, root);
}

void send_key(Display *display, Window root, int keycode) {
  XEvent event;
  event.type = KeyPress;
  // serial
  // needs work: Most of this is really lying.
  event.xkey.send_event = True;
  event.xkey.display = display;
  event.xkey.root = root;
  event.xkey.subwindow = None;
  event.xkey.time = CurrentTime;
  event.xkey.x = 1;
  event.xkey.y = 1;
  event.xkey.x_root = 1;
  event.xkey.y_root = 1;
  event.xkey.state = 0;
  event.xkey.keycode = keycode;
  event.xkey.same_screen = True;
  Window window;
  int focus_state;
  XGetInputFocus(display, &window, &focus_state);
  event.xkey.window = window;
  XSendEvent(display, window, True, KeyPressMask, &event);
  XSync(display, False);
}

void *trigger_task(void *args) {
  int slices_per_tr = ((struct trigger_task_args *)args)->slices_per_tr;
  Display *display = XOpenDisplay("");
  if (display==NULL) presenter_error("display is NULL in trigger_task");
  Window root = DefaultRootWindow(display);
  XEvent event;
  int keycode5 = XKeysymToKeycode(display, XK_5);
  grab_key(display, root, keycode5);
  XSelectInput(display, root, KeyPressMask);
  while (TRUE) {
    XNextEvent(display, &event);
    if (event.type==KeyPress) {
      if (event.xkey.keycode==keycode5) {
        if (debugging) printf("keypress\n");
        first_trigger = TRUE;
        if (counter==slices_per_tr-1) {
          counter = 0;
          volume = TRUE;
          volumes++;
          if (debugging) printf("volume %u\n", volumes);
        }
	else counter++;
      }
      else {
        /* With simultaneous keypresses X delivers both of them to the
           listener, even if we didn't ask for them. So we send it back on its
	   way. */
        send_key(display, root, event.xkey.keycode);
      }
    }
  }
  // These will never actually be done since the task/thread never ends.
  un_grab_key(display, root, keycode5);
  XCloseDisplay(display);
  return NULL;
}

void start_trigger_thread(int slices_per_tr) {
  trigger_task_args.slices_per_tr = slices_per_tr;
  pthread_attr_t attributes;
  pthread_attr_init(&attributes);
  pthread_attr_setstacksize(&attributes, 10*1024*1024); /* hardwired 10MB */
  if (pthread_create
      (&trigger_thread, &attributes, trigger_task, &trigger_task_args)) {
    presenter_error("Can't start trigger thread");
  }
}
