/* cookie.c - print a cookie at random from cookie-dough
 * Jim Blandy - Tue May  8 1990 - Oberlin College, OH
 * Modified by Karl Fogel, June 21 1995, Bloomington, IN, USA.
 * Further modified by Noah Friedman, 1995-07-20, Austin, TX, USA.
 */

/* $Id$ */

#define _POSIX_SOURCE

#include <stdio.h>
#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif /* HAVE_STDLIB_H */
#include <sys/types.h>
#include <time.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif /* HAVE_UNISTD_H */
#include <sys/stat.h>
#ifdef HAVE_SYS_TIMEB_H
#include <sys/timeb.h>
#endif /* HAVE_SYS_TIMEB_H */
#include <string.h>

/* This program selects and prints a cookie from cookie-dough.  It
   builds and updates an index file to find cookies quickly.

   The cookie-dough file should be a text file containing the cookies,
   separated by \ characters.  If you want to include a \ in the text,
   you can double it.

   The cookie-index file is maintained by cookie itself.  Whenever
   cookie runs, if the index file is out of date with respect to
   cookie-dough, it updates it.  If it looks as if cookie-dough has been
   modified by having new fortunes added to the end, cookie refrains
   from rebuilding the entire index.  Cookie makes this guess by
   saving the last few characters from cookie-dough and their offset in
   the file; if an update seems necessary but these characters are
   still the same, cookie assumes that it need only index fortunes from
   that point on.  Running cookie with the --rebuild option forces it
   to rebuild the index file.

   Cookie-index starts out with a struct cookieheader, which says how
   many cookies are in the file, and contains information for the
   incremental rebuilding stuff.  The remainder of cookie-index holds
   longword offsets into cookie-dough, giving the starting locations of
   each cookie. */

#define COOKIEMAGIC (('Y' << 24) | ('U' << 16) | ('M' << 8) | '!')
#define CHECKSIZE (8)		/* # of characters to sample from
				   end of file */
#define SEPARATOR ('\\')	/* character which separates cookies */

/* These used to be #defines and were compiled in, but we like softer
 * cookies nowadays.
 */

#ifndef DEFAULT_COOKIEJAR
#define DEFAULT_COOKIEJAR "/usr/local/share/cookie/cookie-dough"
#endif

char *datafile = NULL;
char *indexfile = NULL;

struct cookieheader
  {
    long magic;			/* magic number for cookie file */
    long n_cookies;		/* number of cookies in file */
    char check[CHECKSIZE];	/* data to match for incremental re-index */
    long checkposn;		/* where it should be */
    /* check and checkposn are irrelevant
       if n_cookies == 0.  */
  }
h;

char *progname;
FILE *data, *indexfp;
struct stat datastat, indexstat;

#define index_old() (datastat.st_mtime > indexstat.st_mtime)

void errordie (char *mesg);
void open_files (void);
void incremental_rebuild (void);
void full_rebuild (void);
void rebuild (void);
void choose_cookie (void);
void giveusage (void);

int main (int argc, char **argv)
{
  char needs_rebuild = 0;
  size_t datalen;

  progname = argv[0];

  for (argc--, argv++; argc > 0; argc--, argv++)
    if (argv[0][0] == '-')
      {
	if (strcmp (argv[0], "--rebuild") == 0)
	  needs_rebuild = 1;
	else
	  giveusage ();
      }
    else
      datafile = argv[0];

  /* If no arg for datafile file, try environment;
   * failing that, use a hardcoded default.
   */
  if (datafile == NULL)
    datafile = getenv ("COOKIEJAR");
  if (datafile == NULL)
    datafile = DEFAULT_COOKIEJAR;

  /* datafile filename "foo" ==> indexfile filename "foo.idx". */
  datalen = strlen (datafile);
  indexfile = malloc (datalen + sizeof (".idx"));
  strcpy (indexfile, datafile);
  strcpy (indexfile + datalen, ".idx");

  open_files ();

  if (needs_rebuild)
    full_rebuild ();
  else if (index_old ())
    incremental_rebuild ();

  choose_cookie ();

  exit (0);
}

void giveusage ()
{
  char *usage =
  "Usage: %s [--rebuild] [COOKIEFILE]\n"
  "Prints out a fortune cookie message.\n"
  "\n"
  "If no COOKIEFILE given, it checks environment variable COOKIEJAR.\n"
  "Failing that, it tries " DEFAULT_COOKIEJAR ".\n"
  "\n"
  "--rebuild: this option forces cookie to rebuild the cookie index file.\n"
  "           Cookie automatically rebuilds the index if it is out of date\n"
  "           with respect to the data file.\n"
   ;

  fprintf (stderr, usage, progname);
  exit (1);
}


char *
bkcat (char *str1, char *str2)
/* Wow, Ben and I wrote some utility routines a long time ago... this
 * is one.
 */
{
  char *newloc;

  /* Can we get away with doing nothing? */
  if (str1 == NULL)
    return str2;
  if (str2 == NULL)
    return str1;

  if ((newloc = malloc (strlen (str1) + strlen (str2) + 1)) != NULL)
    {
      strcpy (newloc, str1);
      strcat (newloc, str2);
      return newloc;
    }

  printf ("\nUnable to allocate sufficient memory.\n");
  printf ("Please buy more and try again.\n");
  exit (1);
}

void
errordie (char *mesg)
/* print "progname: mesg: system error message" and exit(2). */
{
  fprintf (stderr, "%s: ", progname);
  perror (mesg);
  exit (2);
}

FILE *
myfopen (char *name, char *mode)
{
  return fopen (name, mode);
}

void
open_files ()
/* open the data file and the index file.  If the index file does
   not exist, create it.  Read h. */
{
  if ((data = myfopen ((datafile), "r")) == NULL)
    errordie (bkcat ("error opening ", datafile));
  if (fstat (fileno (data), &datastat) == -1)
    errordie (bkcat ("error examining ", datafile));

  /* try to open the index file.  If it doesn't exist, create it. */
  {
    int index_created = 0;

    if ((indexfp = myfopen ((indexfile), "r+")) == NULL)
      {
	fprintf (stderr, "Creating index file...\n");

	if ((indexfp = myfopen ((indexfile), "w+")) == NULL)
	  errordie (bkcat ("error opening ", indexfile));

	index_created = 1;
      }

    if (fstat (fileno (indexfp), &indexstat) == -1)
      errordie (bkcat ("error examining ", indexfile));

    /* if we've just created the index file, the last modification
       date does *not* reflect the latest modification of the
       data *in* the file.  So hack it. */
    if (index_created)
      indexstat.st_mtime = 0;
  }

  /* read in the header info. */
  if (fread (&h, sizeof (h), 1, indexfp) != 1 ||
      h.magic != COOKIEMAGIC)
    {

      /* problems reading the header - make it sane. */
      h.magic = COOKIEMAGIC;
      h.n_cookies = 0;
    }
}


/*==================== index rebuilding stuff! ====================*/

void
incremental_rebuild ()
/* The index is out of date; see if we can rebuild it incrementally.
   if not, call full_rebuild(). */
{
  /* check the check. */

  /* no cookies? */
  if (h.n_cookies == 0)
    full_rebuild ();

  /* no *new* cookies, indicating changes elsewhere? */
  else if (h.checkposn + CHECKSIZE == datastat.st_size)
    full_rebuild ();

  else
    {
      char current[CHECKSIZE];

      /* find the check text. */
      if (fseek (data, h.checkposn, 0) == -1)
	errordie ("error reading check text");

      /* read & compare the check text. */
      if (fread (current, sizeof (char), CHECKSIZE, data) == CHECKSIZE &&
	  memcmp (current, h.check, CHECKSIZE) == 0)
	{

	  /* we're going to do an incremental rebuild.  Move the
	     index pointer to the end of the file, and move the
	     data pointer to the first new cookie. */
	  if (fseek (indexfp, sizeof (h) + h.n_cookies * sizeof (long), 0)
	      == -1 ||
	      fseek (data, h.checkposn + CHECKSIZE, 0) == -1)
	      errordie ("error starting incremental rebuild");

	  rebuild ();
	}
      else
	full_rebuild ();
    }
}


void
full_rebuild ()
/* completely rebuild the index file. */
{
  /* Move the index pointer to the start of the index area, and
     move the data pointer to the start of the cookie data file. */

  if (fseek (indexfp, sizeof (h), 0) == -1 ||
      fseek (data, 0, 0) == -1)
    errordie ("error starting full rebuild");

  h.n_cookies = 0;
  rebuild ();
}


void
rebuild ()
/* start scanning the data file at the current position, and adding
   index entries at the current position in the index file.  Both
   files should be fseek()ed to someplace appropriate before calling
   this function, and h.n_cookies should be the number of cookies
   already indexed. */
{
  int c;
  long posn = 0;

  do
    {

      /* add an index entry for the current cookie */
      if (fwrite (&posn, sizeof (posn), 1, indexfp) != 1)
	errordie ("error writing index");
      h.n_cookies++;

      /* scan for the beginning of the next cookie */
      while ((c = getc (data)) != EOF)
	if (c == SEPARATOR)
	  {
	    posn = ftell (data);
	    if ((c = getc (data)) != SEPARATOR)
	      break;
	  }

    }
  while (c != EOF);

  /* acquire the new check text */
  if (fseek (data, -CHECKSIZE, 2) == -1)
    errordie ("error finding new check text");
  h.checkposn = ftell (data);
  if (fread (&h.check, sizeof (char), CHECKSIZE, data) != CHECKSIZE)
      errordie ("error getting new check text");

  /* write out the header */
  if (fseek (indexfp, 0, 0) == -1 ||
      fwrite (&h, sizeof (h), 1, indexfp) != 1)
    errordie ("error writing index header");
}


/*==================== actual cookie stuff ====================*/

void
choose_cookie ()
/* pick a random cookie and print it. */
{
  long cookienum;
  long cookieposn;

  cookienum = (time (NULL) + getpid ()) % h.n_cookies;

  /* extract the index entry and move to the position in the data file */
  if (fseek (indexfp, sizeof (h) + cookienum * sizeof (long), 0) == -1)
    errordie ("error seeking in index file");

  if (fread (&cookieposn, sizeof (long), 1, indexfp) != 1)
    errordie ("error reading index file");
  
  if (fseek (data, cookieposn, 0) == -1)
    errordie ("error seeking to cookie");

  /* print the cookie */
  {
    int c;

    /* print characters until the next separator; if the separator
       is doubled, undouble it surrepetitiously. */
    while ((c = getc (data)) != EOF)
      {
	if (c == SEPARATOR)
	  if ((c = getc (data)) != SEPARATOR)
	    break;
	putchar (c);
      }
  }
}
