/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/* GIO - GLib Input, Output and Streaming Library
 *
 * Copyright (C) 2009 Red Hat, Inc.
 *
 * 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 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
 * 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, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * Author: David Zeuthen <davidz@redhat.com>
 */

#include <config.h>
#include <string.h>
#include <glib/gi18n-lib.h>

#ifdef HAVE_BLURAY
#include <langinfo.h>
#include <libbluray/bluray.h>
#include <libbluray/meta_data.h>
#endif /* HAVE_BLURAY */

#include "gvfsmountinfo.h"

#define VOLUME_INFO_GROUP "Volume Info"

static GFile *
_g_find_file_insensitive_finish (GFile        *parent,
                                 GAsyncResult *result,
                                 GError      **error);

static void
_g_find_file_insensitive_async (GFile              *parent,
                                const gchar        *name,
                                GCancellable       *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer            user_data);

static void
on_icon_file_located (GObject       *source_object,
                      GAsyncResult  *res,
                      gpointer      user_data)
{
  GTask *task = G_TASK (user_data);
  GFile *icon_file;
  GError *error;

  error = NULL;
  icon_file = _g_find_file_insensitive_finish (G_FILE (source_object),
                                               res,
                                               &error);
  if (icon_file != NULL)
    {
      g_task_return_pointer (task, g_file_icon_new (icon_file), g_object_unref);
      g_object_unref (icon_file);
    }
  else
    {
      g_task_return_error (task, error);
    }

  g_object_unref (task);
}

static void
on_autorun_loaded (GObject      *source_object,
                   GAsyncResult *res,
                   gpointer      user_data)
{
  GTask *task = G_TASK (user_data);
  GFile *autorun_file;
  gchar *content;
  gchar *relative_icon_path;
  gsize content_length;
  GError *error;

  relative_icon_path = NULL;

  autorun_file = G_FILE (source_object);

  error = NULL;
  if (g_file_load_contents_finish (autorun_file,
                                   res,
                                   &content,
                                   &content_length,
                                   NULL,
                                   &error))
    {
      /* Scan through for an "icon=" line. Can't use GKeyFile,
       * because .inf files aren't always valid key files
       **/
      GRegex *icon_regex;
      GMatchInfo *match_info;

      /* [^,] is because sometimes the icon= line
       * has a comma at the end
       **/
      icon_regex = g_regex_new ("icon\\s*=\\s*+([^,\\r\\n]+)",
                                G_REGEX_CASELESS | G_REGEX_RAW, 0, NULL);
      g_regex_match (icon_regex, content, 0,
                     &match_info);

      /* Even if there are multiple matches, pick only the
       * first.
       **/
      if (g_match_info_matches (match_info))
        {
          gchar *chr;
          gchar *word = g_match_info_fetch (match_info, 1);

          /* Replace '\' with '/' */
          while ((chr = strchr (word, '\\')) != NULL)
            *chr = '/';

          /* If the file name's not valid UTF-8,
           * don't even try to load it
           **/
          if (g_utf8_validate (word, -1, NULL))
            {
              relative_icon_path = word;
            }
          else
            {
              /* TODO: mark for translation. Strictly, this isn't very important; this string
               * will never be displayed since all current users of g_vfs_mount_info_query_autorun_info()
               * passes NULL for the GError**.
               */
              error = g_error_new_literal (G_IO_ERROR,
                                           G_IO_ERROR_FAILED,
                                           "Icon name is not valid UTF-8");
              g_free (word);
            }
        }

      g_match_info_free (match_info);

      g_regex_unref (icon_regex);
      g_free (content);
    }

  /* some autorun.in points to the .exe file for the icon; make sure we avoid using that */
  if (relative_icon_path != NULL)
    {
      if (!g_str_has_suffix (relative_icon_path, ".exe"))
        {
          GFile *root;

          root = g_file_get_parent (autorun_file);

          _g_find_file_insensitive_async (root,
                                          relative_icon_path,
                                          g_task_get_cancellable (task),
                                          on_icon_file_located,
                                          task);

          g_object_unref (root);
        }
      else
        {
          /* TODO: mark for translation. Strictly, this isn't very important; this string
           * will never be displayed since all current users of g_vfs_mount_info_query_autorun_info()
           * passes NULL for the GError**.
           */
          error = g_error_new_literal (G_IO_ERROR,
                                       G_IO_ERROR_FAILED,
                                       "Icon is an .exe file");
        }
    }

  if (error != NULL)
    {
      g_task_return_error (task, error);
      g_object_unref (task);
    }

  g_free (relative_icon_path);
}

static void
on_autorun_located (GObject      *source_object,
                    GAsyncResult *res,
                    gpointer      user_data)
{
  GTask *task = G_TASK (user_data);
  GFile *autorun_path;
  GError *error;

  error = NULL;
  autorun_path = _g_find_file_insensitive_finish (G_FILE (source_object),
                                                  res,
                                                  &error);
  if (error != NULL)
    {
      g_task_return_error (task, error);
      g_object_unref (task);
    }
  else
    {
      g_file_load_contents_async (autorun_path,
                                  g_task_get_cancellable (task),
                                  on_autorun_loaded,
                                  task);
      g_object_unref (autorun_path);
    }
}

void
g_vfs_mount_info_query_autorun_info (GFile               *directory,
                                     GCancellable        *cancellable,
                                     GAsyncReadyCallback  callback,
                                     gpointer             user_data)
{
  GTask *task;

  task = g_task_new (directory, cancellable, callback, user_data);
  g_task_set_source_tag (task, g_vfs_mount_info_query_autorun_info);

  _g_find_file_insensitive_async (directory,
                                  "autorun.inf",
                                  cancellable,
                                  on_autorun_located,
                                  task);
}

GIcon *
g_vfs_mount_info_query_autorun_info_finish (GFile          *directory,
                                            GAsyncResult   *res,
                                            GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (res, directory), NULL);
  g_return_val_if_fail (g_async_result_is_tagged (res, g_vfs_mount_info_query_autorun_info), NULL);

  return g_task_propagate_pointer (G_TASK (res), error);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
on_xdg_volume_info_loaded (GObject      *source_object,
                           GAsyncResult *res,
                           gpointer      user_data)
{
  GTask *task = G_TASK (user_data);
  GFile *xdg_volume_info_file;
  gchar *content;
  gsize content_length;
  GError *error;
  GKeyFile *key_file;
  gchar *name;
  gchar *icon_name;
  gchar *icon_file;
  GIcon *icon;

  content = NULL;
  key_file = NULL;
  name = NULL;
  icon_name = NULL;
  icon_file = NULL;

  xdg_volume_info_file = G_FILE (source_object);

  error = NULL;
  if (g_file_load_contents_finish (xdg_volume_info_file,
                                   res,
                                   &content,
                                   &content_length,
                                   NULL,
                                   &error))
    {
      key_file = g_key_file_new ();
      if (!g_key_file_load_from_data (key_file,
                                      content,
                                      content_length,
                                      G_KEY_FILE_NONE,
                                      &error))
        goto out;


      name = g_key_file_get_locale_string (key_file,
                                           VOLUME_INFO_GROUP,
                                           "Name",
                                           NULL,
                                           NULL);

      icon_name = g_key_file_get_string (key_file,
                                         VOLUME_INFO_GROUP,
                                         "Icon",
                                         NULL);
      
      icon_file = g_key_file_get_string (key_file,
                                         VOLUME_INFO_GROUP,
                                         "IconFile",
                                         NULL);

      icon = NULL;
      
      if (icon_file != NULL)
        {
          GFile *dir, *f;

          dir = g_file_get_parent (xdg_volume_info_file);
          if (dir)
            {
              f = g_file_resolve_relative_path (dir, icon_file);
              if (f)
                {
                  icon = g_file_icon_new (f);
                  g_object_unref (f);
                }
              
              g_object_unref (dir);
            }
        }
            
      if (icon == NULL && icon_name != NULL)
        {
          icon = g_themed_icon_new (icon_name);
          g_themed_icon_append_name (G_THEMED_ICON (icon), "drive-removable-media");
          g_themed_icon_append_name (G_THEMED_ICON (icon), "drive-removable");
          g_themed_icon_append_name (G_THEMED_ICON (icon), "drive");
        }

      g_object_set_data_full (G_OBJECT (task), "name", name, g_free);
      g_task_return_pointer (task, icon, g_object_unref);
      name = NULL; /* steals name */
    }

 out:

  if (key_file != NULL)
    g_key_file_free (key_file);

  if (error != NULL)
    g_task_return_error (task, error);

  g_object_unref (task);

  g_free (name);
  g_free (icon_name);
  g_free (icon_file);
  g_free (content);
}

void
g_vfs_mount_info_query_xdg_volume_info (GFile               *directory,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
{
  GTask *task;
  GFile *file;

  task = g_task_new (directory, cancellable, callback, user_data);
  g_task_set_source_tag (task, g_vfs_mount_info_query_xdg_volume_info);

  file = g_file_resolve_relative_path (directory, ".xdg-volume-info");
  g_file_load_contents_async (file,
                              cancellable,
                              on_xdg_volume_info_loaded,
                              task);
  g_object_unref (file);
}

GIcon *g_vfs_mount_info_query_xdg_volume_info_finish (GFile          *directory,
                                                      GAsyncResult   *res,
                                                      gchar         **out_name,
                                                      GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (res, directory), NULL);
  g_return_val_if_fail (g_async_result_is_tagged (res, g_vfs_mount_info_query_xdg_volume_info), NULL);

  if (g_task_had_error (G_TASK (res)))
    goto out;

  if (out_name != NULL)
    *out_name = g_strdup (g_object_get_data (G_OBJECT (res), "name"));

 out:
  return g_task_propagate_pointer (G_TASK (res), error);
}

/* ---------------------------------------------------------------------------------------------------- */

#ifdef HAVE_BLURAY
static const char *
get_iso_639_3_for_locale (void)
{
  const char *lang = NULL;

#ifdef HAVE_NL_ADDRESS_LANG_TERM
  lang = nl_langinfo (_NL_ADDRESS_LANG_TERM);
  if (lang == NULL || *lang == '\0')
    {
#ifdef HAVE_NL_ADDRESS_COUNTRY_AB3
      lang = nl_langinfo (_NL_ADDRESS_COUNTRY_AB3);
      if (lang == NULL || *lang == '\0')
#endif
        return NULL;
    }
#endif

  return lang;
}

static const char *
get_icon (const META_DL *meta)
{
  const char *icon;
  guint i;
  guint size;

  icon = NULL;
  size = 0;

  for (i = 0; i < meta->thumb_count; i++)
    {
      if (meta->thumbnails[i].xres > size)
        {
          icon = meta->thumbnails[i].path;
          size = meta->thumbnails[i].xres;
        }
    }

  return icon;
}

static void
bdmv_metadata_thread (GTask *task,
                      gpointer object,
                      gpointer task_data,
                      GCancellable *cancellable)
{
  BLURAY *bd;
  const META_DL *meta;
  GError *error;
  GFile *file;
  char *disc_root;
  char *icon;
  char *name;
  const char *lang;
  char *path;

  file = G_FILE (object);

  disc_root = g_file_get_path (file);
  if (!disc_root)
    {
      error = g_error_new_literal (G_IO_ERROR,
                                   G_IO_ERROR_FAILED,
                                   "Device is not a Blu-Ray disc");
      goto error;
    }

  path = g_build_filename (disc_root, "BDMV", NULL);
  if (!g_file_test (path, G_FILE_TEST_IS_DIR))
    {
      error = g_error_new_literal (G_IO_ERROR,
                                   G_IO_ERROR_FAILED,
                                   "Device is not a Blu-Ray disc");
      goto error;
    }
  g_free (path);

  bd = bd_open (disc_root, NULL);
  g_free (disc_root);

  if (bd == NULL)
    {
      error = g_error_new_literal (G_IO_ERROR,
                                   G_IO_ERROR_FAILED,
                                   "Device is not a Blu-Ray disc");
      goto error;
    }

  lang = get_iso_639_3_for_locale ();
  if (lang != NULL)
    bd_set_player_setting_str (bd, BLURAY_PLAYER_SETTING_MENU_LANG, lang);

  meta = bd_get_meta (bd);
  if (meta == NULL)
    {
      error = g_error_new_literal (G_IO_ERROR,
                                   G_IO_ERROR_FAILED,
                                   "Device is not a Blu-Ray disc, or has no metadata");
      bd_close (bd);
      goto error;
    }
  name = icon = NULL;

  if (meta != NULL)
    {
      if (meta->di_name && *meta->di_name)
        name = g_strdup (meta->di_name);
      icon = g_strdup (get_icon (meta));
    }

  /* We're missing either an icon, or the name */
  if (!name || !icon)
    {
      bd_set_player_setting_str (bd, BLURAY_PLAYER_SETTING_MENU_LANG, "eng");
      meta = bd_get_meta (bd);

      if (meta != NULL && name == NULL && meta->di_name && *meta->di_name)
        name = g_strdup (meta->di_name);

      if (meta != NULL && icon == NULL)
        icon = g_strdup (get_icon (meta));
    }

  if (name != NULL)
    g_object_set_data_full (G_OBJECT (task), "name", name, g_free);

  /* Set the results */
  if (icon != NULL)
    {
      char *icon_path;
      GFile *icon_file;

      icon_path = g_strdup_printf ("BDMV/META/DL/%s", icon);
      g_free (icon);
      icon_file = g_file_resolve_relative_path (file, icon_path);
      g_free (icon_path);

      g_task_return_pointer (task, g_file_icon_new (icon_file), g_object_unref);
      g_object_unref (icon_file);
    }
  else
    {
      g_task_return_pointer (task, NULL, NULL);
    }

  g_object_unref (task);
  bd_close (bd);

  return;

error:
  g_task_return_error (task, error);
  g_object_unref (task);
}
#endif /* HAVE_BLURAY */

void
g_vfs_mount_info_query_bdmv_volume_info (GFile               *directory,
                                         GCancellable        *cancellable,
                                         GAsyncReadyCallback  callback,
                                         gpointer             user_data)
{
  GTask *task;

  task = g_task_new (directory, cancellable, callback, user_data);
  g_task_set_source_tag (task, g_vfs_mount_info_query_bdmv_volume_info);

#ifdef HAVE_BLURAY
  g_task_run_in_thread (task, (GTaskThreadFunc) bdmv_metadata_thread);
#else
  g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                           "gvfs built without Expat support, no BDMV support");
  g_object_unref (task);
#endif /* HAVE_BLURAY */
}

GIcon *g_vfs_mount_info_query_bdmv_volume_info_finish (GFile          *directory,
                                                       GAsyncResult   *res,
                                                       gchar         **out_name,
                                                       GError        **error)
{
  g_return_val_if_fail (g_task_is_valid (res, directory), NULL);
  g_return_val_if_fail (g_async_result_is_tagged (res, g_vfs_mount_info_query_bdmv_volume_info), NULL);

  if (g_task_had_error (G_TASK (res)))
    goto out;

  if (out_name != NULL)
    *out_name = g_strdup (g_object_get_data (G_OBJECT (res), "name"));

 out:
  return g_task_propagate_pointer (G_TASK (res), error);
}

/* ---------------------------------------------------------------------------------------------------- */

#define INSENSITIVE_SEARCH_ITEMS_PER_CALLBACK 100

static void
enumerated_children_callback (GObject *source_object, GAsyncResult *res,
                              gpointer user_data);

static void
more_files_callback (GObject *source_object, GAsyncResult *res,
                     gpointer user_data);

static void
find_file_insensitive_exists_callback (GObject *source_object,
                                       GAsyncResult *res,
                                       gpointer user_data);

typedef struct _InsensitiveFileSearchData
{
        gchar *original_path;
        gchar **split_path;
        gint index;
        GFileEnumerator *enumerator;
        GFile *current_file;
} InsensitiveFileSearchData;

static void
clear_find_file_insensitive_state (InsensitiveFileSearchData *data);

static void
_g_find_file_insensitive_async (GFile              *parent,
                                const gchar        *name,
                                GCancellable       *cancellable,
                                GAsyncReadyCallback callback,
                                gpointer            user_data)
{
  GTask *task;
  InsensitiveFileSearchData *data;
  GFile *direct_file = g_file_get_child (parent, name);

  task = g_task_new (parent, cancellable, callback, user_data);
  g_task_set_source_tag (task, _g_find_file_insensitive_async);

  data = g_new0 (InsensitiveFileSearchData, 1);
  data->original_path = g_strdup (name);
  g_task_set_task_data (task, data, (GDestroyNotify)clear_find_file_insensitive_state);

  g_file_query_info_async (direct_file, G_FILE_ATTRIBUTE_STANDARD_TYPE,
                           G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT,
                           cancellable,
                           find_file_insensitive_exists_callback, task);


}

static void
clear_find_file_insensitive_state (InsensitiveFileSearchData *data)
{
  g_free (data->original_path);
  if (data->split_path)
    g_strfreev (data->split_path);
  if (data->enumerator)
    g_object_unref (data->enumerator);
  if (data->current_file)
    g_object_unref (data->current_file);
  g_free (data);
}

static void
find_file_insensitive_exists_callback (GObject *source_object,
                                       GAsyncResult *res,
                                       gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  GFileInfo *info;
  InsensitiveFileSearchData *data = g_task_get_task_data (task);

  /* The file exists and can be found with the given path, no need to search. */
  if ((info = g_file_query_info_finish (G_FILE (source_object), res, NULL)))
    {
      g_task_return_pointer (task, g_object_ref (source_object), g_object_unref);
      g_object_unref (task);
      g_object_unref (info);
    }
  else
    {
      data->split_path = g_strsplit (data->original_path, G_DIR_SEPARATOR_S, -1);
      data->index = 0;
      data->enumerator = NULL;
      data->current_file = g_object_ref (g_task_get_source_object (task));

      /* Skip any empty components due to multiple slashes */
      while (data->split_path[data->index] != NULL &&
             *data->split_path[data->index] == 0)
        data->index++;

      g_file_enumerate_children_async (data->current_file,
                                       G_FILE_ATTRIBUTE_STANDARD_NAME,
                                       0, G_PRIORITY_DEFAULT,
                                       g_task_get_cancellable (task),
                                       enumerated_children_callback, task);
    }
}

static void
enumerated_children_callback (GObject *source_object, GAsyncResult *res,
                              gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  GFileEnumerator *enumerator;
  InsensitiveFileSearchData *data = g_task_get_task_data (task);

  enumerator = g_file_enumerate_children_finish (G_FILE (source_object),
                                                 res, NULL);

  if (enumerator == NULL)
    {
      GFile *file;

      file = g_file_get_child (g_task_get_source_object (task), data->original_path);

      g_task_return_pointer (task, file, g_object_unref);
      g_object_unref (task);
      return;
    }

  data->enumerator = enumerator;
  g_file_enumerator_next_files_async (enumerator,
                                      INSENSITIVE_SEARCH_ITEMS_PER_CALLBACK,
                                      G_PRIORITY_DEFAULT,
                                      g_task_get_cancellable (task),
                                      more_files_callback,
                                      task);
}

static void
more_files_callback (GObject *source_object, GAsyncResult *res,
                     gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  InsensitiveFileSearchData *data = g_task_get_task_data (task);
  GList *files, *l;
  gchar *filename = NULL, *component, *case_folded_name,
    *name_collation_key;
  gboolean end_of_files, is_utf8;

  files = g_file_enumerator_next_files_finish (data->enumerator,
                                               res, NULL);

  end_of_files = files == NULL;

  component = data->split_path[data->index];
  g_return_if_fail (component != NULL);

  is_utf8 = (g_utf8_validate (component, -1, NULL));
  if (is_utf8)
    {
      case_folded_name = g_utf8_casefold (component, -1);
      name_collation_key = g_utf8_collate_key (case_folded_name, -1);
      g_free (case_folded_name);
    }

  else
    {
      name_collation_key = g_ascii_strdown (component, -1);
    }

  for (l = files; l != NULL; l = l->next)
    {
      GFileInfo *info;
      const gchar *this_name;
      gchar *key;

      info = l->data;
      this_name = g_file_info_get_name (info);

      if (is_utf8 && g_utf8_validate (this_name, -1, NULL))
        {
          gchar *case_folded;
          case_folded = g_utf8_casefold (this_name, -1);
          key = g_utf8_collate_key (case_folded, -1);
          g_free (case_folded);
        }
      else
        {
          key = g_ascii_strdown (this_name, -1);
        }

      if (strcmp (key, name_collation_key) == 0)
          filename = g_strdup (this_name);
      g_free (key);

      if (filename)
        break;
    }

  g_list_free_full (files, g_object_unref);
  g_free (name_collation_key);

  if (filename)
    {
      GFile *next_file;

      g_file_enumerator_close_async (data->enumerator,
                                     G_PRIORITY_DEFAULT,
                                     g_task_get_cancellable (task),
                                     NULL, NULL);
      g_object_unref (data->enumerator);
      data->enumerator = NULL;

      /* Set the current file and continue searching */
      next_file = g_file_get_child (data->current_file, filename);
      g_free (filename);
      g_object_unref (data->current_file);
      data->current_file = next_file;

      data->index++;
      /* Skip any empty components due to multiple slashes */
      while (data->split_path[data->index] != NULL &&
             *data->split_path[data->index] == 0)
        data->index++;

      if (data->split_path[data->index] == NULL)
       {
          /* Search is complete, file was found */
          g_task_return_pointer (task, g_object_ref (data->current_file), g_object_unref);
          g_object_unref (task);
          return;
        }

      /* Continue searching down the tree */
      g_file_enumerate_children_async (data->current_file,
                                       G_FILE_ATTRIBUTE_STANDARD_NAME,
                                       0, G_PRIORITY_DEFAULT,
                                       g_task_get_cancellable (task),
                                       enumerated_children_callback,
                                       task);
      return;
    }

  if (end_of_files)
    {
      /* Could not find the given file, abort the search */
      GFile *file;

      g_object_unref (data->enumerator);
      data->enumerator = NULL;

      file = g_file_get_child (g_task_get_source_object (task), data->original_path);
      g_task_return_pointer (task, file, g_object_unref);
      g_object_unref (task);
      return;
    }

  /* Continue enumerating */
  g_file_enumerator_next_files_async (data->enumerator,
                                      INSENSITIVE_SEARCH_ITEMS_PER_CALLBACK,
                                      G_PRIORITY_DEFAULT,
                                      g_task_get_cancellable (task),
                                      more_files_callback,
                                      task);
}

static GFile *
_g_find_file_insensitive_finish (GFile        *parent,
                                 GAsyncResult *result,
                                 GError      **error)
{
  g_return_val_if_fail (g_task_is_valid (result, parent), NULL);
  g_return_val_if_fail (g_async_result_is_tagged (result, _g_find_file_insensitive_async), NULL);

  return g_task_propagate_pointer (G_TASK (result), error);
}
