/* recanim, Copyright © 2014-2026 Jamie Zawinski <jwz@jwz.org>
 * Record animation frames of the running screenhack.
 *
 * 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.
 *
 * When configured --with-record-animation, each screenhack takes a
 * "-record-animation DUR" arg that will cause it to run for exactly that
 * many frames, and write a "progname.mp4" file into the current directory.
 * "DUR" may be a number of frames, or a time specified as H:MM:SS, "N sec",
 * etc. at 30 FPS.
 *
 * The MP4 file will be written at 30 FPS, and it is therefore assumed that
 * each frame of the screenhack took 1/30th second to render and display,
 * regardless of the --delay option, or how long it actually took; so using
 * --delay 0 will speed up encoding.
 *
 * Likewise, any calls the saver makes to time(), double_time() or
 * gettimeofday() will be falsified to maintain this timing illusion.
 * Even if it takes 2 minutes to write out 30 seconds of video, the
 * screenhack will only see the wall clock advance by 30 seconds.
 *
 * This is how I generate the videos for the XScreenSaver YouTube playlist:
 * https://www.youtube.com/playlist?list=PLbe67PprBSpqM_-HU49fmIS8ncApw4i08
 */

#include "screenhackI.h"
#include "recanim.h"
#include "doubletime.h"

#ifndef HAVE_FFMPEG
# error HAVE_FFMPEG is required
#endif

#include "ffmpeg-out.h"

#if (__GNUC__ >= 4)	/* Ignore useless warnings generated by gtk.h */
# undef inline
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wstrict-prototypes"
# pragma GCC diagnostic ignored "-Wlong-long"
# pragma GCC diagnostic ignored "-Wvariadic-macros"
# pragma GCC diagnostic ignored "-Wpedantic"
#endif

#if (__GNUC__ >= 4)
# pragma GCC diagnostic pop
#endif

#ifdef USE_GL
# ifdef HAVE_JWXYZ
#  include "jwxyz.h"
# else /* !HAVE_JWXYZ -- real Xlib */
#  include <GL/glx.h>
#  include <GL/glu.h>
# endif /* !HAVE_JWXYZ */
# ifdef HAVE_JWZGLES
#  include "jwzgles.h"
# endif /* HAVE_JWZGLES */
#endif /* USE_GL */

#include <sys/stat.h>
#include <sys/types.h>

#undef gettimeofday  /* wrapped by recanim.h */
#undef time
#undef double_time
extern double double_time(void);

struct record_anim_state {
  Screen *screen;
  Window window;
  int frame_count;
  int target_frames;
  int fps;
  XWindowAttributes xgwa;
  char *title;
  int secs_elapsed;
  int fade_frames;
  double start_time;
  XImage *img;
# ifdef USE_GL
  char *data2;
# else  /* !USE_GL */
  Pixmap p;
  GC gc;
# endif /* !USE_GL */

  char *outfile;
  ffmpeg_out_state *ffst;
};


/* Some of the hacks set their timing based on the real-world wall clock,
   so to make the animations record at a sensible speed, we need to slow
   down that clock by discounting the time taken up by snapshotting and
   saving the frame.
 */
static double recanim_current_time = 0;

void
screenhack_record_anim_gettimeofday (struct timeval *tv
# ifdef GETTIMEOFDAY_TWO_ARGS
                                     , struct timezone *tz
# endif
                                     )
{
  /* recanim_current_time == 0 if we are not recording. */
  double now = (recanim_current_time ? recanim_current_time : double_time());
  tv->tv_sec  = (time_t) now;
  tv->tv_usec = 1000000 * (now - tv->tv_sec);
}

time_t
screenhack_record_anim_time (time_t *o)
{
  struct timeval tv;
# ifdef GETTIMEOFDAY_TWO_ARGS
  struct timezone tz;
# endif
  screenhack_record_anim_gettimeofday (&tv
# ifdef GETTIMEOFDAY_TWO_ARGS
                                       , &tz
# endif
                                       );
  if (o) *o = tv.tv_sec;
  return tv.tv_sec;
}


double
screenhack_record_anim_double_time (void)
{
  struct timeval tv;
# ifdef GETTIMEOFDAY_TWO_ARGS
  struct timezone tz;
# endif
  screenhack_record_anim_gettimeofday (&tv
# ifdef GETTIMEOFDAY_TWO_ARGS
                                       , &tz
# endif
                                       );
  return (tv.tv_sec + ((double) tv.tv_usec * 0.000001));
}


record_anim_state *
screenhack_record_anim_init (Screen *screen, Window window, int target_frames)
{
  Display *dpy = DisplayOfScreen (screen);
  record_anim_state *st;

# ifndef USE_GL
  XGCValues gcv;
# endif /* !USE_GL */

  if (target_frames <= 0) return 0;

  st = (record_anim_state *) calloc (1, sizeof(*st));

  st->fps = 30;
  st->screen = screen;
  st->window = window;
  st->target_frames = target_frames;
  st->start_time = double_time();
  st->frame_count = 0;
  st->fade_frames = st->fps * 1.5;

  recanim_current_time = st->start_time;

  if (st->fade_frames >= (st->target_frames / 2) - st->fps)
    st->fade_frames = 0;

  XGetWindowAttributes (dpy, st->window, &st->xgwa);

# ifdef USE_GL
  st->img = XCreateImage (dpy, st->xgwa.visual, 24,
                          ZPixmap, 0, 0, st->xgwa.width, st->xgwa.height,
                          32, 0);
  st->data2 = (char *) calloc (st->xgwa.width, st->xgwa.height * 3);

# else    /* !USE_GL */

  st->gc = XCreateGC (dpy, st->window, 0, &gcv);
  st->p = XCreatePixmap (dpy, st->window,
                         st->xgwa.width, st->xgwa.height, st->xgwa.depth);
  st->img = XCreateImage (dpy, st->xgwa.visual, st->xgwa.depth,
                          ZPixmap, 0, 0, st->xgwa.width, st->xgwa.height,
                          8, 0);
# endif /* !USE_GL */

  st->img->data = (char *) calloc (st->img->height, st->img->bytes_per_line);


# ifndef HAVE_JWXYZ
  XFetchName (dpy, st->window, &st->title);
  {
    char *s = strchr(st->title, ':');
    if (s) *s = 0;
  }
# endif /* !HAVE_JWXYZ */

  {
    char fn[1024];
    struct stat s;

    const char *soundtrack = 0;
#   define ST "images/drives-200.mp3"
    soundtrack = ST;
    if (stat (soundtrack, &s)) soundtrack = 0;
    if (! soundtrack) soundtrack = "../" ST;
    if (stat (soundtrack, &s)) soundtrack = 0;
    if (! soundtrack) soundtrack = "../../" ST;
    if (stat (soundtrack, &s)) soundtrack = 0;

    sprintf (fn, "%s.%s", progname, "mp4");
    unlink (fn);

    st->outfile = strdup (fn);
    st->ffst = ffmpeg_out_init (st->outfile, soundtrack,
                                st->xgwa.width, st->xgwa.height,
                                3, False);
  }

  return st;
}


/* Fade to black. Assumes data is 3-byte packed.
 */
static void
fade_frame (record_anim_state *st, unsigned char *data, double ratio)
{
  int x, y, i;
  int w = st->xgwa.width;
  int h = st->xgwa.height;
  unsigned char *s = data;
  for (y = 0; y < h; y++)
    for (x = 0; x < w; x++)
      for (i = 0; i < 3; i++)
        *s++ *= ratio;
}


void
screenhack_record_anim (record_anim_state *st)
{
  int obpl    = st->img->bytes_per_line;
  char *odata = st->img->data;

# ifndef USE_GL

  Display *dpy = DisplayOfScreen (st->screen);

  /* Under XQuartz we can't just do XGetImage on the Window, we have to
     go through an intermediate Pixmap first.  I don't understand why.
     Also, the fucking resize handle shows up as black.  God dammit.
     A workaround for that is to temporarily remove /opt/X11/bin/quartz-wm
   */
  XCopyArea (dpy, st->window, st->p, st->gc, 0, 0,
             st->xgwa.width, st->xgwa.height, 0, 0);
  XGetSubImage (dpy, st->p, 0, 0, st->xgwa.width, st->xgwa.height,
                ~0L, ZPixmap, st->img, 0, 0);

  /* Convert BGRA to BGR */
  if (st->img->bytes_per_line == st->img->width * 4)
    {
      const char *in = st->img->data;
      char *out = st->img->data;
      int x, y;
      int w = st->img->width;
      int h = st->img->height;
      for (y = 0; y < h; y++)
        {
          const char *in2 = in;
          for (x = 0; x < w; x++)
            {
              *out++ = in2[0];
              *out++ = in2[1];
              *out++ = in2[2];
              in2 += 4;
            }
          in += st->img->bytes_per_line;
        }
      st->img->bytes_per_line = w * 3;
    }
  else if (st->img->bytes_per_line != st->img->width * 3)
    abort();

# else  /* USE_GL */

  int y;

# ifdef HAVE_JWZGLES
#  undef glReadPixels /* Kludge -- unimplemented in the GLES compat layer */
# endif

  /* First OpenGL frame tends to be random data like a shot of my desktop,
     since it is the front buffer when we were drawing in the back buffer.
     Leave it black. */
  /* glDrawBuffer (GL_BACK); */
  if (st->frame_count != 0)
    glReadPixels (0, 0, st->xgwa.width, st->xgwa.height,
                  GL_BGR, GL_UNSIGNED_BYTE, st->img->data);

  /* Flip vertically */
  {
    int bpl = st->xgwa.width * 3;
    for (y = 0; y < st->xgwa.height; y++)
      memcpy (st->data2     + bpl * y,
              st->img->data + bpl * (st->xgwa.height - y - 1),
              bpl);
    st->img->data = st->data2;
    st->img->bytes_per_line = bpl;
  }

# endif /* USE_GL */

  if (st->frame_count < st->fade_frames)
    fade_frame (st, (unsigned char *) st->img->data,
                (double) st->frame_count / st->fade_frames);
  else if (st->frame_count >= st->target_frames - st->fade_frames)
    fade_frame (st, (unsigned char *) st->img->data,
                (double) (st->target_frames - st->frame_count - 1) /
                st->fade_frames);

  ffmpeg_out_add_frame (st->ffst, st->img);
  st->img->bytes_per_line = obpl;
  st->img->data = odata;

# ifndef HAVE_JWXYZ		/* Put percent done in window title */
  {
    double now     = double_time();
    double dur     = st->target_frames / (double) st->fps;
    double ratio   = (st->frame_count + 1) / (double) st->target_frames;
    double encoded = dur * ratio;
    double elapsed = now - st->start_time;
    double rate    = encoded / elapsed;
    double remain  = (elapsed / ratio) - elapsed;

    if (st->title && st->secs_elapsed != (int) elapsed)
      {
        Display *dpy = DisplayOfScreen (st->screen);
        char *t2 = (char *) malloc (strlen(st->title) + 100);
        sprintf (t2,
                 "%s: encoded"
                 " %d:%02d:%02d of"
                 " %d:%02d:%02d at"
                 " %d%% speed;"
                 " %d:%02d:%02d elapsed,"
                 " %d:%02d:%02d remaining",
                 st->title,

                 ((int)  encoded) / (60*60),
                 (((int) encoded) / 60) % 60,
                 ((int)  encoded) % 60,

                 ((int)  dur) / (60*60),
                 (((int) dur) / 60) % 60,
                 ((int)  dur) % 60,

                 (int) (100 * rate),

                 ((int)  elapsed) / (60*60),
                 (((int) elapsed) / 60) % 60,
                 ((int)  elapsed) % 60,

                 ((int)  remain) / (60*60),
                 (((int) remain) / 60) % 60,
                 ((int)  remain) % 60
                 );
        XStoreName (dpy, st->window, t2);
        XSync (dpy, 0);
        free (t2);
        st->secs_elapsed = elapsed;
      }
  }
# endif /* !HAVE_JWXYZ */

  if (++st->frame_count >= st->target_frames)
    screenhack_record_anim_free (st);

  /* Report to screenhack that each frame took exactly 1/30th second. */
  recanim_current_time += 1.0 / st->fps;
}


void
screenhack_record_anim_free (record_anim_state *st)
{
# ifndef USE_GL
  Display *dpy = DisplayOfScreen (st->screen);
# endif /* !USE_GL */
  struct stat s;
  double real_end     = double_time();
  double virt_end     = screenhack_record_anim_double_time();
  double real_elapsed = real_end - st->start_time;
  double virt_elapsed = virt_end - st->start_time;
  double video_dur    = st->frame_count / (double) st->fps;

# ifdef USE_GL
  free (st->data2);
# else  /* !USE_GL */
  free (st->img->data);
  st->img->data = 0;
  XDestroyImage (st->img);
  XFreeGC (dpy, st->gc);
  XFreePixmap (dpy, st->p);
# endif /* !USE_GL */

  ffmpeg_out_close (st->ffst);

  if (stat (st->outfile, &s))
    {
      fprintf (stderr, "%s: error writing %s\n", progname, st->outfile);
      exit (1);
    }

# define FMT "%-24.24s %d:%02d:%02d  %5.1f fps"
  fprintf (stderr, FMT ", %d frames, %.1f MB\n", st->outfile,
           (((int)video_dur)/60/60),
           (((int)video_dur)/60)%60,
           ((int)video_dur)%60,
           (double) st->fps,
           st->frame_count,
           s.st_size / (float) (1024 * 1024));
  fprintf (stderr, FMT "\n", "elapsed virtual",
           (((int)virt_elapsed)/60/60),
           (((int)virt_elapsed)/60)%60,
           ((int)virt_elapsed)%60,
           st->frame_count / virt_elapsed);
  fprintf (stderr, FMT "\n", "elapsed actual",
           (((int)real_elapsed)/60/60),
           (((int)real_elapsed)/60)%60,
           ((int)real_elapsed)%60,
           st->frame_count / real_elapsed);

  if (st->title)
    free (st->title);
  free (st->outfile);
  free (st);
  exit (0);
}
