/**
 * @file photomolo.c
 * The main program
 * @author Marko Mäkelä <msmakela@nic.funet.fi>
 */

/* Copyright © 2003 Marko Mäkelä.

   This file is part of PHOTOMOLO, a program for generating
   thumbnail images and HTML files for browsing digital photographs.

   PHOTOMOLO 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.

   PHOTOMOLO 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.

   The GNU General Public License is often shipped with GNU software, and
   is generally kept in a file called COPYING or LICENSE.  If you do not
   have a copy of the license, write to the Free Software Foundation,
   59 Temple Place, Suite 330, Boston, MA 02111 USA. */

/* sample command line options with present syntax:
 * photomolo -T _8/s8 -T _4/s4 -T _120/h120 -T _240/h240 -T _480/h480 cd1
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "scale.h"
#include "tree.h"
#include "info.h"
#include "spec.h"
#include "html.h"

/** The libjpeg decompression context */
static struct jpeg_decompress_struct cin;
/** The libjpeg compression context */
static struct jpeg_compress_struct cout;

/** Thumbnail specifications */
static struct thumb_spec* thumbs = 0;
/** Index file name */
static const char* indexname = "index.html";
/** Length of the index file name, plus 1 */
static unsigned indexnamelen;
/** Style sheet file name or URI */
static const char* cssname = "index.css";
/** Length of the style sheet file name, plus 1 */
static unsigned cssnamelen;
/** "icon" file name or URI (optional) */
static const char* iconname = 0;
/** "shortcut icon" file name or URI (optional) */
static const char* shortcutname = 0;

/** flag: generate HTML code? */
static int dump_html = 1;
/** flag: remove thumbnail directories */
static int remove_thumbdirs = 0;

/** Cleanup function */
static void
cleanup (void)
{
  /* deallocate the thumbnail specifications */
  struct thumb_spec* spec = thumbs;
  while (spec) {
    thumbs = spec->next;
    free (spec);
    spec = thumbs;
  }
  /* deallocate the libjpeg contexts */
  jpeg_destroy_compress (&cout);
  jpeg_destroy_decompress (&cin);
}

/** Create the thumbnail images
 * @param dir	the directory
 * @param info	the image information (for pruning non-image files)
 * @return	zero on success, nonzero on failure
 */
static int
make_thumbnails (const struct dirtree* dir,
		 const struct imageinfo* info)
{
  unsigned i;
  char* fn = 0;
  unsigned fnlen = strlen (dir->name) + 1;

  if (!(fn = malloc (fnlen + 1))) {
  memory:
    free (fn);
    fputs ("out of memory\n", stderr);
    return 1;
  }

  memcpy (fn, dir->name, fnlen);
  fn[fnlen - 1] = '/';

  for (i = 0; i < dir->n_files; i++) {
    const struct thumb_spec* spec;
    FILE* input;
    char* s;
    unsigned namelen;
    if (!info[i].width || !info[i].height)
      continue; /* no image */
    namelen = strlen (dir->files[i].name) + 1;
    if (!(s = realloc (fn, fnlen + namelen)))
      goto memory;
    fn = s;
    memcpy (fn + fnlen, dir->files[i].name, namelen);
    if (!(input = fopen (fn, "rb"))) {
      fputs (fn, stderr);
      perror (": open");
      free (fn);
      return 2;
    }
    jpeg_stdio_src (&cin, input);
    for (spec = thumbs; spec; spec = spec->next) {
      unsigned speclen = strlen (spec->thumbdir) + 1;
      if (!(s = realloc (fn, fnlen + namelen + speclen))) {
	fclose (input);
	goto memory;
      }
      fn = s;
      memcpy (fn + fnlen, spec->thumbdir, speclen);
      if (make_directory (fn)) {
      errexit:
	fclose (input);
	free (fn);
	return 3;
      }
      fn[fnlen + speclen - 1] = '/';
      memcpy (fn + fnlen + speclen, dir->files[i].name, namelen);
      if (!exists_mtime (fn, dir->files[i].mtime)) {
	FILE* output = fopen (fn, "wb");
	if (!output) {
	  fputs (fn, stderr);
	  perror (": open (writing)");
	  goto errexit;
	}
	jpeg_stdio_dest (&cout, output);
	cout.image_width = spec->max_width;
	cout.image_height = spec->max_height;
	if (scale_image (&cin, &cout, spec->shrink) ||
	    touch_mtime (fn, dir->files[i].mtime)) {
	  fclose (output);
	  goto errexit;
	}
	fclose (output);
	if (spec->next)
	  rewind (input);
      }
    }
    fclose (input);
  }

  free (fn);
  return 0;
}

/** Dump the HTML code (or remove thumbnail directories if remove_thumbdirs)
 * @param tree	the directory
 * @param level	number of '/' characters in the root directory name
 * @param info	image dimensions
 * @param comm	image comments
 * @param top	name of parent directory
 * @param prev	name of previous directory
 * @param next	name of next directory
 * @return	zero on success, nonzero on failure
 */
static int
dumphtmls (const struct dirtree* tree,
	   unsigned level,
	   const struct imageinfo* info,
	   const struct comment*const* comm,
	   const char* top,
	   const char* prev,
	   const char* next)
{
  int status = 0;
  const struct thumb_spec* spec;
  unsigned dirnamelen = strlen (tree->name) + 1;

  char* filename = malloc (dirnamelen + 1);
  if (!filename) {
  memory:
    fputs ("out of memory\n", stderr);
    return 1;
  }
  memcpy (filename, tree->name, dirnamelen - 1);
  filename[dirnamelen - 1] = '/';

  for (spec = thumbs; spec; spec = spec->next) {
    unsigned speclen = strlen (spec->thumbdir) + 1;
    FILE* f;
    char* fn = realloc (filename, dirnamelen + speclen + indexnamelen + 1);
    if (!fn)
      goto memory;
    filename = fn;
    memcpy (filename + dirnamelen, spec->thumbdir, speclen);
    if (remove_thumbdirs) {
      if (remove_directory (filename)) {
	free (filename);
	return 2;
      }
      continue;
    }
    if (make_directory (filename)) {
      free (filename);
      return 2;
    }
    filename[dirnamelen + speclen - 1] = '/';
    memcpy (filename + dirnamelen + speclen, indexname, indexnamelen);
    if (!(f = fopen (filename, "wb"))) {
      fputs (filename, stderr);
      perror (": open");
      free (filename);
      return 1;
    }
    status = dumphtml (f, tree, level, info, comm, thumbs, spec,
		       indexname, cssname, iconname, shortcutname,
		       top, prev, next);
    fclose (f);
    if (status) {
      fputs (filename, stderr);
      perror (": write");
      break;
    }
  }
  free (filename);
  return status;
}

/** Dump the image file directory listing
 * @param tree	the directory
 * @param level	number of '/' characters in the root directory name
 * @param top	name of parent directory
 * @param prev	name of previous directory
 * @param next	name of next directory
 * @return	zero on success, nonzero on failure
 */
static int
dumpfiles (const struct dirtree* tree,
	   unsigned level,
	   const char* top,
	   const char* prev,
	   const char* next)
{
  /** exit status */
  int status = 0;
  /** counter */
  unsigned i;
  /** image file information */
  struct imageinfo* info;
  /** image file comments */
  struct comment** comment;
  /** directory name length */
  unsigned dirnamelen;
  /** file name for reading tree->files */
  char* filename;

  dirnamelen = strlen (tree->name);
  filename = malloc (dirnamelen + 2);
  if (!filename) {
  memory:
    fputs ("out of memory\n", stderr);
    return 1;
  }
  memcpy (filename, tree->name, dirnamelen);
  filename[dirnamelen++] = '/';

  /* gather image info */
  if (!(info = calloc (tree->n_files, sizeof *info)) ||
      !(comment = calloc (tree->n_files, sizeof *comment))) {
    if (info) free (info);
    free (filename);
    goto memory;
  }

  if (!remove_thumbdirs) {
    for (i = 0; i < tree->n_files; i++) {
      unsigned len;
      char* s;
      FILE* f;
      if (!strcmp (tree->files[i].name, cssname) ||
	  !strcmp (tree->files[i].name, indexname) ||
	  (iconname && !strcmp (tree->files[i].name, iconname)) ||
	  (shortcutname && !strcmp (tree->files[i].name, shortcutname)))
	continue; /* ignore known non-image file names */
      len = strlen (tree->files[i].name) + 1;
      if (!(s = realloc (filename, dirnamelen + len))) {
	fputs ("out of memory\n", stderr);
	status = 1;
	break; /* out of memory */
      }
      filename = s;
      memcpy (filename + dirnamelen, tree->files[i].name, len);
      if (!(f = fopen (filename, "rb"))) {
	fputs (filename, stderr);
	perror (": fopen");
	continue; /* ignore unreadable files */
      }
      s = (char*) readinfo (info + i, comment + i, f);
      fclose (f);
      if (s) {
	/* ignore errors while reading image info */
	fputs (filename, stderr);
	fputs (": ", stderr), fputs (s, stderr);
	putc ('\n', stderr);
	freeinfo (comment[i]); comment[i] = 0;
	/* clear the info structure for this file */
	memset (info + i, 0, sizeof *info);
      }
    }
  }
  free (filename);

  /* generate the thumbnails */
  if (!status && !remove_thumbdirs)
    status = make_thumbnails (tree, info);
  /* generate the HTML or remove the thumbnail directories */
  if (!status && (remove_thumbdirs || dump_html))
    status = dumphtmls (tree, level,
			info, (const struct comment**) comment,
			top, prev, next);

  /* deallocate the image comments */
  for (i = tree->n_files; i--; )
    freeinfo (comment[i]);
  free (info);
  free (comment);
  return status;
}

/** Dump the image file directory tree
 * @param tree	the directory tree
 * @param level	number of '/' characters in the root directory name
 * @param top	name of parent directory
 * @param prev	name of previous directory
 * @param next	name of next directory
 * @return	zero on success, nonzero on failure
 */
static int
dumptree (const struct dirtree* tree,
	  unsigned level,
	  const char* top,
	  const char* prev,
	  const char* next)
{
  unsigned i;
  for (i = 0; i < tree->n_dirs; i++)
    if (dumptree (tree->dirs + i,
		  level,
		  tree->name,
		  i > 0 ? tree->dirs[i - 1].name : 0,
		  i < tree->n_dirs - 1 ? tree->dirs[i + 1].name : 0))
      return 1;
  return dumpfiles (tree, level, top, prev, next);
}

/** predicate for pruning directories by name
 * @param name	directory name
 * @return	0 if the directory should be considered, nonzero otherwise
 */
static int
prune_dirs (const char* name)
{
  struct thumb_spec* spec;
  for (spec = thumbs; spec; spec = spec->next)
    if (!strcmp (name, spec->thumbdir))
      return 1;
  return 0;
}

/** create the CSS file if it does not exist
 * @param path	the path name to the directory
 * @return	0 if successful
 */
static int
create_css (const char* path)
{
  /** first part of CSS definitions */
  static const char css1[] =
    "/*\n"
    " * Sample style sheet for pHoToMoLo-generated image gallery HTML\n"
    " * Written in 2003 by Marko Mäkelä (msmakela@nic.funet.fi).\n"
    " * This file is in the public domain.\n"
    " * The layout has been tested with the following browsers:\n"
    " * Lynx, Netscape 4, Opera 6, IE 5.01, IE 5.5, IE 6.0, Mozilla 1.x.\n"
    " */\n"
    "\n"
    "a:link,a:visited,a:active,a:hover{color:red;background:black;}\n"
    "a:link,a:visited{text-decoration:none;}\n"
    "a:hover,a:active{text-decoration:underline;}\n"
    "\n";
  /** second part of CSS definitions */
  static const char css2[] =
    "body{color:white;background:black;"
    "font-family:verdana,helvetica,sans-serif;}\n"
    "body,h1,p,table,tr,td,dl,img{border:0;margin:0;}\n"
    "\n"
    "h1{text-align:left;font-weight:bold;font-size:150%;}\n"
    "\n"
    "dd{margin:0 0 0 1em;}\n"
    "dl{font-weight:bold;font-style:normal;}\n"
    "dl dl{font-weight:normal;}\n"
    "dl dl dl{font-style:oblique;}\n"
    "dl dl dl dl{font-size:smaller;}\n"
    "\n"
    "tr,td{vertical-align:top;margin:0;border:0;}\n";
  FILE* f;
  unsigned pathlen;
  char* s;
  if (*cssname == '/' || strstr (cssname, "://"))
    return 0; /* absolute location */
  pathlen = strlen (path);
  if (!(s = malloc (pathlen + 1 + cssnamelen))) {
    fputs (path, stderr);
    fputs (": out of memory\n", stderr);
    return 1;
  }
  memcpy (s, path, pathlen);
  s[pathlen++] = '/';
  memcpy (s + pathlen, cssname, cssnamelen);
  if ((f = fopen (s, "rt")))
    goto done;
  if (!(f = fopen (s, "wt"))) {
    fputs (s, stderr);
    perror (": open (writing)");
    free (s);
    return 2;
  }
  if ((sizeof css1) - 1 != fwrite (css1, 1, (sizeof css1) - 1, f) ||
      (sizeof css2) - 1 != fwrite (css2, 1, (sizeof css2) - 1, f)) {
    fputs (s, stderr);
    perror (": write");
    free (s);
    fclose (f);
    return 2;
  }
 done:
  free (s);
  fclose (f);
  return 0;
}

/** The main function
 * @param argc	argument count
 * @param argv	argument vector
 * @return	0 if successful
 */
int
main (int argc, char** argv)
{
  jpeg_create_decompress (&cin);
  jpeg_create_compress (&cout);

  atexit (cleanup);

  if (argc == 1) {
  Usage:
    fputs ("pHoToMoLo 1.0\n"
	   "Create thumbnails and HTML navigation\n"
	   "Usage: photomolo [options] directory1 directory2 ...\n"
	   "Options:\n"
	   "-h"
	   "\tusage\n"
	   "-T <dir>/<spec>\n"
	   "\tgenerate thumbnail directory <dir> according to <spec>\n"
	   "-H htmlfile\n"
	   "\tname of HTML files (default is \"index.html\")\n"
	   "-C cssfile\n"
	   "\tname of CSS files (default is \"index.css\")\n"
	   "-i iconfile\n"
	   "-I file.ico\n"
	   "\tname of icon files (default is none)\n"
	   "-t"
	   "\tgenerate thumbnails only (no HTML or CSS)\n"
	   "-r"
	   "\tremove the thumbnail directories listed with -T\n"
	   "--\tdisable processing further options\n",
	   stderr);
    return 0;
  }

  /* process the options */
  for (argv++; argc > 1 && **argv == '-'; argv++, argc--) {
    const char* opts = *argv;
    if (!strcmp (*argv, "--")) { /* disable processing further options */
      argv++;
      argc--;
      break;
    }

    while (*++opts) { /* process all flags */
      switch (*opts) {
      default:
	putc ('-', stderr);
	putc (*opts, stderr);
	fputs (": unknown option (use -h for help)\n", stderr);
	return 1;
      case '?':
      case 'h':
	goto Usage;
      case 'T':
	if (argc <= 2) {
	  fputs ("-T: missing argument (use -h for help)\n", stderr);
	  return 1;
	}
	else {
	  struct thumb_spec* spec = parse_spec (*++argv);
	  argc--;
	  if (!spec)
	    return 1;
	  if (!thumbs)
	    thumbs = spec;
	  else {
	    struct thumb_spec* last = thumbs;
	    while (last->next)
	      last = last->next;
	    last->next = spec;
	  }
	}
	break;
      case 'H':
	if (argc <= 2) {
	  fputs ("-H: missing argument (use -h for help\n", stderr);
	  return 1;
	}
	argc--;
	if (!*(indexname = *++argv)) {
	  fputs ("-H: argument must be nonempty (use -h for help)\n", stderr);
	  return 1;
	}
	else if (strchr (indexname, '/')) {
	  fputs ("-H ", stderr);
	  fputs (indexname, stderr);
	  fputs (": name must not contain '/'", stderr);
	  return 1;
	}
	break;
      case 'C':
	if (argc <= 2) {
	  fputs ("-C: missing argument (use -h for help\n", stderr);
	  return 1;
	}
	argc--;
	if (!*(cssname = *++argv)) {
	  fputs ("-C: argument must be nonempty (use -h for help)\n", stderr);
	  return 1;
	}
	break;
      case 'i':
	if (argc <= 2) {
	  fputs ("-i: missing argument (use -h for help\n", stderr);
	  return 1;
	}
	argc--;
	if (!*(iconname = *++argv))
	  iconname = 0;
	break;
      case 'I':
	if (argc <= 2) {
	  fputs ("-I: missing argument (use -h for help\n", stderr);
	  return 1;
	}
	argc--;
	if (!*(shortcutname = *++argv))
	  shortcutname = 0;
	break;
      case 't':
	dump_html = 0;
	break;
      case 'r':
	remove_thumbdirs = 1;
	break;
      }
    }
  }

  if (!thumbs) {
    fputs ("expected at least one -T parameter (use -h for help)\n", stderr);
    return 1;
  }

  indexnamelen = strlen (indexname) + 1;
  cssnamelen = strlen (cssname) + 1;
  for (; argc > 1; argv++, argc--) {
    struct dirtree dirs;
    /* number of slashes in the specified directory */
    unsigned num_slash;
    if (!*argv)
      continue; /* ignore empty directory names */
    if (dump_html && !remove_thumbdirs && create_css (*argv))
      return 2;
    else {
      char* c;
      for (num_slash = 0, c = *argv; *c; c++)
#if defined WIN32 || defined __WIN32
	if (*c == '\\') *c = '/', num_slash++; else
#endif
	if (*c == '/')
	  num_slash++;
      /* discount trailing slashes */
      while (c-- > *argv && *c == '/')
	num_slash--;
      c[1] = 0;
    }
    inittree (&dirs, *argv);
    if (readtree (&dirs, prune_dirs) ||
	dumptree (&dirs, num_slash, 0, 0, 0)) {
      freetree (&dirs);
      return 2;
    }
    freetree (&dirs);
  }

  return 0;
}
