/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * Copyright (C) 1999, 2000, 2001 Eazel, Inc.
 * Copyright (C) 2011 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Cosimo Cecchi <cosimoc@redhat.com>
 *
 * The code for crawling the directory hierarchy is based on
 * nautilus/libnautilus-private/nautilus-directory-async.c, with
 * the following copyright and author:
 *
 * Copyright (C) 1999, 2000, 2001 Eazel, Inc.
 * Author: Darin Adler <darin@bentspoon.com>
 *
 */

#include "shell-mime-sniffer.h"
#include "hotplug-mimetypes.h"

#include <glib/gi18n.h>

#include <gdk-pixbuf/gdk-pixbuf.h>

#define LOADER_ATTRS                          \
  G_FILE_ATTRIBUTE_STANDARD_TYPE ","          \
  G_FILE_ATTRIBUTE_STANDARD_NAME ","          \
  G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","

#define WATCHDOG_TIMEOUT 1500
#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100
#define HIGH_SCORE_RATIO 0.10

enum {
  PROP_FILE = 1,
  NUM_PROPERTIES
};

static GHashTable *image_type_table = NULL;
static GHashTable *audio_type_table = NULL;
static GHashTable *video_type_table = NULL;
static GHashTable *docs_type_table = NULL;

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };

typedef struct {
  ShellMimeSniffer *self;

  GFile *file;
  GFileEnumerator *enumerator;
  GList *deep_count_subdirectories;

  gint audio_count;
  gint image_count;
  gint document_count;
  gint video_count;

  gint total_items;
} DeepCountState;

typedef struct _ShellMimeSnifferPrivate   ShellMimeSnifferPrivate;

struct _ShellMimeSniffer
{
  GObject parent_instance;

  ShellMimeSnifferPrivate *priv;
};

struct _ShellMimeSnifferPrivate {
  GFile *file;

  GCancellable *cancellable;
  guint watchdog_id;

  GTask *task;
};

G_DEFINE_TYPE_WITH_PRIVATE (ShellMimeSniffer, shell_mime_sniffer, G_TYPE_OBJECT);

static void deep_count_load (DeepCountState *state,
                             GFile *file);

static void
init_mimetypes (void)
{
  static gsize once_init = 0;

  if (g_once_init_enter (&once_init))
    {
      GSList *formats, *l;
      GdkPixbufFormat *format;
      gchar **types;
      gint idx;

      image_type_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
      video_type_table = g_hash_table_new (g_str_hash, g_str_equal);
      audio_type_table = g_hash_table_new (g_str_hash, g_str_equal);
      docs_type_table = g_hash_table_new (g_str_hash, g_str_equal);

      formats = gdk_pixbuf_get_formats ();

      for (l = formats; l != NULL; l = l->next)
        {
          format = l->data;
          types = gdk_pixbuf_format_get_mime_types (format);

          for (idx = 0; types[idx] != NULL; idx++)
            g_hash_table_insert (image_type_table, g_strdup (types[idx]), GINT_TO_POINTER (1));

          g_strfreev (types);
        }

      g_slist_free (formats);

      for (idx = 0; audio_mimetypes[idx] != NULL; idx++)
        g_hash_table_insert (audio_type_table, (gpointer) audio_mimetypes[idx], GINT_TO_POINTER (1));

      for (idx = 0; video_mimetypes[idx] != NULL; idx++)
        g_hash_table_insert (video_type_table, (gpointer) video_mimetypes[idx], GINT_TO_POINTER (1));

      for (idx = 0; docs_mimetypes[idx] != NULL; idx++)
        g_hash_table_insert (docs_type_table, (gpointer) docs_mimetypes[idx], GINT_TO_POINTER (1));

      g_once_init_leave (&once_init, 1);
    }
}

static void
add_content_type_to_cache (DeepCountState *state,
                           const gchar *content_type)
{
  gboolean matched = TRUE;

  if (g_hash_table_lookup (image_type_table, content_type))
    state->image_count++;
  else if (g_hash_table_lookup (video_type_table, content_type))
    state->video_count++;
  else if (g_hash_table_lookup (docs_type_table, content_type))
    state->document_count++;
  else if (g_hash_table_lookup (audio_type_table, content_type))
    state->audio_count++;
  else
    matched = FALSE;

  if (matched)
    state->total_items++;
}

typedef struct {
  const gchar *type;
  gdouble ratio;
} SniffedResult;

static gint
results_cmp_func (gconstpointer a,
                  gconstpointer b)
{
  const SniffedResult *sniffed_a = a;
  const SniffedResult *sniffed_b = b;

  if (sniffed_a->ratio < sniffed_b->ratio)
    return 1;

  if (sniffed_a->ratio > sniffed_b->ratio)
    return -1;

  return 0;
}

static void
prepare_async_result (DeepCountState *state)
{
  ShellMimeSniffer *self = state->self;
  GArray *results;
  GPtrArray *sniffed_mime;
  SniffedResult result;
  char **mimes;

  sniffed_mime = g_ptr_array_new ();
  results = g_array_new (TRUE, TRUE, sizeof (SniffedResult));

  if (state->total_items == 0)
    goto out;

  result.type = "x-content/video";
  result.ratio = (gdouble) state->video_count / (gdouble) state->total_items;
  g_array_append_val (results, result);

  result.type = "x-content/audio";
  result.ratio = (gdouble) state->audio_count / (gdouble) state->total_items;
  g_array_append_val (results, result);

  result.type = "x-content/pictures";
  result.ratio = (gdouble) state->image_count / (gdouble) state->total_items;
  g_array_append_val (results, result);

  result.type = "x-content/documents";
  result.ratio = (gdouble) state->document_count / (gdouble) state->total_items;
  g_array_append_val (results, result);

  g_array_sort (results, results_cmp_func);

  result = g_array_index (results, SniffedResult, 0);
  g_ptr_array_add (sniffed_mime, g_strdup (result.type));

  /* if other types score high in ratio, add them, up to three */
  result = g_array_index (results, SniffedResult, 1);
  if (result.ratio < HIGH_SCORE_RATIO)
    goto out;
  g_ptr_array_add (sniffed_mime, g_strdup (result.type));

  result = g_array_index (results, SniffedResult, 2);
  if (result.ratio < HIGH_SCORE_RATIO)
    goto out;
  g_ptr_array_add (sniffed_mime, g_strdup (result.type));

 out:
  g_ptr_array_add (sniffed_mime, NULL);
  mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE);

  g_array_free (results, TRUE);
  g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev);
}

/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */
static void
deep_count_one (DeepCountState *state,
		GFileInfo *info)
{
  GFile *subdir;
  const char *content_type;

  if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY)
    {
      /* record the fact that we have to descend into this directory */
      subdir = g_file_get_child (state->file, g_file_info_get_name (info));
      state->deep_count_subdirectories =
        g_list_append (state->deep_count_subdirectories, subdir);
    } 
  else
    {
      content_type = g_file_info_get_content_type (info);
      add_content_type_to_cache (state, content_type);
    }
}

static void
deep_count_finish (DeepCountState *state)
{
  prepare_async_result (state);

  if (state->enumerator)
    {
      if (!g_file_enumerator_is_closed (state->enumerator))
        g_file_enumerator_close_async (state->enumerator,
                                       0, NULL, NULL, NULL);

      g_object_unref (state->enumerator);
    }

  g_cancellable_reset (state->self->priv->cancellable);
  g_clear_object (&state->file);

  g_list_free_full (state->deep_count_subdirectories, g_object_unref);

  g_free (state);
}

static void
deep_count_next_dir (DeepCountState *state)
{
  GFile *new_file;

  g_clear_object (&state->file);

  if (state->deep_count_subdirectories != NULL)
    {
      /* Work on a new directory. */
      new_file = state->deep_count_subdirectories->data;
      state->deep_count_subdirectories =
        g_list_remove (state->deep_count_subdirectories, new_file);

      deep_count_load (state, new_file);
      g_object_unref (new_file);
    }
  else
    {
      deep_count_finish (state);
    }
}

static void
deep_count_more_files_callback (GObject *source_object,
				GAsyncResult *res,
				gpointer user_data)
{
  DeepCountState *state;
  GList *files, *l;
  GFileInfo *info;

  state = user_data;

  if (g_cancellable_is_cancelled (state->self->priv->cancellable))
    {
      deep_count_finish (state);
      return;
    }
	
  files = g_file_enumerator_next_files_finish (state->enumerator,
                                               res, NULL);
  
  for (l = files; l != NULL; l = l->next)
    {
      info = l->data;
      deep_count_one (state, info);
      g_object_unref (info);
    }

  if (files == NULL)
    {
      g_file_enumerator_close_async (state->enumerator, 0, NULL, NULL, NULL);
      g_object_unref (state->enumerator);
      state->enumerator = NULL;

      deep_count_next_dir (state);
    }
  else
    {
      g_file_enumerator_next_files_async (state->enumerator,
                                          DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
                                          G_PRIORITY_LOW,
                                          state->self->priv->cancellable,
                                          deep_count_more_files_callback,
                                          state);
    }

  g_list_free (files);
}

static void
deep_count_callback (GObject *source_object,
		     GAsyncResult *res,
		     gpointer user_data)
{
  DeepCountState *state;
  GFileEnumerator *enumerator;

  state = user_data;

  if (g_cancellable_is_cancelled (state->self->priv->cancellable))
    {
      deep_count_finish (state);
      return;
    }

  enumerator = g_file_enumerate_children_finish (G_FILE (source_object),
                                                 res, NULL);
	
  if (enumerator == NULL)
    {
      deep_count_next_dir (state);
    }
  else
    {
      state->enumerator = enumerator;
      g_file_enumerator_next_files_async (state->enumerator,
                                          DIRECTORY_LOAD_ITEMS_PER_CALLBACK,
                                          G_PRIORITY_LOW,
                                          state->self->priv->cancellable,
                                          deep_count_more_files_callback,
                                          state);
    }
}

static void
deep_count_load (DeepCountState *state,
                 GFile *file)
{
  state->file = g_object_ref (file);

  g_file_enumerate_children_async (state->file,
                                   LOADER_ATTRS,
                                   G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, /* flags */
                                   G_PRIORITY_LOW, /* prio */
                                   state->self->priv->cancellable,
                                   deep_count_callback,
                                   state);
}

static void
deep_count_start (ShellMimeSniffer *self)
{
  DeepCountState *state;

  state = g_new0 (DeepCountState, 1);
  state->self = self;

  deep_count_load (state, self->priv->file);
}

static void
query_info_async_ready_cb (GObject *source,
                           GAsyncResult *res,
                           gpointer user_data)
{
  GFileInfo *info;
  GError *error = NULL;
  ShellMimeSniffer *self = user_data;

  info = g_file_query_info_finish (G_FILE (source),
                                   res, &error);

  if (error != NULL)
    {
      g_task_return_error (self->priv->task, error);

      return;
    }

  if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY)
    {
      g_task_return_new_error (self->priv->task,
                               G_IO_ERROR,
                               G_IO_ERROR_NOT_DIRECTORY,
                               "Not a directory");

      return;
    }

  deep_count_start (self);
}

static gboolean
watchdog_timeout_reached_cb (gpointer user_data)
{
  ShellMimeSniffer *self = user_data;

  self->priv->watchdog_id = 0;
  g_cancellable_cancel (self->priv->cancellable);

  return FALSE;
}

static void
start_loading_file (ShellMimeSniffer *self)
{
  g_file_query_info_async (self->priv->file,
                           LOADER_ATTRS,
                           G_FILE_QUERY_INFO_NONE,
                           G_PRIORITY_DEFAULT,
                           self->priv->cancellable,
                           query_info_async_ready_cb,
                           self);
}

static void
shell_mime_sniffer_set_file (ShellMimeSniffer *self,
                            GFile *file)
{
  g_clear_object (&self->priv->file);
  self->priv->file = g_object_ref (file);
}

static void
shell_mime_sniffer_dispose (GObject *object)
{
  ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);

  g_clear_object (&self->priv->file);
  g_clear_object (&self->priv->cancellable);
  g_clear_object (&self->priv->task);

  g_clear_handle_id (&self->priv->watchdog_id, g_source_remove);

  G_OBJECT_CLASS (shell_mime_sniffer_parent_class)->dispose (object);
}

static void
shell_mime_sniffer_get_property (GObject *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);

  switch (prop_id) {
  case PROP_FILE:
    g_value_set_object (value, self->priv->file);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    break;
  }
}

static void
shell_mime_sniffer_set_property (GObject *object,
                                guint       prop_id,
                                const GValue *value,
                                GParamSpec *pspec)
{
  ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object);

  switch (prop_id) {
  case PROP_FILE:
    shell_mime_sniffer_set_file (self, g_value_get_object (value));
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    break;
  }
}

static void
shell_mime_sniffer_class_init (ShellMimeSnifferClass *klass)
{
  GObjectClass *oclass;

  oclass = G_OBJECT_CLASS (klass);
  oclass->dispose = shell_mime_sniffer_dispose;
  oclass->get_property = shell_mime_sniffer_get_property;
  oclass->set_property = shell_mime_sniffer_set_property;

  properties[PROP_FILE] =
    g_param_spec_object ("file",
                         "File",
                         "The loaded file",
                         G_TYPE_FILE,
                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
}

static void
shell_mime_sniffer_init (ShellMimeSniffer *self)
{
  self->priv = shell_mime_sniffer_get_instance_private (self);
  init_mimetypes ();
}

ShellMimeSniffer *
shell_mime_sniffer_new (GFile *file)
{
  return g_object_new (SHELL_TYPE_MIME_SNIFFER,
                       "file", file,
                       NULL);
}

void
shell_mime_sniffer_sniff_async (ShellMimeSniffer *self,
                                GAsyncReadyCallback callback,
                                gpointer user_data)
{
  g_assert (self->priv->watchdog_id == 0);
  g_assert (self->priv->task == NULL);

  self->priv->cancellable = g_cancellable_new ();
  self->priv->task = g_task_new (self, self->priv->cancellable,
                                 callback, user_data);

  self->priv->watchdog_id =
    g_timeout_add (WATCHDOG_TIMEOUT,
                   watchdog_timeout_reached_cb, self);
  g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb");

  start_loading_file (self);
}

gchar **
shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self,
                                 GAsyncResult *res,
                                 GError **error)
{
  return g_task_propagate_pointer (self->priv->task, error);
}
