/* COMPRESSHELP, convert Help directory to a `Help.store' file.
   All rights reserved. (C) 1992,1993 Frank Thomas <frank@glocke.robin.de>
   Copyright (C) 1993 by Michael L.H. Brouwer <michael@urc.tue.nl>

   This program is free software; you can redistribute it and/or modify it
   under the terms of the GNU General Public License as published by the
   Free Software Foundation; either version 2, or (at your option) any later
   version.

   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
   Public License for more details.

   You should have received a copy of the GNU General Public License along
   with this program; if not, write to the Free Software Foundation, Inc.,
   675 Mass Ave, Cambridge, MA 02139, USA.

   Outline: 2 BTrees, a dirTree and a fileTree dirTree: Key=Name of
   directory; Value=f1 n1;f2 n2;...
            where f1.. are the filenames and n1.. are numbers (FileNumber)
   fileTree: Key=FileNumber Value=the whole file
             Rtf and .index.store files are in compressed form in the
   fileTree The fileTree is reduced by the fact that identical files are
   mapped to the same FileNumber in the dirTree.

   $Modified: Mon Apr  5 00:56:44 1993 by michael $
   $Id: compressHelp.m,v 1.14 1993/04/04 22:57:22 michael Exp $  */

#import <appkit/appkit.h>
#import <objc/objc.h>
#import <mach/mach.h>
#import <mach/mach_error.h>
#import <assert.h>
#import <stdio.h>
#import <libc.h>
#import <errno.h>
#import <string.h>
#import <stdlib.h>
#import <sys/wait.h>
#import <store/IXStoreFile.h>
#import <btree/IXBTree.h>
#import <btree/IXBTreeCursor.h>
#import <sys/param.h>
#import <sys/types.h>
#import <sys/dir.h>
#import <sys/stat.h>

#import "global.h"
#import "getopt.h"
#import "dirtree.h"
#import "mapfile.h"

char *version_string = "compressHelp 1.1 (5 apr 93)";

/* No include file contains this one!  */
extern char *realpath (const char *file_name, char *resolved_name);

/* Command line options.  */
bool delete_dir = FALSE;

static struct option longopts[] =
{
  {"delete", 0, NULL, 'd'},
  {"help", 0, NULL, 'h'},
  {"nowarn", 0, NULL, 'n'},
  {"verbose", 0, NULL, 'v'},
  {"version", 0, NULL, 'V'},
  {NULL, 0, NULL, 0}
};

static char *getBufferPointer;

static char *
getBuffer (size_t size)
{
  static bufferSize;

  if (getBufferPointer)
    {
      if (size < bufferSize)
	return getBufferPointer;
      free (getBufferPointer);
    }
  bufferSize = size;
  return getBufferPointer = xmalloc (size);
} /* getBuffer */

static void
freeBuffer (void)
{
  if (getBufferPointer)
    free (getBufferPointer);
} /* freeBuffer */

/* Compress file src to dst.  Return 0 on success and -1 on failure.  */
static bool
compressTo (const char *src, const char *dst)
{
  union wait statusp;
  int pid;

  pid = fork ();
  if (pid < 0)
    return errorf ("fork");
  if (pid == 0)
    {
      int src_fd, dst_fd;

      src_fd = open (src, O_RDONLY);
      if (src_fd < 0)
	fatalf ("open (RDONLY): %s", src);
      dst_fd = open (dst, O_RDWR | O_TRUNC | O_CREAT, 0666);
      if (dst_fd < 0)
	fatalf ("open (RDWR): %s", dst);
      dup2 (src_fd, fileno (stdin));
      dup2 (dst_fd, fileno (stdout));
      execl ("/usr/ucb/compress", "/usr/ucb/compress", NULL);
      fatalf ("exec /usr/ucb/compress");
    }
  if (wait (&statusp) < 0 || statusp.w_retcode)
    return -1;
  return 0;
} /* compressTo */

/* Store each directory in T into the IXBTree associsated with DIRCURSOR.
   Return 0 on success and -1 on failure.  */
static bool
insertDirTree (dt_tree *t, IXBTreeCursor *dirCursor)
{
  int i;
  dt_file *f;

  if (verbose)
    fprintf (stderr, "Writing DirTree\n");
  /* Sort for dirtree.  */
  sort_by_name (t);

  for (i = t->elems, f = t->files; i > 0; i--, f++)
    {
      char *ext;
      char key[MAXPATHLEN];

      ext = strrchr (f->dir, '.');
      if (ext && strcmp (ext, ".rtfd/") == 0)
	{
	  /* Stuff the whole dir in the store.  */
	  int cnt;
	  dt_file *f2;
	  size_t size = 0;
	  char *data;
	  char *curr;
	  int keyLen = ext - f->dir + 6;
	  bool found;

	  strcpy (key, f->dir);
	  key[keyLen - 1] = 0;
	  /* How large is the data?  */
	  for (cnt = i, f2 = f; cnt > 0 && f2->dir == f->dir; f2++, cnt--)
	    size += strlen (f2->name);

	  /* For number, semicolon and space.  */
	  size += (i - cnt) * 14;
	  curr = data = getBuffer (size);

	  /* Build the data.  */
	  for (; cnt != i; f++, i--)
	    {
	      sprintf (curr, "%s %ld; ", f->name, f->id);
	      curr += strlen (curr);
	    }

	  /* Compensate the last step-part of the for-loop.  */
	  f--;
	  i++;

	  /* And stuff the data in the store.  */
	  found = [dirCursor setKey: key andLength: keyLen];
	  assert (found == NO);
	  [dirCursor writeValue: data andLength: curr - data + 1];
	}
      else
	{
	  /* Single file.  */
	  char data[MAXPATHLEN + 12];

	  sprintf (key, "%s%s", f->dir, f->name);
	  sprintf (data, "%s %ld;", f->name, f->id);

	  ext = strrchr (f->name, '.');

	  /* Check if we know its type.  */
	  if (strcmp (f->name, ".index.store") == 0
	      || (ext && strcmp(ext,".rtf") == 0))
	    {
	      /* Insert single file.  */
	      [dirCursor setKey: key andLength: strlen (key) + 1];
	      [dirCursor writeValue: data andLength: strlen (data) + 1];
	    }
	  else
	    {
	      all_ok = FALSE;
	      if (warnings)
		fprintf (stderr, "Unknown filetype %s%s\n", f->dir, f->name);
	    }
	}
    }
  return 0;
} /* insertDirTree */

/* Store each file with a unique ID in T once into the IXBTree associsated
   with FILECURSOR.  Return 0 on success and -1 on failure.  */
static bool
insertFileTree (dt_tree *t, IXBTreeCursor *fileCursor)
{
  /* Long enough for /usr/tmp...  */
  char compressedName[30];
  long lastId = -1;
  size_t dataSize;
  char *dataFile;
  dt_file *f;
  int i;

  if (verbose)
    fprintf (stderr, "Writing FileTree\n");
  sort_by_id (t);

  sprintf (compressedName, "/usr/tmp/ch..%d", getpid ());
  for (i = t->elems, f = t->files; i > 0; i--, f++)
    {
      char name[MAXPATHLEN];
      bool compressed;
      map_handle mh;
      char *ext;

      /* We store every id only once.  */
      if (f->id == lastId)
	continue;
      /* Save for next time around.  */
      lastId = f->id;

      [fileCursor setKey: &f->id andLength: sizeof (f->id)];

      /* Full name.  */
      sprintf (name, "%s%s", f->dir, f->name);
      /* Extension of basename.  */
      ext = strrchr (f->name, '.');

      /* Check if we have to compress this one.  */
      if (strcmp (f->name, ".index.store") == 0
	  || (ext && strcmp(ext,".rtf") == 0))
	{
	  struct stat st;

	  /* Compress it.  */
	  if (compressTo (name, compressedName))
	    return -1;
	  stat (compressedName, &st);
	  dataFile = compressedName;
	  dataSize = st.st_size;
	  compressed = TRUE;
	}
      else
	{
	  dataFile = name;
	  dataSize = f->size;
	  compressed = FALSE;
	}

      /* Map file to mem.  */
      mh.name = dataFile;
      if (map_file (&mh))
	return -1;
      [fileCursor writeValue: mh.mem andLength: dataSize];
      if (unmap_file (&mh))
	return -1;
    }
  unlink (compressedName);
  return 0;
} /* insertFileTree */

/* Store all *directories* in T into the IXBTree associsated with DIRCURSOR,
   and store all *files* in T into the IXBTree associsated with DIRCURSOR.
   Return 0 on success and -1 on failure.  */
static bool
insertAll (dt_tree *t, IXBTreeCursor *dirCursor, IXBTreeCursor *fileCursor)
{
#ifdef DEBUG
  dt_print (t);
#endif
  /* Write the dirtree followed by the filetree.  */
  if (insertDirTree (t, dirCursor) || insertFileTree (t, fileCursor))
    return -1;
  return 0;
} /* insertAll */

/* Create a DIR.store file for DIR.  Return 0 on success and return -1 on
   failure.  */
static int
compressHelp_dir (char *dir)
{
  char resolved_name[MAXPATHLEN], oldPath[MAXPATHLEN];
  IXBTreeCursor *fileCursor;
  IXBTreeCursor *dirCursor;
  IXStoreFile *helpStore;
  IXBTree *fileTree;
  IXBTree *dirTree;
  char *helpName;
  dt_tree *t;
  int files;
  int res;

  /* Save old path.  */
  getwd (oldPath);
  /* Build name of store-file.  */
  if (!realpath (dir, resolved_name))
    return errorf ("%s", resolved_name);

  helpName = alloca (strlen (resolved_name) + sizeof (".store"));
  strcat (strcpy (helpName, resolved_name), ".store");

  /* Destroy old one store file.  */
  unlink (helpName);
  /* Create help.store before changing directory.  */
  helpStore = [[IXStoreFile alloc] initWithFile: helpName];
  if (!helpStore)
    return errorf ("[IXStoreFile initWithFile: %s]", helpName);

  chdir (resolved_name);
  t = dt_read ();
  if (t == (dt_tree *) -1)
    {
      chdir (oldPath);
      return -1;
    }
  files = dt_calc_ids (t);
  if (files == -1)
    {
      chdir (oldPath);
      return -1;
    }

  /* I do only inserts but want a small Help.store and -compact doesn't work
     (see GeneralRef/07.../Classes/IXBTree.rtf) using optimizeForSpace
     doesn't seem to help as well (the only difference seems to be some
     timestamps in the database), but NeXT's Help.store files are smaller. I
     think they used a test version of the indexing kit which can compact
     IXBTrees.  So I added 'compact' and hope that this will help some time
     in the future.  */

  /* Create Dir-Tree as Block 1.  */
  dirTree = [[IXBTree alloc] initInStore: helpStore];
  if (!dirTree)
    {
      fprintf (stderr, "Can't create dirTree\n");
      [helpStore free];
      chdir (oldPath);
      return -1;
    }

  /* Create File-Tree as Block 2.  */
  fileTree = [[IXBTree alloc] initInStore: helpStore];
  if (!fileTree)
    {
      fprintf (stderr, "Can't create fileTree\n");
      [dirTree free];
      [helpStore free];
      chdir (oldPath);
      return -1;
    }
  /* How to compare.  */
  [fileTree setComparator: IXCompareUnsignedLongs andContext: NULL];
  [dirTree setComparator: IXCompareStrings andContext: NULL];

  /* One cursor for each tree.  */
  dirCursor = [[IXBTreeCursor alloc] initWithBTree: dirTree];
  fileCursor = [[IXBTreeCursor alloc] initWithBTree: fileTree];

  /* Insert all files we gathered (and compress them if necessary).  */
  res = insertAll (t, dirCursor, fileCursor);
  if (!res)
    {
      if (verbose)
	fprintf (stderr, "Compacting DirTree\n");
      [dirTree compact];
      if (verbose)
	fprintf (stderr, "Compacting FileTree\n");
      [fileTree compact];
    }
  [fileCursor free];
  [dirCursor free];
  [fileTree free];
  [dirTree free];

  /* Save all outstanding blocks to disk.  */
  [helpStore commitTransaction];
  [helpStore free];
  chdir (oldPath);
  if (res)
    unlink (helpName);
  else if (delete_dir && all_ok)
    {
      char *buf = malloc (8 + strlen (dir));

      sprintf (buf, "rm -rf %s", dir);
      if (verbose)
	fprintf (stderr, "Removing directory: %s\n", dir);
      res = system (buf);
      if (res)
	errorf ("system (%s) returned %d", buf, res);
      free (buf);
    }
  return res;
} /* compressHelp_dir */

/* If PRINTHELP is TRUE, print verbose help otherwise just show a short
   usage line.  Exit return code 4.  */
static void
usage (bool printhelp)
{
  if (!printhelp)
    fprintf (stderr, "\
usage: %s [-dhnvV] dir...\n\
for more help, type: %s -h\n", prog, prog);
  else
    fprintf (stderr, "\
%s\n\
usage: %s [-dhnvV] dir...\n\
 -d --delete		delete directory after creating .store file\n\
 -h --help		give this help\n\
 -n --nowarn		don't print warnings for unrecognized files\n\
 -v --verbose		verbose mode\n\
 -V --version		display version number\n\
 dir...			directories to create .store files for.\n",
	     version_string, prog);
  exit (4);
} /* usage */

/* Yes, the main program, which parses arguments, and does the right thing
   with them.  */
int
main (int argc, char **argv)
{
  int opt;

  init_prog (argv[0]);
  while ((opt = getopt_long (argc, argv, "dhnvV", longopts, (int *) 0)) != EOF)
    {
      switch (opt)
	{
	case 'd':
	  delete_dir = TRUE;
	  break;
	case 'h':
	  usage (TRUE);
	case 'n':
	  warnings = FALSE;
	  break;
	case 'v':
	  verbose = TRUE;
	  break;
	case 'V':
	  fprintf (stderr, "%s\n", version_string);
	  break;
	default:
	  usage (FALSE);
	}
    }
  if (optind == argc)
    usage (FALSE);

  /* Loop though all the directory arguments.  Exit when the first one goes
     wrong.  */
  while (optind < argc)
    if (compressHelp_dir (argv[optind++]))
      return 1;
  return 0;
} /* main */
