/* updateiconcache.c
 * Copyright (C) 2004  Anders Carlsson <andersca@gnome.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <locale.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif
#include <errno.h>
#ifdef _MSC_VER
#include <io.h>
#include <sys/utime.h>
#else
#include <utime.h>
#endif

#include <glib.h>
#include <glib/gstdio.h>
#include <gdk-pixbuf/gdk-pixdata.h>
#include <glib/gi18n.h>
#include "gtkiconcachevalidator.h"

static gboolean force_update = FALSE;
static gboolean ignore_theme_index = FALSE;
static gboolean quiet = FALSE;
static gboolean index_only = TRUE;
static gboolean validate = FALSE;
static gchar *var_name = "-";

/* Quite ugly - if we just add the c file to the
 * list of sources in Makefile.am, libtool complains.
 */
#include "gtkiconcachevalidator.c"

#define CACHE_NAME "icon-theme.cache"

#define HAS_SUFFIX_XPM (1 << 0)
#define HAS_SUFFIX_SVG (1 << 1)
#define HAS_SUFFIX_PNG (1 << 2)
#define HAS_ICON_FILE  (1 << 3)

#define MAJOR_VERSION 1
#define MINOR_VERSION 0
#define HASH_OFFSET 12

#define ALIGN_VALUE(this, boundary) \
  (( ((unsigned long)(this)) + (((unsigned long)(boundary)) -1)) & (~(((unsigned long)(boundary))-1)))

#ifdef HAVE_FTW_H

#include <ftw.h>

static GStatBuf cache_dir_stat;
static gboolean cache_up_to_date;

static int check_dir_mtime (const char        *dir,
                            const GStatBuf    *sb,
                            int                tf)
{
  if (tf != FTW_NS && sb->st_mtime > cache_dir_stat.st_mtime)
    {
      cache_up_to_date = FALSE;
      /* stop tree walk */
      return 1;
    }

  return 0;
}

static gboolean
is_cache_up_to_date (const gchar *path)
{
  gchar *cache_path;
  gint retval;

  cache_path = g_build_filename (path, CACHE_NAME, NULL);
  retval = g_stat (cache_path, &cache_dir_stat);
  g_free (cache_path);

  if (retval < 0)
    {
      /* Cache file not found */
      return FALSE;
    }

  cache_up_to_date = TRUE;

  ftw (path, check_dir_mtime, 20);

  return cache_up_to_date;
}

#else  /* !HAVE_FTW_H */

gboolean
is_cache_up_to_date (const gchar *path)
{
  GStatBuf path_stat, cache_stat;
  gchar *cache_path;
  int retval;

  retval = g_stat (path, &path_stat);

  if (retval < 0)
    {
      /* We can't stat the path,
       * assume we have a updated cache */
      return TRUE;
    }

  cache_path = g_build_filename (path, CACHE_NAME, NULL);
  retval = g_stat (cache_path, &cache_stat);
  g_free (cache_path);

  if (retval < 0)
    {
      /* Cache file not found */
      return FALSE;
    }

  /* Check mtime */
  return cache_stat.st_mtime >= path_stat.st_mtime;
}

#endif  /* !HAVE_FTW_H */

static gboolean
has_theme_index (const gchar *path)
{
  gboolean result;
  gchar *index_path;

  index_path = g_build_filename (path, "index.theme", NULL);

  result = g_file_test (index_path, G_FILE_TEST_IS_REGULAR);

  g_free (index_path);

  return result;
}


typedef struct
{
  GdkPixdata pixdata;
  gboolean has_pixdata;
  guint32 offset;
  guint size;
} ImageData;

typedef struct
{
  int has_embedded_rect;
  int x0, y0, x1, y1;

  int n_attach_points;
  int *attach_points;

  int n_display_names;
  char **display_names;

  guint32 offset;
  gint size;
} IconData;

static GHashTable *image_data_hash = NULL;
static GHashTable *icon_data_hash = NULL;

typedef struct
{
  int flags;
  int dir_index;

  ImageData *image_data;
  guint pixel_data_size;

  IconData *icon_data;
  guint icon_data_size;
} Image;


static gboolean
foreach_remove_func (gpointer key, gpointer value, gpointer user_data)
{
  Image *image = (Image *)value;
  GHashTable *files = user_data;
  GList *list;
  gboolean free_key = FALSE;

  if (image->flags == HAS_ICON_FILE)
    {
      /* just a .icon file, throw away */
      g_free (key);
      g_free (image);

      return TRUE;
    }

  list = g_hash_table_lookup (files, key);
  if (list)
    free_key = TRUE;

  list = g_list_prepend (list, value);
  g_hash_table_insert (files, key, list);

  if (free_key)
    g_free (key);

  return TRUE;
}

static IconData *
load_icon_data (const char *path)
{
  GKeyFile *icon_file;
  char **split;
  gsize length;
  char *str;
  char *split_point;
  int i;
  gint *ivalues;
  GError *error = NULL;
  gchar **keys;
  gsize n_keys;
  IconData *data;

  icon_file = g_key_file_new ();
  g_key_file_set_list_separator (icon_file, ',');
  g_key_file_load_from_file (icon_file, path, G_KEY_FILE_KEEP_TRANSLATIONS, &error);
  if (error)
    {
      g_error_free (error);
      g_key_file_free (icon_file);

      return NULL;
    }

  data = g_new0 (IconData, 1);

  ivalues = g_key_file_get_integer_list (icon_file,
					 "Icon Data", "EmbeddedTextRectangle",
					 &length, NULL);
  if (ivalues)
    {
      if (length == 4)
	{
	  data->has_embedded_rect = TRUE;
	  data->x0 = ivalues[0];
	  data->y0 = ivalues[1];
	  data->x1 = ivalues[2];
	  data->y1 = ivalues[3];
	}

      g_free (ivalues);
    }

  str = g_key_file_get_string (icon_file, "Icon Data", "AttachPoints", NULL);
  if (str)
    {
      split = g_strsplit (str, "|", -1);

      data->n_attach_points = g_strv_length (split);
      data->attach_points = g_new (int, 2 * data->n_attach_points);

      for (i = 0; i < data->n_attach_points; ++i)
	{
	  split_point = strchr (split[i], ',');
	  if (split_point)
	    {
	      *split_point = 0;
	      split_point++;
	      data->attach_points[2 * i] = atoi (split[i]);
	      data->attach_points[2 * i + 1] = atoi (split_point);
	    }
	}

      g_strfreev (split);
      g_free (str);
    }

  keys = g_key_file_get_keys (icon_file, "Icon Data", &n_keys, &error);
  data->display_names = g_new0 (gchar *, 2 * n_keys + 1);
  data->n_display_names = 0;

  for (i = 0; i < n_keys; i++)
    {
      gchar *lang, *name;

      if (g_str_has_prefix (keys[i], "DisplayName"))
	{
	  gchar *open, *close = NULL;

	  open = strchr (keys[i], '[');

	  if (open)
	    close = strchr (open, ']');

	  if (open && close)
	    {
	      lang = g_strndup (open + 1, close - open - 1);
	      name = g_key_file_get_locale_string (icon_file,
						   "Icon Data", "DisplayName",
						   lang, NULL);
	    }
	  else
	    {
	      lang = g_strdup ("C");
	      name = g_key_file_get_string (icon_file,
					    "Icon Data", "DisplayName",
					    NULL);
	    }

	  data->display_names[2 * data->n_display_names] = lang;
	  data->display_names[2 * data->n_display_names + 1] = name;
	  data->n_display_names++;
	}
    }

  g_strfreev (keys);

  g_key_file_free (icon_file);

  /* -1 means not computed yet, the real value depends
   * on string pool state, and will be computed
   * later
   */
  data->size = -1;

  return data;
}

/*
 * This function was copied from gtkfilesystemunix.c, it should
 * probably go to GLib
 */
static void
canonicalize_filename (gchar *filename)
{
  gchar *p, *q;
  gboolean last_was_slash = FALSE;

  p = filename;
  q = filename;

  while (*p)
    {
      if (*p == G_DIR_SEPARATOR)
	{
	  if (!last_was_slash)
	    *q++ = G_DIR_SEPARATOR;

	  last_was_slash = TRUE;
	}
      else
	{
	  if (last_was_slash && *p == '.')
	    {
	      if (*(p + 1) == G_DIR_SEPARATOR ||
		  *(p + 1) == '\0')
		{
		  if (*(p + 1) == '\0')
		    break;

		  p += 1;
		}
	      else if (*(p + 1) == '.' &&
		       (*(p + 2) == G_DIR_SEPARATOR ||
			*(p + 2) == '\0'))
		{
		  if (q > filename + 1)
		    {
		      q--;
		      while (q > filename + 1 &&
			     *(q - 1) != G_DIR_SEPARATOR)
			q--;
		    }

		  if (*(p + 2) == '\0')
		    break;

		  p += 2;
		}
	      else
		{
		  *q++ = *p;
		  last_was_slash = FALSE;
		}
	    }
	  else
	    {
	      *q++ = *p;
	      last_was_slash = FALSE;
	    }
	}

      p++;
    }

  if (q > filename + 1 && *(q - 1) == G_DIR_SEPARATOR)
    q--;

  *q = '\0';
}

static gchar *
follow_links (const gchar *path)
{
  gchar *target;
  gchar *d, *s;
  gchar *path2 = NULL;

  path2 = g_strdup (path);
  while (g_file_test (path2, G_FILE_TEST_IS_SYMLINK))
    {
      target = g_file_read_link (path2, NULL);

      if (target)
	{
	  if (g_path_is_absolute (target))
	    path2 = target;
	  else
	    {
	      d = g_path_get_dirname (path2);
	      s = g_build_filename (d, target, NULL);
	      g_free (d);
	      g_free (target);
	      g_free (path2);
	      path2 = s;
	    }
	}
      else
	break;
    }

  if (strcmp (path, path2) == 0)
    {
      g_free (path2);
      path2 = NULL;
    }

  return path2;
}

static void
maybe_cache_image_data (Image       *image,
			const gchar *path)
{
  if (!index_only && !image->image_data &&
      (g_str_has_suffix (path, ".png") || g_str_has_suffix (path, ".xpm")))
    {
      GdkPixbuf *pixbuf;
      ImageData *idata;
      gchar *path2;

      idata = g_hash_table_lookup (image_data_hash, path);
      path2 = follow_links (path);

      if (path2)
	{
	  ImageData *idata2;

	  canonicalize_filename (path2);

	  idata2 = g_hash_table_lookup (image_data_hash, path2);

	  if (idata && idata2 && idata != idata2)
	    g_error ("different idatas found for symlinked '%s' and '%s'\n",
		     path, path2);

	  if (idata && !idata2)
	    g_hash_table_insert (image_data_hash, g_strdup (path2), idata);

	  if (!idata && idata2)
	    {
	      g_hash_table_insert (image_data_hash, g_strdup (path), idata2);
	      idata = idata2;
	    }
	}

      if (!idata)
	{
	  idata = g_new0 (ImageData, 1);
	  g_hash_table_insert (image_data_hash, g_strdup (path), idata);
	  if (path2)
	    g_hash_table_insert (image_data_hash, g_strdup (path2), idata);
	}

      if (!idata->has_pixdata)
	{
	  pixbuf = gdk_pixbuf_new_from_file (path, NULL);

	  if (pixbuf)
	    {
G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
	      gdk_pixdata_from_pixbuf (&idata->pixdata, pixbuf, FALSE);
G_GNUC_END_IGNORE_DEPRECATIONS;
	      idata->size = idata->pixdata.length + 8;
	      idata->has_pixdata = TRUE;
	    }
	}

      image->image_data = idata;

      g_free (path2);
    }
}

static void
maybe_cache_icon_data (Image       *image,
                       const gchar *path)
{
  if (g_str_has_suffix (path, ".icon"))
    {
      IconData *idata = NULL;
      gchar *path2 = NULL;

      idata = g_hash_table_lookup (icon_data_hash, path);
      path2 = follow_links (path);

      if (path2)
	{
	  IconData *idata2;

	  canonicalize_filename (path2);

	  idata2 = g_hash_table_lookup (icon_data_hash, path2);

	  if (idata && idata2 && idata != idata2)
	    g_error ("different idatas found for symlinked '%s' and '%s'\n",
		     path, path2);

	  if (idata && !idata2)
	    g_hash_table_insert (icon_data_hash, g_strdup (path2), idata);

	  if (!idata && idata2)
	    {
	      g_hash_table_insert (icon_data_hash, g_strdup (path), idata2);
	      idata = idata2;
	    }
	}

      if (!idata)
	{
	  idata = load_icon_data (path);
	  g_hash_table_insert (icon_data_hash, g_strdup (path), idata);
	  if (path2)
	    g_hash_table_insert (icon_data_hash, g_strdup (path2), idata);
        }

      image->icon_data = idata;

      g_free (path2);
    }
}

/*
 * Finds all dir separators and replaces them with “/”.
 * This makes sure that only /-separated paths are written in cache files,
 * maintaining compatibility with theme index files that use slashes as
 * directory separators on all platforms.
 */
static void
replace_backslashes_with_slashes (gchar *path)
{
  size_t i;
  if (path == NULL)
    return;
  for (i = 0; path[i]; i++)
    if (G_IS_DIR_SEPARATOR (path[i]))
      path[i] = '/';
}

static GList *
scan_directory (const gchar *base_path,
		const gchar *subdir,
		GHashTable  *files,
		GList       *directories,
		gint         depth)
{
  GHashTable *dir_hash;
  GDir *dir;
  const gchar *name;
  gchar *dir_path;
  gboolean dir_added = FALSE;
  guint dir_index = 0xffff;

  dir_path = g_build_path ("/", base_path, subdir, NULL);

  /* FIXME: Use the gerror */
  dir = g_dir_open (dir_path, 0, NULL);

  if (!dir)
    return directories;

  dir_hash = g_hash_table_new (g_str_hash, g_str_equal);

  while ((name = g_dir_read_name (dir)))
    {
      gchar *path;
      gboolean retval;
      int flags = 0;
      Image *image;
      gchar *basename, *dot;

      path = g_build_filename (dir_path, name, NULL);

      retval = g_file_test (path, G_FILE_TEST_IS_DIR);
      if (retval)
	{
	  gchar *subsubdir;

	  if (subdir)
	    subsubdir = g_build_path ("/", subdir, name, NULL);
	  else
	    subsubdir = g_strdup (name);
	  directories = scan_directory (base_path, subsubdir, files,
					directories, depth + 1);
	  g_free (subsubdir);

	  continue;
	}

      /* ignore images in the toplevel directory */
      if (subdir == NULL)
        continue;

      retval = g_file_test (path, G_FILE_TEST_IS_REGULAR);
      if (retval)
	{
	  if (g_str_has_suffix (name, ".png"))
	    flags |= HAS_SUFFIX_PNG;
	  else if (g_str_has_suffix (name, ".svg"))
	    flags |= HAS_SUFFIX_SVG;
	  else if (g_str_has_suffix (name, ".xpm"))
	    flags |= HAS_SUFFIX_XPM;
	  else if (g_str_has_suffix (name, ".icon"))
	    flags |= HAS_ICON_FILE;

	  if (flags == 0)
	    continue;

	  basename = g_strdup (name);
	  dot = strrchr (basename, '.');
	  *dot = '\0';

	  image = g_hash_table_lookup (dir_hash, basename);
	  if (!image)
	    {
	      if (!dir_added)
		{
		  dir_added = TRUE;
		  if (subdir)
		    {
		      dir_index = g_list_length (directories);
		      directories = g_list_append (directories, g_strdup (subdir));
		    }
		  else
		    dir_index = 0xffff;
		}

	      image = g_new0 (Image, 1);
	      image->dir_index = dir_index;
	      g_hash_table_insert (dir_hash, g_strdup (basename), image);
	    }

	  image->flags |= flags;

	  maybe_cache_image_data (image, path);
          maybe_cache_icon_data (image, path);

	  g_free (basename);
	}

      g_free (path);
    }

  g_dir_close (dir);

  /* Move dir into the big file hash */
  g_hash_table_foreach_remove (dir_hash, foreach_remove_func, files);

  g_hash_table_destroy (dir_hash);

  return directories;
}

typedef struct _HashNode HashNode;

struct _HashNode
{
  HashNode *next;
  gchar *name;
  GList *image_list;
  gint offset;
};

static guint
icon_name_hash (gconstpointer key)
{
  const signed char *p = key;
  guint32 h = *p;

  if (h)
    for (p += 1; *p != '\0'; p++)
      h = (h << 5) - h + *p;

  return h;
}

typedef struct {
  gint size;
  HashNode **nodes;
} HashContext;

static gboolean
convert_to_hash (gpointer key, gpointer value, gpointer user_data)
{
  HashContext *context = user_data;
  guint hash;
  HashNode *node;

  hash = icon_name_hash (key) % context->size;

  node = g_new0 (HashNode, 1);
  node->next = NULL;
  node->name = key;
  node->image_list = value;

  if (context->nodes[hash] != NULL)
    node->next = context->nodes[hash];

  context->nodes[hash] = node;

  return TRUE;
}

static GHashTable *string_pool = NULL;

static int
find_string (const gchar *n)
{
  return GPOINTER_TO_INT (g_hash_table_lookup (string_pool, n));
}

static void
add_string (const gchar *n, int offset)
{
  g_hash_table_insert (string_pool, (gpointer) n, GINT_TO_POINTER (offset));
}

static gboolean
write_string (FILE *cache, const gchar *n)
{
  gchar *s;
  int i, l;

  l = ALIGN_VALUE (strlen (n) + 1, 4);

  s = g_malloc0 (l);
  strcpy (s, n);

  i = fwrite (s, l, 1, cache);

  g_free (s);

  return i == 1;

}

static gboolean
write_card16 (FILE *cache, guint16 n)
{
  int i;

  n = GUINT16_TO_BE (n);

  i = fwrite ((char *)&n, 2, 1, cache);

  return i == 1;
}

static gboolean
write_card32 (FILE *cache, guint32 n)
{
  int i;

  n = GUINT32_TO_BE (n);

  i = fwrite ((char *)&n, 4, 1, cache);

  return i == 1;
}


static gboolean
write_image_data (FILE *cache, ImageData *image_data, int offset)
{
  guint8 *s;
  guint len;
  gint i;
  GdkPixdata *pixdata = &image_data->pixdata;

  /* Type 0 is GdkPixdata */
  if (!write_card32 (cache, 0))
    return FALSE;

G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  s = gdk_pixdata_serialize (pixdata, &len);
G_GNUC_END_IGNORE_DEPRECATIONS;

  if (!write_card32 (cache, len))
    {
      g_free (s);
      return FALSE;
    }

  i = fwrite (s, len, 1, cache);

  g_free (s);

  return i == 1;
}

static gboolean
write_icon_data (FILE *cache, IconData *icon_data, int offset)
{
  int ofs = offset + 12;
  int j;
  int tmp, tmp2;

  if (icon_data->has_embedded_rect)
    {
      if (!write_card32 (cache, ofs))
        return FALSE;

       ofs += 8;
    }
  else
    {
      if (!write_card32 (cache, 0))
        return FALSE;
    }

  if (icon_data->n_attach_points > 0)
    {
      if (!write_card32 (cache, ofs))
        return FALSE;

      ofs += 4 + 4 * icon_data->n_attach_points;
    }
  else
    {
      if (!write_card32 (cache, 0))
        return FALSE;
    }

  if (icon_data->n_display_names > 0)
    {
      if (!write_card32 (cache, ofs))
	return FALSE;
    }
  else
    {
      if (!write_card32 (cache, 0))
        return FALSE;
    }

  if (icon_data->has_embedded_rect)
    {
      if (!write_card16 (cache, icon_data->x0) ||
          !write_card16 (cache, icon_data->y0) ||
	  !write_card16 (cache, icon_data->x1) ||
	  !write_card16 (cache, icon_data->y1))
        return FALSE;
    }

  if (icon_data->n_attach_points > 0)
    {
      if (!write_card32 (cache, icon_data->n_attach_points))
        return FALSE;

      for (j = 0; j < 2 * icon_data->n_attach_points; j++)
        {
          if (!write_card16 (cache, icon_data->attach_points[j]))
            return FALSE;
        }
    }

  if (icon_data->n_display_names > 0)
    {
      if (!write_card32 (cache, icon_data->n_display_names))
        return FALSE;

      ofs += 4 + 8 * icon_data->n_display_names;

      tmp = ofs;
      for (j = 0; j < 2 * icon_data->n_display_names; j++)
        {
          tmp2 = find_string (icon_data->display_names[j]);
          if (tmp2 == 0 || tmp2 == -1)
            {
              tmp2 = tmp;
              tmp += ALIGN_VALUE (strlen (icon_data->display_names[j]) + 1, 4);
              /* We're playing a little game with negative
               * offsets here to handle duplicate strings in
               * the array.
               */
              add_string (icon_data->display_names[j], -tmp2);
            }
          else if (tmp2 < 0)
            {
              tmp2 = -tmp2;
            }

          if (!write_card32 (cache, tmp2))
            return FALSE;

        }

      g_assert (ofs == ftell (cache));
      for (j = 0; j < 2 * icon_data->n_display_names; j++)
        {
          tmp2 = find_string (icon_data->display_names[j]);
          g_assert (tmp2 != 0 && tmp2 != -1);
          if (tmp2 < 0)
            {
              tmp2 = -tmp2;
              g_assert (tmp2 == ftell (cache));
              add_string (icon_data->display_names[j], tmp2);
              if (!write_string (cache, icon_data->display_names[j]))
                return FALSE;
            }
        }
    }

  return TRUE;
}

static gboolean
write_header (FILE *cache, guint32 dir_list_offset)
{
  return (write_card16 (cache, MAJOR_VERSION) &&
	  write_card16 (cache, MINOR_VERSION) &&
	  write_card32 (cache, HASH_OFFSET) &&
	  write_card32 (cache, dir_list_offset));
}

static gint
get_image_meta_data_size (Image *image)
{
  gint i;

  /* The complication with storing the size in both
   * IconData and Image is necessary since we attribute
   * the size of the IconData only to the first Image
   * using it (at which time it is written out in the
   * cache). Later Images just refer to the written out
   * IconData via the offset.
   */
  if (image->icon_data_size == 0)
    {
      if (image->icon_data && image->icon_data->size < 0)
	{
          IconData *data = image->icon_data;

          data->size = 0;

          if (data->has_embedded_rect ||
              data->n_attach_points > 0 ||
              data->n_display_names > 0)
            data->size += 12;

          if (data->has_embedded_rect)
            data->size += 8;

          if (data->n_attach_points > 0)
            data->size += 4 + data->n_attach_points * 4;

          if (data->n_display_names > 0)
            {
              data->size += 4 + 8 * data->n_display_names;

              for (i = 0; data->display_names[i]; i++)
                {
                  int poolv;
                  if ((poolv = find_string (data->display_names[i])) == 0)
                    {
                      data->size += ALIGN_VALUE (strlen (data->display_names[i]) + 1, 4);
                      /* Adding the string to the pool with -1
                       * to indicate that it hasn't been written out
                       * to the cache yet. We still need it in the
                       * pool in case the same string occurs twice
                       * during a get_single_node_size() calculation.
                       */
                      add_string (data->display_names[i], -1);
                    }
                }
           }

	  image->icon_data_size = data->size;
	  data->size = 0;
	}
    }

  g_assert (image->icon_data_size % 4 == 0);

  return image->icon_data_size;
}

static gint
get_image_pixel_data_size (Image *image)
{
  /* The complication with storing the size in both
   * ImageData and Image is necessary since we attribute
   * the size of the ImageData only to the first Image
   * using it (at which time it is written out in the
   * cache). Later Images just refer to the written out
   * ImageData via the offset.
   */
  if (image->pixel_data_size == 0)
    {
      if (image->image_data &&
	  image->image_data->has_pixdata)
	{
	  image->pixel_data_size = image->image_data->size;
	  image->image_data->size = 0;
	}
    }

  g_assert (image->pixel_data_size % 4 == 0);

  return image->pixel_data_size;
}

static gint
get_image_data_size (Image *image)
{
  gint len;

  len = 0;

  len += get_image_pixel_data_size (image);
  len += get_image_meta_data_size (image);

  /* Even if len is zero, we need to reserve space to
   * write the ImageData, unless this is an .svg without
   * .icon, in which case both image_data and icon_data
   * are NULL.
   */
  if (len > 0 || image->image_data || image->icon_data)
    len += 8;

  return len;
}

static void
get_single_node_size (HashNode *node, int *node_size, int *image_data_size)
{
  GList *list;

  /* Node pointers */
  *node_size = 12;

  /* Name */
  if (find_string (node->name) == 0)
    {
      *node_size += ALIGN_VALUE (strlen (node->name) + 1, 4);
      add_string (node->name, -1);
    }

  /* Image list */
  *node_size += 4 + g_list_length (node->image_list) * 8;

  /* Image data */
  *image_data_size = 0;
  for (list = node->image_list; list; list = list->next)
    {
      Image *image = list->data;

      *image_data_size += get_image_data_size (image);
    }
}

static gboolean
write_bucket (FILE *cache, HashNode *node, int *offset)
{
  while (node != NULL)
    {
      int node_size, image_data_size;
      int next_offset, image_data_offset;
      int data_offset;
      int name_offset;
      int name_size;
      int image_list_offset;
      int i, len;
      GList *list;

      g_assert (*offset == ftell (cache));

      node->offset = *offset;

      get_single_node_size (node, &node_size, &image_data_size);
      g_assert (node_size % 4 == 0);
      g_assert (image_data_size % 4 == 0);
      image_data_offset = *offset + node_size;
      next_offset = *offset + node_size + image_data_size;
      /* Chain offset */
      if (node->next != NULL)
        {
          if (!write_card32 (cache, next_offset))
            return FALSE;
        }
      else
        {
          if (!write_card32 (cache, 0xffffffff))
            return FALSE;
        }

      name_size = 0;
      name_offset = find_string (node->name);
      if (name_offset <= 0)
        {
          name_offset = *offset + 12;
          name_size = ALIGN_VALUE (strlen (node->name) + 1, 4);
          add_string (node->name, name_offset);
        }
      if (!write_card32 (cache, name_offset))
        return FALSE;

      image_list_offset = *offset + 12 + name_size;
      if (!write_card32 (cache, image_list_offset))
        return FALSE;

      /* Icon name */
      if (name_size > 0)
        {
          if (!write_string (cache, node->name))
            return FALSE;
        }

      /* Image list */
      len = g_list_length (node->image_list);
      if (!write_card32 (cache, len))
        return FALSE;

      list = node->image_list;
      data_offset = image_data_offset;
      for (i = 0; i < len; i++)
        {
          Image *image = list->data;
          int image_size = get_image_data_size (image);

          /* Directory index */
          if (!write_card16 (cache, image->dir_index))
            return FALSE;

          /* Flags */
          if (!write_card16 (cache, image->flags))
            return FALSE;

          /* Image data offset */
          if (image_size > 0)
            {
              if (!write_card32 (cache, data_offset))
                return FALSE;
              data_offset += image_size;
            }
          else
            {
              if (!write_card32 (cache, 0))
                return FALSE;
            }

          list = list->next;
        }

      /* Now write the image data */
      list = node->image_list;
      for (i = 0; i < len; i++, list = list->next)
        {
          Image *image = list->data;
          int pixel_data_size = get_image_pixel_data_size (image);
          int meta_data_size = get_image_meta_data_size (image);

          if (get_image_data_size (image) == 0)
            continue;

          /* Pixel data */
          if (pixel_data_size > 0)
            {
              image->image_data->offset = image_data_offset + 8;
              if (!write_card32 (cache, image->image_data->offset))
                return FALSE;
            }
          else
            {
              if (!write_card32 (cache, (guint32) (image->image_data ? image->image_data->offset : 0)))
                return FALSE;
            }

          if (meta_data_size > 0)
            {
              image->icon_data->offset = image_data_offset + pixel_data_size + 8;
              if (!write_card32 (cache, image->icon_data->offset))
                return FALSE;
            }
          else
            {
              if (!write_card32 (cache, image->icon_data ? image->icon_data->offset : 0))
                return FALSE;
            }

          if (pixel_data_size > 0)
            {
              if (!write_image_data (cache, image->image_data, image->image_data->offset))
                return FALSE;
            }

          if (meta_data_size > 0)
            {
              if (!write_icon_data (cache, image->icon_data, image->icon_data->offset))
                return FALSE;
            }

          image_data_offset += pixel_data_size + meta_data_size + 8;
        }

      *offset = next_offset;
      node = node->next;
    }

  return TRUE;
}

static gboolean
write_hash_table (FILE *cache, HashContext *context, int *new_offset)
{
  int offset = HASH_OFFSET;
  int node_offset;
  int i;

  if (!(write_card32 (cache, context->size)))
    return FALSE;

  offset += 4;
  node_offset = offset + context->size * 4;
  /* Just write zeros here, we will rewrite this later */
  for (i = 0; i < context->size; i++)
    {
      if (!write_card32 (cache, 0))
	return FALSE;
    }

  /* Now write the buckets */
  for (i = 0; i < context->size; i++)
    {
      if (!context->nodes[i])
	continue;

      g_assert (node_offset % 4 == 0);
      if (!write_bucket (cache, context->nodes[i], &node_offset))
	return FALSE;
    }

  *new_offset = node_offset;

  /* Now write out the bucket offsets */

  fseek (cache, offset, SEEK_SET);

  for (i = 0; i < context->size; i++)
    {
      if (context->nodes[i] != NULL)
        node_offset = context->nodes[i]->offset;
      else
	node_offset = 0xffffffff;
      if (!write_card32 (cache, node_offset))
        return FALSE;
    }

  fseek (cache, 0, SEEK_END);

  return TRUE;
}

static gboolean
write_dir_index (FILE *cache, int offset, GList *directories)
{
  int n_dirs;
  GList *d;
  char *dir;
  int tmp, tmp2;

  n_dirs = g_list_length (directories);

  if (!write_card32 (cache, n_dirs))
    return FALSE;

  offset += 4 + n_dirs * 4;

  tmp = offset;
  for (d = directories; d; d = d->next)
    {
      dir = d->data;

      tmp2 = find_string (dir);

      if (tmp2 == 0 || tmp2 == -1)
        {
          tmp2 = tmp;
          tmp += ALIGN_VALUE (strlen (dir) + 1, 4);
          /* We're playing a little game with negative
           * offsets here to handle duplicate strings in
           * the array, even though that should not
           * really happen for the directory index.
           */
          add_string (dir, -tmp2);
        }
      else if (tmp2 < 0)
        {
          tmp2 = -tmp2;
        }

      if (!write_card32 (cache, tmp2))
	return FALSE;
    }

  g_assert (offset == ftell (cache));
  for (d = directories; d; d = d->next)
    {
      dir = d->data;

      tmp2 = find_string (dir);
      g_assert (tmp2 != 0 && tmp2 != -1);
      if (tmp2 < 0)
        {
          tmp2 = -tmp2;
          g_assert (tmp2 == ftell (cache));
          add_string (dir, tmp2);
          if (!write_string (cache, dir))
	    return FALSE;
        }
    }

  return TRUE;
}

static gboolean
write_file (FILE *cache, GHashTable *files, GList *directories)
{
  HashContext context;
  int new_offset;

  /* Convert the hash table into something looking a bit more
   * like what we want to write to disk.
   */
  context.size = g_spaced_primes_closest (g_hash_table_size (files) / 3);
  context.nodes = g_new0 (HashNode *, context.size);

  g_hash_table_foreach_remove (files, convert_to_hash, &context);

  /* Now write the file */
  /* We write 0 as the directory list offset and go
   * back and change it later */
  if (!write_header (cache, 0))
    {
      g_printerr (_("Failed to write header\n"));
      return FALSE;
    }

  if (!write_hash_table (cache, &context, &new_offset))
    {
      g_printerr (_("Failed to write hash table\n"));
      return FALSE;
    }

  if (!write_dir_index (cache, new_offset, directories))
    {
      g_printerr (_("Failed to write folder index\n"));
      return FALSE;
    }

  rewind (cache);

  if (!write_header (cache, new_offset))
    {
      g_printerr (_("Failed to rewrite header\n"));
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_file (const gchar *file)
{
  GMappedFile *map;
  CacheInfo info;

  map = g_mapped_file_new (file, FALSE, NULL);
  if (!map)
    return FALSE;

  info.cache = g_mapped_file_get_contents (map);
  info.cache_size = g_mapped_file_get_length (map);
  info.n_directories = 0;
  info.flags = CHECK_OFFSETS|CHECK_STRINGS|CHECK_PIXBUFS;

  if (!_gtk_icon_cache_validate (&info))
    {
      g_mapped_file_unref (map);
      return FALSE;
    }

  g_mapped_file_unref (map);

  return TRUE;
}

/**
 * safe_fclose:
 * @f: A FILE* stream, must have underlying fd
 *
 * Unix defaults for data preservation after system crash
 * are unspecified, and many systems will eat your data
 * in this situation unless you explicitly fsync().
 *
 * Returns: %TRUE on success, %FALSE on failure, and will set errno()
 */
static gboolean
safe_fclose (FILE *f)
{
  int fd = fileno (f);
  g_assert (fd >= 0);
  if (fflush (f) == EOF)
    return FALSE;
#ifndef G_OS_WIN32
  if (fsync (fd) < 0)
    return FALSE;
#endif
  if (fclose (f) == EOF)
    return FALSE;
  return TRUE;
}

static void
build_cache (const gchar *path)
{
  gchar *cache_path, *tmp_cache_path;
#ifdef G_OS_WIN32
  gchar *bak_cache_path = NULL;
#endif
  GHashTable *files;
  FILE *cache;
  GStatBuf path_stat, cache_stat;
  struct utimbuf utime_buf;
  GList *directories = NULL;
  int fd;
  int retry_count = 0;
#ifndef G_OS_WIN32
  mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
#else
  int mode = _S_IWRITE | _S_IREAD;
#endif
#ifndef _O_BINARY
#define _O_BINARY 0
#endif

  tmp_cache_path = g_build_filename (path, "."CACHE_NAME, NULL);
  cache_path = g_build_filename (path, CACHE_NAME, NULL);

opentmp:
  if ((fd = g_open (tmp_cache_path, O_WRONLY | O_CREAT | O_EXCL | O_TRUNC | _O_BINARY, mode)) == -1)
    {
      if (retry_count == 0)
        {
          retry_count++;
          g_remove (tmp_cache_path);
          goto opentmp;
        }
      g_printerr (_("Failed to open file %s : %s\n"), tmp_cache_path, g_strerror (errno));
      exit (1);
    }

  cache = fdopen (fd, "wb");

  if (!cache)
    {
      g_printerr (_("Failed to write cache file: %s\n"), g_strerror (errno));
      exit (1);
    }

  files = g_hash_table_new (g_str_hash, g_str_equal);
  image_data_hash = g_hash_table_new (g_str_hash, g_str_equal);
  icon_data_hash = g_hash_table_new (g_str_hash, g_str_equal);
  string_pool = g_hash_table_new (g_str_hash, g_str_equal);

  directories = scan_directory (path, NULL, files, NULL, 0);

  if (g_hash_table_size (files) == 0)
    {
      /* Empty table, just close and remove the file */

      fclose (cache);
      g_unlink (tmp_cache_path);
      g_unlink (cache_path);
      exit (0);
    }

  /* FIXME: Handle failure */
  if (!write_file (cache, files, directories))
    {
      g_unlink (tmp_cache_path);
      exit (1);
    }

  if (!safe_fclose (cache))
    {
      g_printerr (_("Failed to write cache file: %s\n"), g_strerror (errno));
      g_unlink (tmp_cache_path);
      exit (1);
    }
  cache = NULL;

  g_list_free_full (directories, g_free);

  if (!validate_file (tmp_cache_path))
    {
      g_printerr (_("The generated cache was invalid.\n"));
      /*g_unlink (tmp_cache_path);*/
      exit (1);
    }

#ifdef G_OS_WIN32
  if (g_file_test (cache_path, G_FILE_TEST_EXISTS))
    {
      bak_cache_path = g_strconcat (cache_path, ".bak", NULL);
      g_unlink (bak_cache_path);
      if (g_rename (cache_path, bak_cache_path) == -1)
	{
          int errsv = errno;

	  g_printerr (_("Could not rename %s to %s: %s, removing %s then.\n"),
		      cache_path, bak_cache_path,
		      g_strerror (errsv),
		      cache_path);
	  g_unlink (cache_path);
	  bak_cache_path = NULL;
	}
    }
#endif

  if (g_rename (tmp_cache_path, cache_path) == -1)
    {
      int errsv = errno;

      g_printerr (_("Could not rename %s to %s: %s\n"),
		  tmp_cache_path, cache_path,
		  g_strerror (errsv));
      g_unlink (tmp_cache_path);
#ifdef G_OS_WIN32
      if (bak_cache_path != NULL)
	if (g_rename (bak_cache_path, cache_path) == -1)
          {
            errsv = errno;

            g_printerr (_("Could not rename %s back to %s: %s.\n"),
                        bak_cache_path, cache_path,
                        g_strerror (errsv));
          }
#endif
      exit (1);
    }
#ifdef G_OS_WIN32
  if (bak_cache_path != NULL)
    g_unlink (bak_cache_path);
#endif

  /* Update time */
  /* FIXME: What do do if an error occurs here? */
  if (g_stat (path, &path_stat) < 0 ||
      g_stat (cache_path, &cache_stat))
    exit (1);

  utime_buf.actime = path_stat.st_atime;
  utime_buf.modtime = cache_stat.st_mtime;
#if GLIB_CHECK_VERSION (2, 17, 1)
  g_utime (path, &utime_buf);
#else
  utime (path, &utime_buf);
#endif

  if (!quiet)
    g_printerr (_("Cache file created successfully.\n"));
}

static void
write_csource (const gchar *path)
{
  gchar *cache_path;
  gchar *data;
  gsize len;
  gint i;

  cache_path = g_build_filename (path, CACHE_NAME, NULL);
  if (!g_file_get_contents (cache_path, &data, &len, NULL))
    exit (1);

  g_printf ("#ifdef __SUNPRO_C\n");
  g_printf ("#pragma align 4 (%s)\n", var_name);
  g_printf ("#endif\n");

  g_printf ("#ifdef __GNUC__\n");
  g_printf ("static const guint8 %s[] __attribute__ ((__aligned__ (4))) = \n", var_name);
  g_printf ("#else\n");
  g_printf ("static const guint8 %s[] = \n", var_name);
  g_printf ("#endif\n");

  g_printf ("{\n");
  for (i = 0; i < len - 1; i++)
    {
      if (i %12 == 0)
	g_printf ("  ");
      g_printf ("0x%02x, ", (guint8)data[i]);
      if (i % 12 == 11)
        g_printf ("\n");
    }

  g_printf ("0x%02x\n};\n", (guint8)data[i]);
}

static GOptionEntry args[] = {
  { "force", 'f', 0, G_OPTION_ARG_NONE, &force_update, N_("Overwrite an existing cache, even if up to date"), NULL },
  { "ignore-theme-index", 't', 0, G_OPTION_ARG_NONE, &ignore_theme_index, N_("Don't check for the existence of index.theme"), NULL },
  { "index-only", 'i', 0, G_OPTION_ARG_NONE, &index_only, N_("Don't include image data in the cache"), NULL },
  { "include-image-data", 0, G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &index_only, N_("Include image data in the cache"), NULL },
  { "source", 'c', 0, G_OPTION_ARG_STRING, &var_name, N_("Output a C header file"), "NAME" },
  { "quiet", 'q', 0, G_OPTION_ARG_NONE, &quiet, N_("Turn off verbose output"), NULL },
  { "validate", 'v', 0, G_OPTION_ARG_NONE, &validate, N_("Validate existing icon cache"), NULL },
  { NULL }
};

static void
printerr_handler (const gchar *string)
{
  const gchar *charset;

  fputs (g_get_prgname (), stderr);
  fputs (": ", stderr);
  if (g_get_charset (&charset))
    fputs (string, stderr); /* charset is UTF-8 already */
  else
    {
      gchar *result;

      result = g_convert_with_fallback (string, -1, charset, "UTF-8", "?", NULL, NULL, NULL);

      if (result)
        {
          fputs (result, stderr);
          g_free (result);
        }

      fflush (stderr);
    }
}


int
main (int argc, char **argv)
{
  gchar *path;
  GOptionContext *context;

  if (argc < 2)
    return 0;

  g_set_printerr_handler (printerr_handler);

  setlocale (LC_ALL, "");

#ifdef ENABLE_NLS
  bindtextdomain (GETTEXT_PACKAGE, GTK_LOCALEDIR);
#ifdef HAVE_BIND_TEXTDOMAIN_CODESET
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
#endif
#endif

  context = g_option_context_new ("ICONPATH");
  g_option_context_add_main_entries (context, args, GETTEXT_PACKAGE);

  g_option_context_parse (context, &argc, &argv, NULL);

  path = argv[1];
#ifdef G_OS_WIN32
  path = g_locale_to_utf8 (path, -1, NULL, NULL, NULL);
#endif

  if (validate)
    {
       gchar *file = g_build_filename (path, CACHE_NAME, NULL);

       if (!g_file_test (file, G_FILE_TEST_IS_REGULAR))
         {
            if (!quiet)
              g_printerr (_("File not found: %s\n"), file);
            exit (1);
         }
       if (!validate_file (file))
         {
           if (!quiet)
             g_printerr (_("Not a valid icon cache: %s\n"), file);
           exit (1);
         }
       else
         {
           exit (0);
         }
    }

  if (!ignore_theme_index && !has_theme_index (path))
    {
      if (path)
	{
	  g_printerr (_("No theme index file.\n"));
	}
      else
	{
	  g_printerr (_("No theme index file in '%s'.\n"
		    "If you really want to create an icon cache here, use --ignore-theme-index.\n"), path);
	}

      return 1;
    }

  if (!force_update && is_cache_up_to_date (path))
    return 0;

  replace_backslashes_with_slashes (path);
  build_cache (path);

  if (strcmp (var_name, "-") != 0)
    write_csource (path);

  return 0;
}
