/* LIBGIMP - The GIMP Library
 * Copyright (C) 1995-1997 Peter Mattis and Spencer Kimball
 *
 * Thumbnail handling according to the Thumbnail Managing Standard.
 * http://triq.net/~pearl/thumbnail-spec/
 *
 * Copyright (C) 2001-2003  Sven Neumann <sven@gimp.org>
 *                          Michael Natterer <mitch@gimp.org>
 *
 * This library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <errno.h>
#include <string.h>
#include <sys/types.h>

#ifdef PLATFORM_OSX
#include <AppKit/AppKit.h>
#endif

#include <gio/gio.h>
#include <glib/gstdio.h>

#ifdef G_OS_WIN32
#include "libgimpbase/gimpwin32-io.h"
#endif

#include "gimpthumb-error.h"
#include "gimpthumb-types.h"
#include "gimpthumb-utils.h"

#include "libgimp/libgimp-intl.h"


/**
 * SECTION: gimpthumb-utils
 * @title: GimpThumb-utils
 * @short_description: Utility functions provided and used by libgimpthumb
 *
 * Utility functions provided and used by libgimpthumb
 **/


static gint           gimp_thumb_size       (GimpThumbSize  size);
static gchar        * gimp_thumb_png_lookup (const gchar   *name,
                                             const gchar   *basedir,
                                             GimpThumbSize *size) G_GNUC_MALLOC;
static const gchar  * gimp_thumb_png_name   (const gchar   *uri);
static void           gimp_thumb_exit       (void);



static gboolean      gimp_thumb_initialized = FALSE;
static gint          thumb_num_sizes        = 0;
static gint         *thumb_sizes            = NULL;
static const gchar **thumb_sizenames        = NULL;
static gchar        *thumb_dir              = NULL;
static gchar       **thumb_subdirs          = NULL;
static gchar        *thumb_fail_subdir      = NULL;


/**
 * gimp_thumb_init:
 * @creator: an ASCII string that identifies the thumbnail creator
 * @thumb_basedir: an absolute path or %NULL to use the default
 *
 * This function initializes the thumbnail system. It must be called
 * before any other functions from libgimpthumb are used. You may call
 * it more than once if you want to change the @thumb_basedir but if
 * you do that, you should make sure that no thread is still using the
 * library. Apart from this function, libgimpthumb is multi-thread
 * safe.
 *
 * The @creator string must be 7bit ASCII and should contain the name
 * of the software that creates the thumbnails. It is used to handle
 * thumbnail creation failures. See the spec for more details.
 *
 * Usually you will pass %NULL for @thumb_basedir. Thumbnails will
 * then be stored in the user's personal thumbnail directory as
 * defined in the spec. If you wish to use libgimpthumb to store
 * application-specific thumbnails, you can specify a different base
 * directory here.
 *
 * Return value: %TRUE if the library was successfully initialized.
 **/
gboolean
gimp_thumb_init (const gchar *creator,
                 const gchar *thumb_basedir)
{
  GEnumClass *enum_class;
  GEnumValue *enum_value;
  gint        i;

  g_return_val_if_fail (creator != NULL, FALSE);
  g_return_val_if_fail (thumb_basedir == NULL ||
                        g_path_is_absolute (thumb_basedir), FALSE);

  if (gimp_thumb_initialized)
    gimp_thumb_exit ();

  if (thumb_basedir)
    {
      thumb_dir = g_strdup (thumb_basedir);
    }
  else
    {
#ifdef PLATFORM_OSX

      NSAutoreleasePool *pool;
      NSArray           *path;
      NSString          *cache_dir;

      pool = [[NSAutoreleasePool alloc] init];

      path = NSSearchPathForDirectoriesInDomains (NSCachesDirectory,
                                                  NSUserDomainMask, YES);
      cache_dir = [path objectAtIndex:0];

      thumb_dir = g_build_filename ([cache_dir UTF8String], "org.freedesktop.thumbnails",
                                    NULL);

      [pool drain];

#else

      const gchar *home_dir = g_get_home_dir ();

      if (home_dir && g_file_test (home_dir, G_FILE_TEST_IS_DIR))
        {
          thumb_dir = g_build_filename (home_dir, ".thumbnails", NULL);
        }

#endif

      if (! thumb_dir)
        {
          gchar *name = g_filename_display_name (g_get_tmp_dir ());

          g_message (_("Cannot determine a valid home directory.\n"
                       "Thumbnails will be stored in the folder for "
                       "temporary files (%s) instead."), name);
          g_free (name);

          thumb_dir = g_build_filename (g_get_tmp_dir (), ".thumbnails", NULL);
        }
    }

  enum_class = g_type_class_ref (GIMP_TYPE_THUMB_SIZE);

  thumb_num_sizes = enum_class->n_values;
  thumb_sizes     = g_new (gint, thumb_num_sizes);
  thumb_sizenames = g_new (const gchar *, thumb_num_sizes);
  thumb_subdirs   = g_new (gchar *, thumb_num_sizes);

  for (i = 0, enum_value = enum_class->values;
       i < enum_class->n_values;
       i++, enum_value++)
    {
      thumb_sizes[i]     = enum_value->value;
      thumb_sizenames[i] = enum_value->value_nick;
      thumb_subdirs[i]   = g_build_filename (thumb_dir,
                                             enum_value->value_nick, NULL);
    }

  thumb_fail_subdir = thumb_subdirs[0];
  thumb_subdirs[0]  = g_build_filename (thumb_fail_subdir, creator, NULL);

  g_type_class_unref (enum_class);

  gimp_thumb_initialized = TRUE;

  return gimp_thumb_initialized;
}

/**
 * gimp_thumb_get_thumb_dir:
 * @size: a GimpThumbSize
 *
 * Retrieve the name of the thumbnail folder for a specific size. The
 * returned pointer will become invalid if gimp_thumb_init() is used
 * again. It must not be changed or freed.
 *
 * Return value: the thumbnail directory in the encoding of the filesystem
 **/
const gchar *
gimp_thumb_get_thumb_dir (GimpThumbSize  size)
{
  g_return_val_if_fail (gimp_thumb_initialized, NULL);

  size = gimp_thumb_size (size);

  return thumb_subdirs[size];
}

/**
 * gimp_thumb_get_thumb_dir_local:
 * @dirname: the basename of the dir, without the actual dirname itself
 * @size:    a GimpThumbSize
 *
 * Retrieve the name of the local thumbnail folder for a specific
 * size.  Unlike gimp_thumb_get_thumb_dir() the returned string is not
 * constant and should be free'd when it is not any longer needed.
 *
 * Return value: the thumbnail directory in the encoding of the filesystem
 *
 * Since: GIMP 2.2
 **/
gchar *
gimp_thumb_get_thumb_dir_local (const gchar   *dirname,
                                GimpThumbSize  size)
{
  g_return_val_if_fail (gimp_thumb_initialized, NULL);
  g_return_val_if_fail (dirname != NULL, NULL);
  g_return_val_if_fail (size > GIMP_THUMB_SIZE_FAIL, NULL);

  size = gimp_thumb_size (size);

  return g_build_filename (dirname, thumb_sizenames[size], NULL);
}

/**
 * gimp_thumb_ensure_thumb_dir:
 * @size: a GimpThumbSize
 * @error: return location for possible errors
 *
 * This function checks if the directory that is required to store
 * thumbnails for a particular @size exist and attempts to create it
 * if necessary.
 *
 * You shouldn't have to call this function directly since
 * gimp_thumbnail_save_thumb() and gimp_thumbnail_save_failure() will
 * do this for you.
 *
 * Return value: %TRUE is the directory exists, %FALSE if it could not
 *               be created
 **/
gboolean
gimp_thumb_ensure_thumb_dir (GimpThumbSize   size,
                             GError        **error)
{
  g_return_val_if_fail (gimp_thumb_initialized, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  size = gimp_thumb_size (size);

  if (g_file_test (thumb_subdirs[size], G_FILE_TEST_IS_DIR))
    return TRUE;

  if (g_file_test (thumb_dir, G_FILE_TEST_IS_DIR) ||
      (g_mkdir_with_parents (thumb_dir, S_IRUSR | S_IWUSR | S_IXUSR) == 0))
    {
      if (size == 0)
        g_mkdir_with_parents (thumb_fail_subdir, S_IRUSR | S_IWUSR | S_IXUSR);

      g_mkdir_with_parents (thumb_subdirs[size], S_IRUSR | S_IWUSR | S_IXUSR);
    }

  if (g_file_test (thumb_subdirs[size], G_FILE_TEST_IS_DIR))
    return TRUE;

  g_set_error (error,
               GIMP_THUMB_ERROR, GIMP_THUMB_ERROR_MKDIR,
               _("Failed to create thumbnail folder '%s'."),
               thumb_subdirs[size]);

  return FALSE;
}

/**
 * gimp_thumb_ensure_thumb_dir_local:
 * @dirname: the basename of the dir, without the actual dirname itself
 * @size:    a GimpThumbSize
 * @error:   return location for possible errors
 *
 * This function checks if the directory that is required to store
 * local thumbnails for a particular @size exist and attempts to
 * create it if necessary.
 *
 * You shouldn't have to call this function directly since
 * gimp_thumbnail_save_thumb_local() will do this for you.
 *
 * Return value: %TRUE is the directory exists, %FALSE if it could not
 *               be created
 *
 * Since: GIMP 2.2
 **/
gboolean
gimp_thumb_ensure_thumb_dir_local (const gchar    *dirname,
                                   GimpThumbSize   size,
                                   GError        **error)
{
  gchar *basedir;
  gchar *subdir;

  g_return_val_if_fail (gimp_thumb_initialized, FALSE);
  g_return_val_if_fail (dirname != NULL, FALSE);
  g_return_val_if_fail (g_path_is_absolute (dirname), FALSE);
  g_return_val_if_fail (size > GIMP_THUMB_SIZE_FAIL, FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  size = gimp_thumb_size (size);

  subdir = g_build_filename (dirname,
                             ".thumblocal", thumb_sizenames[size],
                             NULL);

  if (g_file_test (subdir, G_FILE_TEST_IS_DIR))
    {
      g_free (subdir);
      return TRUE;
    }

  basedir = g_build_filename (dirname, ".thumblocal", NULL);

  if (g_file_test (basedir, G_FILE_TEST_IS_DIR) ||
      (g_mkdir (thumb_dir, S_IRUSR | S_IWUSR | S_IXUSR) == 0))
    {
      g_mkdir (subdir, S_IRUSR | S_IWUSR | S_IXUSR);
    }

  g_free (basedir);

  if (g_file_test (subdir, G_FILE_TEST_IS_DIR))
    {
      g_free (subdir);
      return TRUE;
    }

  g_set_error (error,
               GIMP_THUMB_ERROR, GIMP_THUMB_ERROR_MKDIR,
               _("Failed to create thumbnail folder '%s'."),
               subdir);
  g_free (subdir);

  return FALSE;
}

/**
 * gimp_thumb_name_from_uri:
 * @uri: an escaped URI
 * @size: a #GimpThumbSize
 *
 * Creates the name of the thumbnail file of the specified @size that
 * belongs to an image file located at the given @uri.
 *
 * Return value: a newly allocated filename in the encoding of the
 *               filesystem or %NULL if @uri points to the user's
 *               thumbnail repository.
 **/
gchar *
gimp_thumb_name_from_uri (const gchar   *uri,
                          GimpThumbSize  size)
{
  g_return_val_if_fail (gimp_thumb_initialized, NULL);
  g_return_val_if_fail (uri != NULL, NULL);

  if (strstr (uri, thumb_dir))
    return NULL;

  size = gimp_thumb_size (size);

  return g_build_filename (thumb_subdirs[size],
                           gimp_thumb_png_name (uri),
                           NULL);
}

/**
 * gimp_thumb_name_from_uri_local:
 * @uri: an escaped URI
 * @size: a #GimpThumbSize
 *
 * Creates the name of a local thumbnail file of the specified @size
 * that belongs to an image file located at the given @uri. Local
 * thumbnails have been introduced with version 0.7 of the spec.
 *
 * Return value: a newly allocated filename in the encoding of the
 *               filesystem or %NULL if @uri is a remote file or
 *               points to the user's thumbnail repository.
 *
 * Since: GIMP 2.2
 **/
gchar *
gimp_thumb_name_from_uri_local (const gchar   *uri,
                                GimpThumbSize  size)
{
  gchar *filename;
  gchar *result = NULL;

  g_return_val_if_fail (gimp_thumb_initialized, NULL);
  g_return_val_if_fail (uri != NULL, NULL);
  g_return_val_if_fail (size > GIMP_THUMB_SIZE_FAIL, NULL);

  if (strstr (uri, thumb_dir))
    return NULL;

  filename = _gimp_thumb_filename_from_uri (uri);

  if (filename)
    {
      const gchar *baseuri = strrchr (uri, '/');

      if (baseuri && baseuri[0] && baseuri[1])
        {
          gchar *dirname = g_path_get_dirname (filename);
          gint   i       = gimp_thumb_size (size);

          result = g_build_filename (dirname,
                                     ".thumblocal", thumb_sizenames[i],
                                     gimp_thumb_png_name (uri),
                                     NULL);

          g_free (dirname);
        }

      g_free (filename);
    }

  return result;
}

/**
 * gimp_thumb_find_thumb:
 * @uri: an escaped URI
 * @size: pointer to a #GimpThumbSize
 *
 * This function attempts to locate a thumbnail for the given
 * @uri. First it tries the size that is stored at @size. If no
 * thumbnail of that size is found, it will look for a larger
 * thumbnail, then falling back to a smaller size.
 *
 * If the user's thumbnail repository doesn't provide a thumbnail but
 * a local thumbnail repository exists for the folder the image is
 * located in, the same search is done among the local thumbnails (if
 * there are any).
 *
 * If a thumbnail is found, it's size is written to the variable
 * pointer to by @size and the file location is returned.
 *
 * Return value: a newly allocated string in the encoding of the
 *               filesystem or %NULL if no thumbnail for @uri was found
 **/
gchar *
gimp_thumb_find_thumb (const gchar   *uri,
                       GimpThumbSize *size)
{
  gchar *result;

  g_return_val_if_fail (gimp_thumb_initialized, NULL);
  g_return_val_if_fail (uri != NULL, NULL);
  g_return_val_if_fail (size != NULL, NULL);
  g_return_val_if_fail (*size > GIMP_THUMB_SIZE_FAIL, NULL);

  result = gimp_thumb_png_lookup (gimp_thumb_png_name (uri), NULL, size);

  if (! result)
    {
      gchar *filename = _gimp_thumb_filename_from_uri (uri);

      if (filename)
        {
          const gchar *baseuri = strrchr (uri, '/');

          if (baseuri && baseuri[0] && baseuri[1])
            {
              gchar *dirname = g_path_get_dirname (filename);

              result = gimp_thumb_png_lookup (gimp_thumb_png_name (baseuri + 1),
                                              dirname, size);

              g_free (dirname);
            }

          g_free (filename);
        }
    }

  return result;
}

/**
 * gimp_thumb_file_test:
 * @filename: a filename in the encoding of the filesystem
 * @mtime: return location for modification time
 * @size: return location for file size
 * @err_no: return location for system "errno"
 *
 * This is a convenience and portability wrapper around stat(). It
 * checks if the given @filename exists and returns modification time
 * and file size in 64bit integer values.
 *
 * Return value: The type of the file, or #GIMP_THUMB_FILE_TYPE_NONE if
 *               the file doesn't exist.
 **/
GimpThumbFileType
gimp_thumb_file_test (const gchar *filename,
                      gint64      *mtime,
                      gint64      *size,
                      gint        *err_no)
{
  GimpThumbFileType  type = GIMP_THUMB_FILE_TYPE_NONE;
  GFile             *file;
  GFileInfo         *info;

  g_return_val_if_fail (filename != NULL, FALSE);

  file = g_file_new_for_path (filename);

  info = g_file_query_info (file,
                            G_FILE_ATTRIBUTE_STANDARD_TYPE ","
                            G_FILE_ATTRIBUTE_STANDARD_SIZE ","
                            G_FILE_ATTRIBUTE_TIME_MODIFIED,
                            G_FILE_QUERY_INFO_NONE,
                            NULL, NULL);

  if (info)
    {
      if (mtime)
        *mtime =
          g_file_info_get_attribute_uint64 (info,
                                            G_FILE_ATTRIBUTE_TIME_MODIFIED);

      if (size)
        *size = g_file_info_get_size (info);

      if (err_no)
        *err_no = 0;

      switch (g_file_info_get_attribute_uint32 (info,
                                                G_FILE_ATTRIBUTE_STANDARD_TYPE))
        {
        case G_FILE_TYPE_REGULAR:
          type = GIMP_THUMB_FILE_TYPE_REGULAR;
          break;

        case G_FILE_TYPE_DIRECTORY:
          type = GIMP_THUMB_FILE_TYPE_FOLDER;
          break;

        default:
          type = GIMP_THUMB_FILE_TYPE_SPECIAL;
          break;
        }

      g_object_unref (info);
    }
  else
    {
      if (mtime)  *mtime  = 0;
      if (size)   *size   = 0;
      if (err_no) *err_no = ENOENT;
    }

  g_object_unref (file);

  return type;
}

/**
 * gimp_thumbs_delete_for_uri:
 * @uri: an escaped URI
 *
 * Deletes all thumbnails for the image file specified by @uri from the
 * user's thumbnail repository.
 *
 * Since: GIMP 2.2
 **/
void
gimp_thumbs_delete_for_uri (const gchar *uri)
{
  gint i;

  g_return_if_fail (gimp_thumb_initialized);
  g_return_if_fail (uri != NULL);

  for (i = 0; i < thumb_num_sizes; i++)
    {
      gchar *filename = gimp_thumb_name_from_uri (uri, thumb_sizes[i]);

      if (filename)
        {
          g_unlink (filename);
          g_free (filename);
        }
    }
}

/**
 * gimp_thumbs_delete_for_uri_local:
 * @uri: an escaped URI
 *
 * Deletes all thumbnails for the image file specified by @uri from
 * the local thumbnail repository.
 *
 * Since: GIMP 2.2
 **/
void
gimp_thumbs_delete_for_uri_local (const gchar *uri)
{
  gint i;

  g_return_if_fail (gimp_thumb_initialized);
  g_return_if_fail (uri != NULL);

  for (i = 0; i < thumb_num_sizes; i++)
    {
      gchar *filename = gimp_thumb_name_from_uri_local (uri, thumb_sizes[i]);

      if (filename)
        {
          g_unlink (filename);
          g_free (filename);
        }
    }
}

void
_gimp_thumbs_delete_others (const gchar   *uri,
                            GimpThumbSize  size)
{
  gint i;

  g_return_if_fail (gimp_thumb_initialized);
  g_return_if_fail (uri != NULL);

  size = gimp_thumb_size (size);

  for (i = 0; i < thumb_num_sizes; i++)
    {
      gchar *filename;

      if (i == size)
        continue;

      filename = gimp_thumb_name_from_uri (uri, thumb_sizes[i]);
      if (filename)
        {
          g_unlink (filename);
          g_free (filename);
        }
    }
}

gchar *
_gimp_thumb_filename_from_uri (const gchar *uri)
{
  gchar *filename;
  gchar *hostname;

  g_return_val_if_fail (uri != NULL, NULL);

  filename = g_filename_from_uri (uri, &hostname, NULL);

  if (!filename)
    return NULL;

  if (hostname)
    {
      /*  we have a file: URI with a hostname                           */

#ifdef G_OS_WIN32
      /*  on Win32, create a valid UNC path and use it as the filename  */
      gchar *tmp = g_build_filename ("//", hostname, filename, NULL);

      g_free (filename);
      filename = tmp;
#else
      /*  otherwise return NULL, caller should use URI then             */
      g_free (filename);
      filename = NULL;
#endif

      g_free (hostname);
    }

  return filename;
}

static void
gimp_thumb_exit (void)
{
  gint i;

  g_free (thumb_dir);
  g_free (thumb_sizes);
  g_free (thumb_sizenames);
  for (i = 0; i < thumb_num_sizes; i++)
    g_free (thumb_subdirs[i]);
  g_free (thumb_subdirs);
  g_free (thumb_fail_subdir);

  thumb_num_sizes        = 0;
  thumb_sizes            = NULL;
  thumb_sizenames        = NULL;
  thumb_dir              = NULL;
  thumb_subdirs          = NULL;
  thumb_fail_subdir      = NULL;
  gimp_thumb_initialized = FALSE;
}

static gint
gimp_thumb_size (GimpThumbSize size)
{
  gint i = 0;

  if (size > GIMP_THUMB_SIZE_FAIL)
    {
      for (i = 1;
           i < thumb_num_sizes && thumb_sizes[i] < size;
           i++)
        /* nothing */;

      if (i == thumb_num_sizes)
        i--;
    }

  return i;
}

static gchar *
gimp_thumb_png_lookup (const gchar   *name,
                       const gchar   *basedir,
                       GimpThumbSize *size)
{
  gchar  *thumb_name = NULL;
  gchar **subdirs    = NULL;
  gint    i, n;

  if (basedir)
    {
      gchar *dir = g_build_filename (basedir, ".thumblocal", NULL);

      if (g_file_test (basedir, G_FILE_TEST_IS_DIR))
        {
          gint i;

          subdirs = g_new (gchar *, thumb_num_sizes);

          subdirs[0] = NULL;  /*  GIMP_THUMB_SIZE_FAIL  */

          for (i = 1; i < thumb_num_sizes; i++)
            subdirs[i] = g_build_filename (dir, thumb_sizenames[i], NULL);
        }

      g_free (dir);
    }
  else
    {
      subdirs = thumb_subdirs;
    }

  if (! subdirs)
    return NULL;

  i = n = gimp_thumb_size (*size);

  for (; i < thumb_num_sizes; i++)
    {
      if (! subdirs[i])
        continue;

      thumb_name = g_build_filename (subdirs[i], name, NULL);

      if (gimp_thumb_file_test (thumb_name,
                                NULL, NULL,
                                NULL) == GIMP_THUMB_FILE_TYPE_REGULAR)
        {
          *size = thumb_sizes[i];
          goto finish;
        }

      g_free (thumb_name);
    }

  for (i = n - 1; i >= 0; i--)
    {
      if (! subdirs[i])
        continue;

      thumb_name = g_build_filename (subdirs[i], name, NULL);

      if (gimp_thumb_file_test (thumb_name,
                                NULL, NULL,
                                NULL) == GIMP_THUMB_FILE_TYPE_REGULAR)
        {
          *size = thumb_sizes[i];
          goto finish;
        }

      g_free (thumb_name);
    }

  thumb_name = NULL;

 finish:
  if (basedir)
    {
      for (i = 0; i < thumb_num_sizes; i++)
        g_free (subdirs[i]);
      g_free (subdirs);
    }

  return thumb_name;
}

static const gchar *
gimp_thumb_png_name (const gchar *uri)
{
  static gchar name[40];

  GChecksum *checksum;
  guchar     digest[16];
  gsize      len = sizeof (digest);
  gsize      i;

  checksum = g_checksum_new (G_CHECKSUM_MD5);
  g_checksum_update (checksum, (const guchar *) uri, -1);
  g_checksum_get_digest (checksum, digest, &len);
  g_checksum_free (checksum);

  for (i = 0; i < len; i++)
    {
      guchar n;

      n = (digest[i] >> 4) & 0xF;
      name[i * 2]     = (n > 9) ? 'a' + n - 10 : '0' + n;

      n = digest[i] & 0xF;
      name[i * 2 + 1] = (n > 9) ? 'a' + n - 10 : '0' + n;
    }

  strncpy (name + 32, ".png", 5);

  return (const gchar *) name;
}
