/* xscreensaver, Copyright (c) 2003 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.
 *
 * fontglide -- reads text from a subprocess and puts it on the screen using
 * large characters that glide in from the edges, assemble, then disperse.
 * Requires a system with scalable fonts.  (X's font handing sucks.  A lot.)
 */

#include <math.h>
#include "screenhack.h"
#include <X11/Intrinsic.h>

#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
#include "xdbe.h"
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */

extern XtAppContext app;


typedef struct {
  char *text;
  int x, y, width, height;
  int ascent, lbearing, rbearing;

  int nticks, tick;
  int start_x,  start_y;
  int target_x, target_y;
  Pixmap pixmap, mask;
} word;


typedef struct {
  int id;
  XColor fg;
  XColor bg;
  Bool dark_p;
  Bool move_chars_p;
  int width;

  char *font_name;
  XFontStruct *font;

  GC fg_gc;

  int nwords;
  word **words;

  enum { IN, PAUSE, OUT } anim_state;
  enum { LEFT, CENTER, RIGHT } alignment;
  int pause_tick;

} sentence;


typedef struct {
  Display *dpy;
  Window window;
  XWindowAttributes xgwa;

  Pixmap b, ba;	/* double-buffer to reduce flicker */
  GC bg_gc;

#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
  XdbeBackBuffer backb;
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */

  Bool dbuf;            /* Whether we're using double buffering. */
  Bool dbeclear_p;      /* ? */

  int border_width;     /* size of the font outline */
  char *charset;        /* registry and encoding for font lookups */
  double speed;		/* frame rate multiplier */
  double linger;	/* multiplier for how long to leave words on screen */
  Bool trails_p;
  Bool debug_p;
  enum { PAGE, SCROLL } mode;

  char *font_override;  /* if -font was specified on the cmd line */

  FILE *pipe;
  XtInputId pipe_id;
  Time subproc_relaunch_delay;
  Bool input_available_p;

  char buf [40];	/* this only needs to be as big as one "word". */
  int buf_tail;

  int nsentences;
  sentence **sentences;
  Bool spawn_p;		/* whether it is time to create a new sentence */
  int latest_sentence;

} state;


static void launch_text_generator (state *);
static void drain_input (state *s);


/* Finds the set of scalable fonts on the system; picks one;
   and loads that font in a random pixel size.
   Returns False if something went wrong.
 */
static Bool
pick_font_1 (state *s, sentence *se)
{
  char pattern[1024];
  char **names = 0;
  char **names2 = 0;
  XFontStruct *info = 0;
  int count = 0, count2 = 0;
  int i;
  Bool ok = False;

  if (se->font)
    {
      XFreeFont (s->dpy, se->font);
      free (se->font_name);
      se->font = 0;
      se->font_name = 0;
    }

  if (s->font_override)
    sprintf (pattern, "%.200s", s->font_override);
  else
    sprintf (pattern, "-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s-%s",
             "*",         /* foundry */
             "*",         /* family */
             "*",         /* weight */
             "*",         /* slant */
             "*",         /* swidth */
             "*",         /* adstyle */
             "0",         /* pixel size */
             "0",         /* point size */
             "0",         /* resolution x */
             "0",         /* resolution y */
             "p",         /* spacing */
             "0",         /* avg width */
             s->charset); /* registry + encoding */

  names = XListFonts (s->dpy, pattern, 1000, &count);

  if (count <= 0)
    {
      if (s->font_override)
        fprintf (stderr, "%s: -font option bogus: %s\n", progname, pattern);
      else
        fprintf (stderr, "%s: no scalable fonts found!  (pattern: %s)\n",
                 progname, pattern);
      exit (1);
    }

  i = random() % count;

  names2 = XListFontsWithInfo (s->dpy, names[i], 1000, &count2, &info);
  if (count2 <= 0)
    {
      fprintf (stderr, "%s: pattern %s\n"
                "     gave unusable %s\n\n",
               progname, pattern, names[i]);
      goto FAIL;
    }

  {
    XFontStruct *font = &info[0];
    unsigned long value = 0;
    char *foundry=0, *family=0, *weight=0, *slant=0, *setwidth=0, *add_style=0;
    unsigned long pixel=0, point=0, res_x=0, res_y=0;
    char *spacing=0;
    unsigned long avg_width=0;
    char *registry=0, *encoding=0;
    Atom a;
    char *bogus = "\"?\"";

# define STR(ATOM,VAR)					\
  bogus = (ATOM);					\
  a = XInternAtom (s->dpy, (ATOM), False);		\
  if (XGetFontProperty (font, a, &value))		\
    VAR = XGetAtomName (s->dpy, value);			\
  else							\
    goto FAIL2

# define INT(ATOM,VAR)					\
  bogus = (ATOM);					\
  a = XInternAtom (s->dpy, (ATOM), False);		\
  if (!XGetFontProperty (font, a, &VAR) ||		\
      VAR > 9999)					\
    goto FAIL2

    STR ("FOUNDRY",          foundry);
    STR ("FAMILY_NAME",      family);
    STR ("WEIGHT_NAME",      weight);
    STR ("SLANT",            slant);
    STR ("SETWIDTH_NAME",    setwidth);
    STR ("ADD_STYLE_NAME",   add_style);
    INT ("PIXEL_SIZE",       pixel);
    INT ("POINT_SIZE",       point);
    INT ("RESOLUTION_X",     res_x);
    INT ("RESOLUTION_Y",     res_y);
    STR ("SPACING",          spacing);
    INT ("AVERAGE_WIDTH",    avg_width);
    STR ("CHARSET_REGISTRY", registry);
    STR ("CHARSET_ENCODING", encoding);

#undef INT
#undef STR

    {
      double scale = s->xgwa.height / 1024.0;  /* shrink for small windows */
      int min, max, r;

      min = scale * 24;
      max = scale * 260;

      if (min < 10) min = 10;
      if (max < 30) max = 30;

      r = ((max-min)/3)+1;

      pixel = min + ((random() % r) + (random() % r) + (random() % r));

      if (s->mode == SCROLL)  /* scroll mode likes bigger fonts */
        pixel *= 1.5;
    }

#if 0
    /* Occasionally change the aspect ratio of the font, by increasing
       either the X or Y resolution (while leaving the other alone.)

       #### Looks like this trick doesn't really work that well: the
            metrics of the individual characters are ok, but the
            overall font ascent comes out wrong (unscaled.)
     */
    if (! (random() % 8))
      {
        double n = 2.5 / 3;
        double scale = 1 + (frand(n) + frand(n) + frand(n));
        if (random() % 2)
          res_x *= scale;
        else
          res_y *= scale;
      }
# endif

    sprintf (pattern,
             "-%s-%s-%s-%s-%s-%s-%ld-%s-%ld-%ld-%s-%s-%s-%s",
             foundry, family, weight, slant, setwidth, add_style,
             pixel, "*", /* point, */
             res_x, res_y, spacing,
             "*", /* avg_width */
             registry, encoding);
    ok = True;

  FAIL2:
    if (!ok)
      fprintf (stderr, "%s: font has bogus %s property: %s\n",
               progname, bogus, names[i]);

    if (foundry)   XFree (foundry);
    if (family)    XFree (family);
    if (weight)    XFree (weight);
    if (slant)     XFree (slant);
    if (setwidth)  XFree (setwidth);
    if (add_style) XFree (add_style);
    if (spacing)   XFree (spacing);
    if (registry)  XFree (registry);
    if (encoding)  XFree (encoding);
  }

 FAIL: 

  XFreeFontInfo (names2, info, count2);
  XFreeFontNames (names);

  if (! ok) return False;

  se->font = XLoadQueryFont (s->dpy, pattern);
  if (! se->font)
    {
      fprintf (stderr, "%s: unable to load font %s\n",
               progname, pattern);
      return False;
    }

  if (se->font->min_bounds.width == se->font->max_bounds.width &&
      !s->font_override)
    {
      /* This is to weed out
         "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
         "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1".
         We asked for only proportional fonts, but this fixed-width font
         shows up anyway -- but it has goofy metrics (see below) so it
         looks terrible anyway.
       */
      if (s->debug_p)
        fprintf (stderr,
                 "%s: skipping bogus monospace non-charcell font: %s\n",
                 progname, pattern);
      return False;
    }

  if (s->debug_p) 
    fprintf(stderr, "%s: %s\n", progname, pattern);

  se->font_name = strdup (pattern);
  XSetFont (s->dpy, se->fg_gc, se->font->fid);
  return True;
}


/* Finds the set of scalable fonts on the system; picks one;
   and loads that font in a random pixel size.
 */
static void
pick_font (state *s, sentence *se)
{
  int i;
  for (i = 0; i < 20; i++)
    if (pick_font_1 (s, se))
      return;
  fprintf (stderr, "%s: too many failures: giving up!\n", progname);
  exit (1);
}


static char *unread_word_text = 0;

/* Returns a newly-allocated string with one word in it, or NULL if there
   is no complete word available.
 */
static char *
get_word_text (state *s)
{
  char *start = s->buf;
  char *end;
  char *result = 0;
  int lfs = 0;

  if (unread_word_text)
    {
      char *s = unread_word_text;
      unread_word_text = 0;
      return s;
    }

  /* Skip over whitespace at the beginning of the buffer,
     and count up how many linebreaks we see while doing so.
   */
  while (*start &&
         (*start == ' ' ||
          *start == '\t' ||
          *start == '\r' ||
          *start == '\n'))
    {
      if (*start == '\n' || (*start == '\r' && start[1] != '\n'))
        lfs++;
      start++;
    }

  end = start;

  /* If we saw a blank line, then return NULL (treat it as a temporary "eof",
     to trigger a sentence break here.) */
  if (lfs >= 2)
    goto DONE;

  /* Skip forward to the end of this word (find next whitespace.) */
  while (*end &&
         (! (*end == ' ' ||
             *end == '\t' ||
             *end == '\r' ||
             *end == '\n')))
    end++;

  /* If we have a word, allocate a string for it */
  if (end > start)
    {
      result = malloc ((end - start) + 1);
      strncpy (result, start, (end-start));
      result [end-start] = 0;
    }

 DONE:

  /* Make room in the buffer by compressing out any bytes we've processed.
   */
  if (end > s->buf)
    {
      int n = end - s->buf;
      memmove (s->buf, end, sizeof(s->buf) - n);
      s->buf_tail -= n;
    }

  /* See if there is more to be read, now that there's room in the buffer. */
  drain_input (s);

  return result;
}


/* Gets some random text, and creates a "word" object from it.
 */
static word *
new_word (state *s, sentence *se, char *txt, Bool alloc_p)
{
  word *w;
  XCharStruct overall;
  int dir, ascent, descent;
  int bw = s->border_width;

  if (!txt)
    return 0;

  w = (word *) calloc (1, sizeof(*w));
  XTextExtents (se->font, txt, strlen(txt), &dir, &ascent, &descent, &overall);

  w->width    = overall.rbearing - overall.lbearing + bw + bw;
  w->height   = overall.ascent   + overall.descent  + bw + bw;
  w->ascent   = overall.ascent   + bw;
  w->lbearing = overall.lbearing - bw;
  w->rbearing = overall.width    + bw;

# if 0
  /* The metrics on some fonts are strange -- e.g.,
     "-urw-nimbus mono l-medium-o-normal--*-*-*-*-p-*-iso8859-1" and
     "-urw-courier-medium-r-normal--*-*-*-*-p-*-iso8859-1" both have
     an rbearing so wide that it looks like there are two spaces after
     each letter.  If this character says it has an rbearing that is to
     the right of its ink, ignore that.

     #### Of course, this hack only helps when we're in `move_chars_p' mode
          and drawing a char at a time -- when we draw the whole word at once,
          XDrawString believes the bogus metrics and spaces the font out
          crazily anyway.

     Sigh, this causes some text to mis-render in, e.g.,
     "-adobe-utopia-medium-i-normal--114-*-100-100-p-*-iso8859-1"
     (in "ux", we need the rbearing on "r" or we get too much overlap.)
   */
  if (w->rbearing > w->width)
    w->rbearing = w->width;
# endif /* 0 */

  if (s->mode == SCROLL && !alloc_p) abort();

  if (alloc_p)
    {
      int i, j;
      XGCValues gcv;
      GC gc0, gc1;

      w->pixmap = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);
      w->mask   = XCreatePixmap (s->dpy, s->b, w->width, w->height, 1L);

      gcv.font = se->font->fid;
      gcv.foreground = 0L;
      gcv.background = 1L;
      gc0 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
                       &gcv);
      gcv.foreground = 1L;
      gcv.background = 0L;
      gc1 = XCreateGC (s->dpy, w->pixmap, GCFont|GCForeground|GCBackground,
                       &gcv);

      XFillRectangle (s->dpy, w->mask,   gc0, 0, 0, w->width, w->height);
      XFillRectangle (s->dpy, w->pixmap, gc0, 0, 0, w->width, w->height);

      if (s->debug_p)
        {
          /* bounding box (behind the characters) */
          XDrawRectangle (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
                          0, 0, w->width-1, w->height-1);
          XDrawRectangle (s->dpy, w->mask,   gc1,
                          0, 0, w->width-1, w->height-1);
        }

      /* Draw foreground text */
      XDrawString (s->dpy, w->pixmap, gc1, -w->lbearing, w->ascent,
                   txt, strlen(txt));

      /* Cheesy hack to draw a border */
      /* (I should be able to do this in i*2 time instead of i*i time,
         but I can't get it right, so fuck it.) */
      XSetFunction (s->dpy, gc1, GXor);
      for (i = -bw; i <= bw; i++)
        for (j = -bw; j <= bw; j++)
          XCopyArea (s->dpy, w->pixmap, w->mask, gc1,
                     0, 0, w->width, w->height,
                     i, j);

      if (s->debug_p)
        {
          XSetFunction (s->dpy, gc1, GXset);
          if (w->ascent != w->height)
            {
              /* baseline (on top of the characters) */
              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
                         0, w->ascent, w->width-1, w->ascent);
              XDrawLine (s->dpy, w->mask,   gc1,
                         0, w->ascent, w->width-1, w->ascent);
            }

          if (w->lbearing != 0)
            {
              /* left edge of charcell */
              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
                         w->lbearing, 0, w->lbearing, w->height-1);
              XDrawLine (s->dpy, w->mask,   gc1,
                         w->lbearing, 0, w->lbearing, w->height-1);
            }

          if (w->rbearing != w->width)
            {
              /* right edge of charcell */
              XDrawLine (s->dpy, w->pixmap, (se->dark_p ? gc0 : gc1),
                         w->rbearing, 0, w->rbearing, w->height-1);
              XDrawLine (s->dpy, w->mask,   gc1,
                         w->rbearing, 0, w->rbearing, w->height-1);
            }
        }

      XFreeGC (s->dpy, gc0);
      XFreeGC (s->dpy, gc1);
    }

  w->text = txt;
  return w;
}


static void
free_word (state *s, word *w)
{
  if (w->text)   free (w->text);
  if (w->pixmap) XFreePixmap (s->dpy, w->pixmap);
  if (w->mask)   XFreePixmap (s->dpy, w->mask);
}


static sentence *
new_sentence (state *s)
{
  static int id = 0;
  XGCValues gcv;
  sentence *se = (sentence *) calloc (1, sizeof (*se));
  se->fg_gc = XCreateGC (s->dpy, s->b, 0, &gcv);
  se->anim_state = IN;
  se->id = ++id;
  return se;
}


static void
free_sentence (state *s, sentence *se)
{
  int i;
  for (i = 0; i < se->nwords; i++)
    free_word (s, se->words[i]);
  if (se->words) free (se->words);

  if (se->fg.flags)
    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
  if (se->bg.flags)
    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);

  if (se->font_name) free (se->font_name);
  if (se->font) XFreeFont (s->dpy, se->font);
  if (se->fg_gc) XFreeGC (s->dpy, se->fg_gc);

  free (se);
}


/* free the word, and put its text back at the front of the input queue,
   to be read next time. */
static void
unread_word (state *s, word *w)
{
  if (unread_word_text)
    abort();
  unread_word_text = w->text;
  w->text = 0;
  free_word (s, w);
}


/* Divide each of the words in the sentence into one character words,
   without changing the positions of those characters.
 */
static void
split_words (state *s, sentence *se)
{
  word **words2;
  int nwords2 = 0;
  int i, j;
  for (i = 0; i < se->nwords; i++)
    nwords2 += strlen (se->words[i]->text);

  words2 = (word **) calloc (nwords2, sizeof(*words2));

  for (i = 0, j = 0; i < se->nwords; i++)
    {
      word *ow = se->words[i];
      int L = strlen (ow->text);
      int k;

      int x  = ow->x;
      int y  = ow->y;
      int sx = ow->start_x;
      int sy = ow->start_y;
      int tx = ow->target_x;
      int ty = ow->target_y;

      for (k = 0; k < L; k++)
        {
          char *t2 = malloc (2);
          word *w2;
          int xoff, yoff;

          t2[0] = ow->text[k];
          t2[1] = 0;
          w2 = new_word (s, se, t2, True);
          words2[j++] = w2;

          xoff = (w2->lbearing - ow->lbearing);
          yoff = (ow->ascent - w2->ascent);

          w2->x        = x  + xoff;
          w2->y        = y  + yoff;
          w2->start_x  = sx + xoff;
          w2->start_y  = sy + yoff;
          w2->target_x = tx + xoff;
          w2->target_y = ty + yoff;

          x  += w2->rbearing;
          sx += w2->rbearing;
          tx += w2->rbearing;
        }

      free_word (s, ow);
      se->words[i] = 0;
    }
  free (se->words);

  se->words = words2;
  se->nwords = nwords2;
}


/* Set the source or destination position of the words to be somewhere
   off screen.
 */
static void
scatter_sentence (state *s, sentence *se)
{
  int i = 0;
  int off = 100;

  int flock_p = ((random() % 4) == 0);
  int mode = (flock_p ? (random() % 12) : 0);

  for (i = 0; i < se->nwords; i++)
    {
      word *w = se->words[i];
      int x, y;
      int r = (flock_p ? mode : (random() % 4));
      switch (r)
        {
          /* random positions on the edges */

        case 0:
          x = -off - w->width;
          y = random() % s->xgwa.height;
          break;
        case 1:
          x = off + s->xgwa.width;
          y = random() % s->xgwa.height;
          break;
        case 2:
          x = random() % s->xgwa.width;
          y = -off - w->height;
          break;
        case 3:
          x = random() % s->xgwa.width;
          y = off + s->xgwa.height;
          break;

          /* straight towards the edges */

        case 4:
          x = -off - w->width;
          y = w->target_y;
          break;
        case 5:
          x = off + s->xgwa.width;
          y = w->target_y;
          break;
        case 6:
          x = w->target_x;
          y = -off - w->height;
          break;
        case 7:
          x = w->target_x;
          y = off + s->xgwa.height;
          break;

          /* corners */

        case 8:
          x = -off - w->width;
          y = -off - w->height;
          break;
        case 9:
          x = -off - w->width;
          y =  off + s->xgwa.height;
          break;
        case 10:
          x =  off + s->xgwa.width;
          y =  off + s->xgwa.height;
          break;
        case 11:
          x =  off + s->xgwa.width;
          y = -off - w->height;
          break;

        default:
          abort();
          break;
        }

      if (se->anim_state == IN)
        {
          w->start_x = x;
          w->start_y = y;
        }
      else
        {
          w->start_x = w->x;
          w->start_y = w->y;
          w->target_x = x;
          w->target_y = y;
        }

      w->nticks = ((100 + ((random() % 140) +
                           (random() % 140) +
                           (random() % 140)))
                   / s->speed);
      if (w->nticks < 2)
        w->nticks = 2;
      w->tick = 0;
    }
}


/* Set the source position of the words to be off the right side,
   and the destination to be off the left side.
 */
static void
aim_sentence (state *s, sentence *se)
{
  int i = 0;
  int nticks;
  int yoff = 0;

  if (se->nwords <= 0) abort();

  /* Have the sentence shift up or down a little bit; not too far, and
     never let it fall off the top or bottom of the screen before its
     last character has reached the left edge.
   */
  for (i = 0; i < 10; i++)
    {
      int ty = random() % (s->xgwa.height - se->words[0]->ascent);
      yoff = ty - se->words[0]->target_y;
      if (yoff < s->xgwa.height/3)  /* this one is ok */
        break;
    }

  for (i = 0; i < se->nwords; i++)
    {
      word *w = se->words[i];
      w->start_x   = w->target_x + s->xgwa.width;
      w->target_x -= se->width;
      w->start_y   = w->target_y;
      w->target_y += yoff;
    }

  nticks = ((se->words[0]->start_x - se->words[0]->target_x)
            / (s->speed * 10));
  nticks *= (frand(0.9) + frand(0.9) + frand(0.9));

  if (nticks < 2)
    nticks = 2;

  for (i = 0; i < se->nwords; i++)
    {
      word *w = se->words[i];
      w->nticks = nticks;
      w->tick = 0;
    }
}


/* Randomize the order of the words in the list (since that changes
   which ones are "on top".)
 */
static void
shuffle_words (state *s, sentence *se)
{
  int i;
  for (i = 0; i < se->nwords-1; i++)
    {
      int j = i + (random() % (se->nwords - i));
      word *swap = se->words[i];
      se->words[i] = se->words[j];
      se->words[j] = swap;
    }
}


/* qsort comparitor */
static int
cmp_sentences (const void *aa, const void *bb)
{
  const sentence *a = *(sentence **) aa;
  const sentence *b = *(sentence **) bb;
  return ((a ? a->id : 999999) - (b ? b->id : 999999));
}


/* Sort the sentences by id, so that sentences added later are on top.
 */
static void
sort_sentences (state *s)
{
  qsort (s->sentences, s->nsentences, sizeof(*s->sentences), cmp_sentences);
}


/* Re-pick the colors of the text and border
 */
static void
recolor (state *s, sentence *se)
{
  if (se->fg.flags)
    XFreeColors (s->dpy, s->xgwa.colormap, &se->fg.pixel, 1, 0);
  if (se->bg.flags)
    XFreeColors (s->dpy, s->xgwa.colormap, &se->bg.pixel, 1, 0);

  se->fg.flags  = DoRed|DoGreen|DoBlue;
  se->bg.flags  = DoRed|DoGreen|DoBlue;

  switch (random() % 2)
    {
    case 0:   /* bright fg, dim bg */
      se->fg.red    = (random() % 0x8888) + 0x8888;
      se->fg.green  = (random() % 0x8888) + 0x8888;
      se->fg.blue   = (random() % 0x8888) + 0x8888;
      se->bg.red    = (random() % 0x5555);
      se->bg.green  = (random() % 0x5555);
      se->bg.blue   = (random() % 0x5555);
      break;

    case 1:   /* bright bg, dim fg */
      se->fg.red    = (random() % 0x4444);
      se->fg.green  = (random() % 0x4444);
      se->fg.blue   = (random() % 0x4444);
      se->bg.red    = (random() % 0x4444) + 0xCCCC;
      se->bg.green  = (random() % 0x4444) + 0xCCCC;
      se->bg.blue   = (random() % 0x4444) + 0xCCCC;
      break;

    default:
      abort();
      break;
    }

  if (s->debug_p)
    se->dark_p = (se->fg.red*2 + se->fg.green*3 + se->fg.blue <
                  se->bg.red*2 + se->bg.green*3 + se->bg.blue);

  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->fg))
    XSetForeground (s->dpy, se->fg_gc, se->fg.pixel);
  if (XAllocColor (s->dpy, s->xgwa.colormap, &se->bg))
    XSetBackground (s->dpy, se->fg_gc, se->bg.pixel);
}


static void
align_line (state *s, sentence *se, int line_start, int x, int right)
{
  int off, j;
  switch (se->alignment)
    {
    case LEFT:   off = 0;               break;
    case CENTER: off = (right - x) / 2; break;
    case RIGHT:  off = (right - x);     break;
    default:     abort();               break;
    }

  if (off != 0)
    for (j = line_start; j < se->nwords; j++)
      se->words[j]->target_x += off;
}


/* Fill the sentence with new words: in "page" mode, fills the page
   with text; in "scroll" mode, just makes one long horizontal sentence.
   The sentence might have *no* words in it, if no text is currently
   available.
 */
static void
populate_sentence (state *s, sentence *se)
{
  int i = 0;
  int left, right, top, x, y;
  int space = 0;
  int line_start = 0;
  Bool done = False;

  int array_size = 100;

  se->move_chars_p = (s->mode == SCROLL ? False :
                      (random() % 3) ? False : True);
  se->alignment = (random() % 3);

  recolor (s, se);

  if (se->words)
    {
      for (i = 0; i < se->nwords; i++)
        free_word (s, se->words[i]);
      free (se->words);
    }

  se->words = (word **) calloc (array_size, sizeof(*se->words));
  se->nwords = 0;

  switch (s->mode)
    {
    case PAGE:
      left  = random() % (s->xgwa.width / 3);
      right = s->xgwa.width - (random() % (s->xgwa.width / 3));
      top = random() % (s->xgwa.height * 2 / 3);
      break;
    case SCROLL:
      left = 0;
      right = s->xgwa.width;
      top = random() % s->xgwa.height;
      break;
    default:
      abort();
      break;
    }

  x = left;
  y = top;

  while (!done)
    {
      char *txt = get_word_text (s);
      word *w;
      if (!txt)
        {
          if (se->nwords == 0)
            return;		/* If the stream is empty, bail. */
          else
            break;		/* If EOF after some words, end of sentence. */
        }

      if (! se->font)           /* Got a word: need a font now */
        {
          pick_font (s, se);
          if (y < se->font->ascent)
            y += se->font->ascent;
          space = XTextWidth (se->font, " ", 1);
        }

      w = new_word (s, se, txt, !se->move_chars_p);

      /* If we have a few words, let punctuation terminate the sentence:
         stop gathering more words if the last word ends in a period, etc. */
      if (se->nwords >= 4)
        {
          char c = w->text[strlen(w->text)-1];
          if (c == '.' || c == '?' || c == '!')
            done = True;
        }

      /* If the sentence is kind of long already, terminate at commas, etc. */
      if (se->nwords >= 12)
        {
          char c = w->text[strlen(w->text)-1];
          if (c == ',' || c == ';' || c == ':' || c == '-' ||
              c == ')' || c == ']' || c == '}')
            done = True;
        }

      if (se->nwords >= 25)  /* ok that's just about enough out of you */
        done = True;

      if (s->mode == PAGE &&
          x + w->rbearing > right)			/* wrap line */
        {
          align_line (s, se, line_start, x, right);
          line_start = se->nwords;

          x = left;
          y += se->font->ascent;

          /* If we're close to the bottom of the screen, stop, and 
             unread the current word.  (But not if this is the first
             word, otherwise we might just get stuck on it.)
           */
          if (se->nwords > 0 &&
              y + se->font->ascent > s->xgwa.height)
            {
              unread_word (s, w);
              done = True;
              break;
            }
        }

      w->target_x = x + w->lbearing;
      w->target_y = y - w->ascent;

      x += w->rbearing + space;
      se->width = x;

      if (se->nwords >= (array_size - 1))
        {
          array_size += 100;
          se->words = (word **) realloc (se->words,
                                         array_size * sizeof(*se->words));
          if (!se->words)
            {
              fprintf (stderr, "%s: out of memory (%d words)\n",
                       progname, array_size);
              exit (1);
            }
        }

      se->words[se->nwords++] = w;
    }

  se->width -= space;

  switch (s->mode)
    {
    case PAGE:
      align_line (s, se, line_start, x, right);
      if (se->move_chars_p)
        split_words (s, se);
      scatter_sentence (s, se);
      shuffle_words (s, se);
      break;
    case SCROLL:
      aim_sentence (s, se);
      break;
    default:
      abort();
      break;
    }

# ifdef DEBUG
  if (s->debug_p)
    {
      fprintf (stderr, "%s: sentence %d:", progname, se->id);
      for (i = 0; i < se->nwords; i++)
        fprintf (stderr, " %s", se->words[i]->text);
      fprintf (stderr, "\n");
    }
# endif
}


/* Render a single word object to the screen.
 */
static void
draw_word (state *s, sentence *se, word *w)
{
  if (! w->pixmap) return;

  if (w->x + w->width < 0 ||
      w->y + w->height < 0 ||
      w->x > s->xgwa.width ||
      w->y > s->xgwa.height)
    return;

  XSetClipMask (s->dpy, se->fg_gc, w->mask);
  XSetClipOrigin (s->dpy, se->fg_gc, w->x, w->y);
  XCopyPlane (s->dpy, w->pixmap, s->b, se->fg_gc,
              0, 0, w->width, w->height,
              w->x, w->y,
              1L);
}


/* If there is room for more sentences, add one.
 */
static void
more_sentences (state *s)
{
  int i;
  Bool any = False;
  for (i = 0; i < s->nsentences; i++)
    {
      sentence *se = s->sentences[i];
      if (! se)
        {
          se = new_sentence (s);
          populate_sentence (s, se);
          if (se->nwords > 0)
            s->spawn_p = False, any = True;
          else
            {
              free_sentence (s, se);
              se = 0;
            }
          s->sentences[i] = se;
          if (se)
            s->latest_sentence = se->id;
          break;
        }
    }

  if (any) sort_sentences (s);
}


/* Render all the words to the screen, and run the animation one step.
 */
static void
draw_sentence (state *s, sentence *se)
{
  int i;
  Bool moved = False;

  if (! se) return;

  for (i = 0; i < se->nwords; i++)
    {
      word *w = se->words[i];

      switch (s->mode)
        {
        case PAGE:
          if (se->anim_state != PAUSE &&
              w->tick <= w->nticks)
            {
              int dx = w->target_x - w->start_x;
              int dy = w->target_y - w->start_y;
              double r = sin (w->tick * M_PI / (2 * w->nticks));
              w->x = w->start_x + (dx * r);
              w->y = w->start_y + (dy * r);

              w->tick++;
              if (se->anim_state == OUT && s->mode == PAGE)
                w->tick++;  /* go out faster */
              moved = True;
            }
          break;
        case SCROLL:
          {
            int dx = w->target_x - w->start_x;
            int dy = w->target_y - w->start_y;
            double r = (double) w->tick / w->nticks;
            w->x = w->start_x + (dx * r);
            w->y = w->start_y + (dy * r);
            w->tick++;
            moved = (w->tick <= w->nticks);

            /* Launch a new sentence when:
               - the front of this sentence is almost off the left edge;
               - the end of this sentence is almost on screen.
               - or, randomly
             */
            if (se->anim_state != OUT &&
                i == 0 &&
                se->id == s->latest_sentence)
              {
                Bool new_p = (w->x < (s->xgwa.width * 0.4) &&
                              w->x + se->width < (s->xgwa.width * 2.1));
                Bool rand_p = (new_p ? 0 : !(random() % 2000));

                if (new_p || rand_p)
                  {
                    se->anim_state = OUT;
                    s->spawn_p = True;
# ifdef DEBUG
                    if (s->debug_p)
                      fprintf (stderr, "%s: OUT   %d (x2 = %d%s)\n",
                               progname, se->id,
                               se->words[0]->x + se->width,
                               rand_p ? " randomly" : "");
# endif
                  }
              }
          }
          break;
        default:
          abort();
          break;
        }

      draw_word (s, se, w);
    }

  if (moved && se->anim_state == PAUSE)
    abort();

  if (! moved)
    {
      switch (se->anim_state)
        {
        case IN:
          se->anim_state = PAUSE;
          se->pause_tick = (se->nwords * 7 * s->linger);
          if (se->move_chars_p)
            se->pause_tick /= 5;
          scatter_sentence (s, se);
          shuffle_words (s, se);
# ifdef DEBUG
          if (s->debug_p)
            fprintf (stderr, "%s: PAUSE %d\n", progname, se->id);
# endif
          break;
        case PAUSE:
          if (--se->pause_tick <= 0)
            {
              se->anim_state = OUT;
              s->spawn_p = True;
# ifdef DEBUG
              if (s->debug_p)
                fprintf (stderr, "%s: OUT   %d\n", progname, se->id);
# endif
            }
          break;
        case OUT:
# ifdef DEBUG
          if (s->debug_p)
            fprintf (stderr, "%s: DEAD  %d\n", progname, se->id);
# endif
          {
            int j;
            for (j = 0; j < s->nsentences; j++)
              if (s->sentences[j] == se)
                s->sentences[j] = 0;
            free_sentence (s, se);
          }
          break;
        default:
          abort();
          break;
        }
    }
}


/* Render all the words to the screen, and run the animation one step.
   Clear screen first, swap buffers after.
 */
static void
draw_fontglide (state *s)
{
  int i;

  if (s->spawn_p)
    more_sentences (s);

  if (!s->trails_p)
    XFillRectangle (s->dpy, s->b, s->bg_gc,
                    0, 0, s->xgwa.width, s->xgwa.height);

  for (i = 0; i < s->nsentences; i++)
    draw_sentence (s, s->sentences[i]);

#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
  if (s->backb)
    {
      XdbeSwapInfo info[1];
      info[0].swap_window = s->window;
      info[0].swap_action = (s->dbeclear_p ? XdbeBackground : XdbeUndefined);
      XdbeSwapBuffers (s->dpy, info, 1);
    }
  else
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
  if (s->dbuf)
    {
      XCopyArea (s->dpy, s->b, s->window, s->bg_gc,
		 0, 0, s->xgwa.width, s->xgwa.height, 0, 0);
    }
}


static void
handle_events (state *s)
{
  while (XPending (s->dpy))
    {
      XEvent event;
      XNextEvent (s->dpy, &event);

      if (event.xany.type == ConfigureNotify)
        {
          XGetWindowAttributes (s->dpy, s->window, &s->xgwa);

          if (s->dbuf && (s->ba))
            {
              XFreePixmap (s->dpy, s->ba);
              s->ba = XCreatePixmap (s->dpy, s->window, 
				     s->xgwa.width, s->xgwa.height,
				     s->xgwa.depth);
              XFillRectangle (s->dpy, s->ba, s->bg_gc, 0, 0, 
                              s->xgwa.width, s->xgwa.height);
              s->b = s->ba;
            }
        }

      screenhack_handle_event (s->dpy, &event);
    }
}



/* Subprocess.
   (This bit mostly cribbed from phosphor.c)
 */

static void
subproc_cb (XtPointer closure, int *source, XtInputId *id)
{
  state *s = (state *) closure;
  s->input_available_p = True;
}


static void
launch_text_generator (state *s)
{
  char *oprogram = get_string_resource ("program", "Program");
  char *program;

  program = (char *) malloc (strlen (oprogram) + 10);
  strcpy (program, "( ");
  strcat (program, oprogram);
  strcat (program, " ) 2>&1");

  if (s->debug_p)
    fprintf (stderr, "%s: forking: %s\n", progname, program);

  if ((s->pipe = popen (program, "r")))
    {
      s->pipe_id =
        XtAppAddInput (app, fileno (s->pipe),
                       (XtPointer) (XtInputReadMask | XtInputExceptMask),
                       subproc_cb, (XtPointer) s);
    }
  else
    {
      perror (program);
    }
}


static void
relaunch_generator_timer (XtPointer closure, XtIntervalId *id)
{
  state *s = (state *) closure;
  launch_text_generator (s);
}


/* When the subprocess has generated some output, this reads as much as it
   can into s->buf at s->buf_tail.
 */
static void
drain_input (state *s)
{
  /* allow subproc_cb() to run, if the select() down in Xt says that
     input is available.  This tells us whether we can read() without
     blocking. */
  if (! s->input_available_p)
    if (XtAppPending (app) & (XtIMTimer|XtIMAlternateInput))
      XtAppProcessEvent (app, XtIMTimer|XtIMAlternateInput);

  if (! s->pipe) return;
  if (! s->input_available_p) return;
  s->input_available_p = False;

  if (s->buf_tail < sizeof(s->buf) - 2)
    {
      int target = sizeof(s->buf) - s->buf_tail - 2;
      int n;
      n = read (fileno (s->pipe),
                (void *) (s->buf + s->buf_tail),
                target);
      if (n > 0)
        {
          s->buf_tail += n;
          s->buf[s->buf_tail] = 0;
        }
      else
        {
          XtRemoveInput (s->pipe_id);
          s->pipe_id = 0;
          pclose (s->pipe);
          s->pipe = 0;

          /* If the process didn't print a terminating newline, add one. */
          if (s->buf_tail > 1 &&
              s->buf[s->buf_tail-1] != '\n')
            {
              s->buf[s->buf_tail++] = '\n';
              s->buf[s->buf_tail] = 0;
            }

          /* Then add one more, to make sure there's a sentence break at EOF.
           */
          s->buf[s->buf_tail++] = '\n';
          s->buf[s->buf_tail] = 0;

          /* Set up a timer to re-launch the subproc in a bit. */
          XtAppAddTimeOut (app, s->subproc_relaunch_delay,
                           relaunch_generator_timer,
                           (XtPointer) s);
        }
    }
}


/* Window setup and resource loading */

static state *
init_fontglide (Display *dpy, Window window)
{
  XGCValues gcv;
  state *s = (state *) calloc (1, sizeof(*s));
  s->dpy = dpy;
  s->window = window;

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

  s->font_override = get_string_resource ("font", "Font");
  if (s->font_override && (!*s->font_override || *s->font_override == '('))
    s->font_override = 0;

  s->charset = get_string_resource ("fontCharset", "FontCharset");
  s->border_width = get_integer_resource ("fontBorderWidth", "Integer");
  if (s->border_width < 0 || s->border_width > 20)
    s->border_width = 1;

  s->speed = get_float_resource ("speed", "Float");
  if (s->speed <= 0 || s->speed > 200)
    s->speed = 1;

  s->linger = get_float_resource ("linger", "Float");
  if (s->linger <= 0 || s->linger > 200)
    s->linger = 1;

  s->debug_p = get_boolean_resource ("debug", "Debug");
  s->trails_p = get_boolean_resource ("trails", "Trails");

  s->dbuf = get_boolean_resource ("doubleBuffer", "Boolean");
  s->dbeclear_p = get_boolean_resource ("useDBEClear", "Boolean");

  if (s->trails_p) s->dbuf = False;  /* don't need it in this case */

  {
    char *ss = get_string_resource ("mode", "Mode");
    if (!ss || !*ss || !strcasecmp (ss, "random"))
      s->mode = ((random() % 2) ? SCROLL : PAGE);
    else if (!strcasecmp (ss, "scroll"))
      s->mode = SCROLL;
    else if (!strcasecmp (ss, "page"))
      s->mode = PAGE;
    else
      {
        fprintf (stderr,
                "%s: `mode' must be `scroll', `page', or `random', not `%s'\n",
                 progname, ss);
      }
  }

  if (s->dbuf)
    {
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
      if (s->dbeclear_p)
        s->b = xdbe_get_backbuffer (dpy, window, XdbeBackground);
      else
        s->b = xdbe_get_backbuffer (dpy, window, XdbeUndefined);
      s->backb = s->b;
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */

      if (!s->b)
        {
          s->ba = XCreatePixmap (s->dpy, s->window, 
                                 s->xgwa.width, s->xgwa.height,
                                 s->xgwa.depth);
          s->b = s->ba;
        }
    }
  else
    {
      s->b = s->window;
    }

  gcv.foreground = get_pixel_resource ("background", "Background",
                                       s->dpy, s->xgwa.colormap);
  s->bg_gc = XCreateGC (s->dpy, s->b, GCForeground, &gcv);

  s->subproc_relaunch_delay = 2 * 1000;

  launch_text_generator (s);

  s->nsentences = 5; /* #### */
  s->sentences = (sentence **) calloc (s->nsentences, sizeof (sentence *));
  s->spawn_p = True;

  return s;
}



char *progclass = "FontGlide";

char *defaults [] = {
  ".background:		#000000",
  ".foreground:		#DDDDDD",
  ".borderColor:	#555555",
  "*delay:	        10000",
  "*program:	      " FORTUNE_PROGRAM,
  "*mode:               random",
  ".font:               (default)",
  "*fontCharset:        iso8859-1",
  "*fontBorderWidth:    2",
  "*speed:              1.0",
  "*linger:             1.0",
  "*trails:             False",
  "*debug:              False",
  "*doubleBuffer:	True",
#ifdef HAVE_DOUBLE_BUFFER_EXTENSION
  "*useDBE:		True",
  "*useDBEClear:	True",
#endif /* HAVE_DOUBLE_BUFFER_EXTENSION */
  0
};

XrmOptionDescRec options [] = {
  { "-scroll",		".mode",	    XrmoptionNoArg, "scroll" },
  { "-page",		".mode",	    XrmoptionNoArg, "page"   },
  { "-random",		".mode",	    XrmoptionNoArg, "random" },
  { "-delay",		".delay",	    XrmoptionSepArg, 0 },
  { "-speed",		".speed",	    XrmoptionSepArg, 0 },
  { "-linger",		".linger",	    XrmoptionSepArg, 0 },
  { "-program",		".program",	    XrmoptionSepArg, 0 },
  { "-font",		".font",	    XrmoptionSepArg, 0 },
  { "-fn",		".font",	    XrmoptionSepArg, 0 },
  { "-bw",		".fontBorderWidth", XrmoptionSepArg, 0 },
  { "-trails",		".trails",	    XrmoptionNoArg,  "True"  },
  { "-no-trails",	".trails",	    XrmoptionNoArg,  "False" },
  { "-db",		".doubleBuffer",    XrmoptionNoArg,  "True"  },
  { "-no-db",		".doubleBuffer",    XrmoptionNoArg,  "False" },
  { "-debug",		".debug",           XrmoptionNoArg,  "True"  },
  { 0, 0, 0, 0 }
};


void
screenhack (Display *dpy, Window window)
{
  state *s = init_fontglide (dpy, window);
  int delay = get_integer_resource ("delay", "Integer");

  while (1)
    {
      handle_events (s);
      draw_fontglide (s);
      XSync (dpy, False);
      if (delay) usleep (delay);
    }
}
