/* $Id$ */
/*-
 * Copyright (c) 2003-2005 Benedikt Meurer <benny@xfce.org>.
 *
 * 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, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA  02111-1307  USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_MEMORY_H
#include <memory.h>
#endif
#ifdef HAVE_STRING_H
#include <string.h>
#endif

#include <exo-helper/exo-helper.h>
#include <exo-helper/exo-helper-utils.h>



static void       exo_helper_class_init (ExoHelperClass *klass);
static void       exo_helper_finalize   (GObject        *object);
static ExoHelper *exo_helper_new        (const gchar    *id,
                                         XfceRc         *rc);



struct _ExoHelperClass
{
  GObjectClass __parent__;
};

struct _ExoHelper
{
  GObject __parent__;

  guint             startup_notify : 1;

  gchar            *id;
  gchar            *icon;
  gchar            *name;
  gchar           **commands;
  gchar           **commands_with_parameter;
  ExoHelperCategory category;
};



static GObjectClass *exo_helper_parent_class;



GType
exo_helper_get_type (void)
{
  static GType type = G_TYPE_INVALID;

  if (G_UNLIKELY (type == G_TYPE_INVALID))
    {
      static const GTypeInfo info =
      {
        sizeof (ExoHelperClass),
        NULL,
        NULL,
        (GClassInitFunc) exo_helper_class_init,
        NULL,
        NULL,
        sizeof (ExoHelper),
        0,
        NULL,
        NULL,
      };

      type = g_type_register_static (G_TYPE_OBJECT, I_("ExoHelper"), &info, 0);
    }

  return type;
}



static void
exo_helper_class_init (ExoHelperClass *klass)
{
  GObjectClass *gobject_class;

  /* determine the parent type class */
  exo_helper_parent_class = g_type_class_peek_parent (klass);

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->finalize = exo_helper_finalize;
}



static void
exo_helper_finalize (GObject *object)
{
  ExoHelper *helper = EXO_HELPER (object);

  g_strfreev (helper->commands_with_parameter);
  g_strfreev (helper->commands);
  g_free (helper->name);
  g_free (helper->icon);
  g_free (helper->id);

  (*G_OBJECT_CLASS (exo_helper_parent_class)->finalize) (object);
}



static gchar**
substitute_binary (const gchar *commands,
                   const gchar *binary)
{
  gchar **result;
  gchar **s, **t;
  gchar  *tmp;

  /* split the commands */
  result = g_strsplit (commands, ";", -1);

  /* (pre-)process the result */
  for (s = t = result; *s != NULL; ++s)
    {
      if (**s == '\0')
        {
          g_free (*s);
        }
      else if (binary != NULL)
        {
          tmp = exo_str_replace (*s, "%B", binary);
          g_free (*s);
          *t++ = tmp;
        }
      else
        {
          *t++ = *s;
        }
    }
  *t = NULL;

  return result;
}



static ExoHelper*
exo_helper_new (const gchar *id,
                XfceRc      *rc)
{
  const gchar *commands_with_parameter;
  const gchar *commands;
  const gchar *str;
  ExoHelper   *helper;
  gchar      **binaries;
  gchar       *binary = NULL;
  guint        n;

  g_return_val_if_fail (id != NULL, NULL);
  g_return_val_if_fail (rc != NULL, NULL);

  xfce_rc_set_group (rc, "Desktop Entry");

  /* allocate a new helper */
  helper = g_object_new (EXO_TYPE_HELPER, NULL);
  helper->id = g_strdup (id);
  helper->startup_notify = xfce_rc_read_bool_entry (rc, "StartupNotify", FALSE);

  /* verify the type of the desktop file */
  str = xfce_rc_read_entry_untranslated (rc, "Type", NULL);
  if (G_UNLIKELY (!exo_str_is_equal (str, "X-XFCE-Helper")))
    goto failed;

  /* determine the category of the helper */
  str = xfce_rc_read_entry_untranslated (rc, "X-XFCE-Category", NULL);
  if (!exo_helper_category_from_string (str, &helper->category))
    goto failed;

  /* determine the name of the helper */
  str = xfce_rc_read_entry (rc, "Name", NULL);
  if (G_UNLIKELY (str == NULL || *str == '\0'))
    goto failed;
  helper->name = g_strdup (str);

  /* determine the icon of the helper */
  str = xfce_rc_read_entry_untranslated (rc, "Icon", NULL);
  if (G_LIKELY (str != NULL && *str != '\0'))
    helper->icon = g_strdup (str);

  /* determine the commands */
  commands = xfce_rc_read_entry_untranslated (rc, "X-XFCE-Commands", NULL);
  if (G_UNLIKELY (commands == NULL))
    goto failed;

  /* determine the commands (with parameter) */
  commands_with_parameter = xfce_rc_read_entry_untranslated (rc, "X-XFCE-CommandsWithParameter", NULL);
  if (G_UNLIKELY (commands_with_parameter == NULL))
    goto failed;

  /* check if we need binaries for substitution */
  if (strstr (commands, "%B") != NULL || strstr (commands_with_parameter, "%B") != NULL)
    {
      /* determine the binaries */
      str = xfce_rc_read_entry_untranslated (rc, "X-XFCE-Binaries", NULL);
      if (G_UNLIKELY (str == NULL))
        goto failed;

      /* determine the first available binary */
      binaries = g_strsplit (str, ";", -1);
      for (binary = NULL, n = 0; binaries[n] != NULL && binary == NULL; ++n)
        if (G_LIKELY (binaries[n][0] != '\0'))
          binary = g_find_program_in_path (binaries[n]);
      g_strfreev (binaries);

      /* check if we found a binary */
      if (G_UNLIKELY (binary == NULL))
        goto failed;
    }

  /* substitute the binary (if any) */
  helper->commands = substitute_binary (commands, binary);
  helper->commands_with_parameter = substitute_binary (commands_with_parameter, binary);
  g_free (binary);

  /* verify that we have atleast one command */
  if (G_UNLIKELY (*helper->commands == NULL || *helper->commands_with_parameter == NULL))
    goto failed;

  return helper;

failed:
  g_object_unref (G_OBJECT (helper));
  return NULL;
}



/**
 * exo_helper_get_category:
 * @helper : an #ExoHelper.
 *
 * Returns the #ExoHelperCategory of @helper.
 *
 * Return value: the #ExoHelperCategory of @helper.
 **/
ExoHelperCategory
exo_helper_get_category (const ExoHelper *helper)
{
  g_return_val_if_fail (EXO_IS_HELPER (helper), EXO_HELPER_WEBBROWSER);
  return helper->category;
}



/**
 * exo_helper_get_id:
 * @helper : an #ExoHelper.
 *
 * Returns the unique id (the .desktop file basename) of @helper.
 *
 * Return value: the unique id of @helper.
 **/
const gchar*
exo_helper_get_id (const ExoHelper *helper)
{
  g_return_val_if_fail (EXO_IS_HELPER (helper), NULL);
  return helper->id;
}



/**
 * exo_helper_get_name:
 * @helper : an #ExoHelper.
 *
 * Returns the (translated) name of the @helper.
 *
 * Return value: the name of @helper.
 **/
const gchar*
exo_helper_get_name (const ExoHelper *helper)
{
  g_return_val_if_fail (EXO_IS_HELPER (helper), NULL);
  return helper->name;
}



/**
 * exo_helper_get_icon:
 * @helper : an #ExoHelper.
 *
 * Return the name of the themed icon for @helper or
 * the absolute path to an icon file, or %NULL if no
 * icon is available for @helper.
 *
 * Return value: the icon for @helper or %NULL.
 **/
const gchar*
exo_helper_get_icon (const ExoHelper *helper)
{
  g_return_val_if_fail (EXO_IS_HELPER (helper), NULL);
  return helper->icon;
}



/**
 * exo_helper_get_command:
 * @helper : an #ExoHelper.
 *
 * Returns a reasonable command for @helper.
 *
 * Return value: a command for @helper.
 **/
const gchar*
exo_helper_get_command (const ExoHelper *helper)
{
  g_return_val_if_fail (EXO_IS_HELPER (helper), NULL);
  return *helper->commands_with_parameter;
}



/**
 * exo_helper_execute:
 * @helper    : an #ExoHelper.
 * @screen    : the #GdkScreen on which to execute @helper or %NULL to use default.
 * @parameter : the parameter to pass to @helper (e.g. URL for WebBrowser) or %NULL
 *              to just run @helper.
 * @error     : return location for errors or %NULL.
 *
 * Executes @helper on @screen with the given @parameter. Returns %TRUE if the
 * execution succeed, else %FALSE and @error will be set.
 *
 * Return value: %TRUE on success, %FALSE if @error is set.
 **/
gboolean
exo_helper_execute (ExoHelper   *helper,
                    GdkScreen   *screen,
                    const gchar *parameter,
                    GError     **error)
{
  gboolean succeed = FALSE;
  GError  *err = NULL;
  gchar  **commands;
  gchar  **argv;
  gchar   *command;
  guint    n;

  // FIXME: startup-notification

  g_return_val_if_fail (EXO_IS_HELPER (helper), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
  g_return_val_if_fail (screen == NULL || GDK_IS_SCREEN (screen), FALSE);

  /* fallback to default screen */
  if (G_UNLIKELY (screen == NULL))
    screen = gdk_screen_get_default ();

  /* determine the command set to use */
  commands = (parameter != NULL) ? helper->commands_with_parameter : helper->commands;

  /* verify that we have atleast one command */
  if (G_UNLIKELY (*commands == NULL))
    {
      g_set_error (error, G_SPAWN_ERROR, G_SPAWN_ERROR_INVAL, _("No command specified"));
      return FALSE;
    }

  /* try to run the helper using the various given commands */
  for (n = 0; commands[n] != NULL; ++n)
    {
      /* reset the error */
      g_clear_error (&err);

      /* parse the command */
      command = (parameter != NULL) ? exo_str_replace (commands[n], "%s", parameter) : g_strdup (commands[n]);
      succeed = g_shell_parse_argv (command, NULL, &argv, &err);
      g_free (command);

      /* check if the parsing failed */
      if (G_UNLIKELY (!succeed))
        continue;

      /* try to run the command */
      succeed = gdk_spawn_on_screen (screen, NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &err);

      /* cleanup */
      g_strfreev (argv);

      /* check if the execution was successful */
      if (G_LIKELY (succeed))
        break;
    }

  /* propagate the error */
  if (G_UNLIKELY (!succeed))
    g_propagate_error (error, err);

  return succeed;
}




static void       exo_helper_database_class_init  (ExoHelperDatabaseClass *klass);
static void       exo_helper_database_init        (ExoHelperDatabase      *database);
static void       exo_helper_database_finalize    (GObject                *object);
static ExoHelper *exo_helper_database_lookup      (ExoHelperDatabase      *database,
                                                   ExoHelperCategory       category,
                                                   const gchar            *id);



struct _ExoHelperDatabaseClass
{
  GObjectClass __parent__;
};

struct _ExoHelperDatabase
{
  GObject     __parent__;
  GHashTable *helpers;
};



static GObjectClass *exo_helper_database_parent_class;



GType
exo_helper_database_get_type (void)
{
  static GType type = G_TYPE_INVALID;

  if (G_UNLIKELY (type == G_TYPE_INVALID))
    {
      static const GTypeInfo info =
      {
        sizeof (ExoHelperDatabaseClass),
        NULL,
        NULL,
        (GClassInitFunc) exo_helper_database_class_init,
        NULL,
        NULL,
        sizeof (ExoHelperDatabase),
        0,
        (GInstanceInitFunc) exo_helper_database_init,
        NULL,
      };

      type = g_type_register_static (G_TYPE_OBJECT, I_("ExoHelperDatabase"), &info, 0);
    }

  return type;
}



static void
exo_helper_database_class_init (ExoHelperDatabaseClass *klass)
{
  GObjectClass *gobject_class;

  /* determine the parent type class */
  exo_helper_database_parent_class = g_type_class_peek_parent (klass);

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->finalize = exo_helper_database_finalize;
}



static void
exo_helper_database_init (ExoHelperDatabase *database)
{
  database->helpers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
}



static void
exo_helper_database_finalize (GObject *object)
{
  ExoHelperDatabase *database = EXO_HELPER_DATABASE (object);

  g_hash_table_destroy (database->helpers);

  (*G_OBJECT_CLASS (exo_helper_database_parent_class)->finalize) (object);
}



static ExoHelper*
exo_helper_database_lookup (ExoHelperDatabase *database,
                            ExoHelperCategory  category,
                            const gchar       *id)
{
  ExoHelper *helper;
  XfceRc    *rc;
  gchar     *file;
  gchar     *spec;

  g_return_val_if_fail (EXO_IS_HELPER_DATABASE (database), NULL);
  g_return_val_if_fail (id != NULL, NULL);

  /* generate the spec for the helper */
  spec = g_strconcat ("xfce4/helpers/", id, ".desktop", NULL);

  /* try to find a cached version */
  helper = g_hash_table_lookup (database->helpers, spec);

  /* load the helper from the file */
  if (G_LIKELY (helper == NULL))
    {
      xfce_resource_push_path (XFCE_RESOURCE_DATA, DATADIR);
      file = xfce_resource_lookup (XFCE_RESOURCE_DATA, spec);
      xfce_resource_pop_path (XFCE_RESOURCE_DATA);

      if (G_LIKELY (file != NULL))
        {
          rc = xfce_rc_simple_open (file, TRUE);
          if (G_LIKELY (rc != NULL))
            {
              helper = exo_helper_new (id, rc);
              xfce_rc_close (rc);
            }
          g_free (file);
        }

      /* add the loaded helper to the cache */
      if (G_LIKELY (helper != NULL))
        {
          g_hash_table_insert (database->helpers, spec, helper);
          spec = NULL;
        }
    }

  if (G_LIKELY (helper != NULL))
    {
      if (exo_helper_get_category (helper) == category)
        g_object_ref (G_OBJECT (helper));
      else
        helper = NULL;
    }

  g_free (spec);

  return helper;
}



/**
 * exo_helper_database_get:
 *
 * Returns a reference on the default #ExoHelperDatabase
 * instance. The caller is responsible to free the
 * returned object using g_object_unref() when no longer
 * needed.
 *
 * Return value: a reference to the default #ExoHelperDatabase.
 **/
ExoHelperDatabase*
exo_helper_database_get (void)
{
  static ExoHelperDatabase *database = NULL;

  if (G_LIKELY (database == NULL))
    {
      database = g_object_new (EXO_TYPE_HELPER_DATABASE, NULL);
      g_object_add_weak_pointer (G_OBJECT (database), (gpointer) &database);
    }
  else
    {
      g_object_ref (G_OBJECT (database));
    }

  return database;
}



/**
 * exo_helper_database_get_default:
 * @database : an #ExoHelperDatabase.
 * @category : an #ExoHelperCategory.
 *
 * Returns a reference on the default #ExoHelper for
 * the @category in @database or %NULL if no default
 * #ExoHelper is registered for @category.
 *
 * The caller is responsible to free the returned
 * object using g_object_unref() when no longer needed.
 *
 * Return value: the default #ExoHelper for @category
 *               or %NULL.
 **/
ExoHelper*
exo_helper_database_get_default (ExoHelperDatabase *database,
                                 ExoHelperCategory  category)
{
  const gchar *id;
  ExoHelper   *helper = NULL;
  XfceRc      *rc;
  gchar       *key;

  g_return_val_if_fail (EXO_IS_HELPER_DATABASE (database), NULL);
  g_return_val_if_fail (category < EXO_HELPER_N_CATEGORIES, NULL);

  rc = xfce_rc_config_open (XFCE_RESOURCE_CONFIG, "xfce4/helpers.rc", TRUE);
  if (G_LIKELY (rc != NULL))
    {
      key = exo_helper_category_to_string (category);
      id = xfce_rc_read_entry_untranslated (rc, key, NULL);
      if (G_LIKELY (id != NULL))
        helper = exo_helper_database_lookup (database, category, id);
      xfce_rc_close (rc);
      g_free (key);
    }

  return helper;
}



/**
 * exo_helper_database_set_default:
 * @database : an #ExoHelperDatabase.
 * @category : an #ExoHelperCategory.
 * @helper   : an #ExoHelper.
 * @error    : return location for errors or %NULL.
 *
 * Sets the default #ExoHelper for @category in @database to
 * @helper. Returns %TRUE on success, %FALSE if @error is set.
 *
 * Return value: %TRUE on success, %FALSE if @error is set.
 **/
gboolean
exo_helper_database_set_default (ExoHelperDatabase *database,
                                 ExoHelperCategory  category,
                                 ExoHelper         *helper,
                                 GError           **error)
{
  XfceRc *rc;
  gchar  *key;

  g_return_val_if_fail (category < EXO_HELPER_N_CATEGORIES, FALSE);
  g_return_val_if_fail (EXO_IS_HELPER_DATABASE (database), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
  g_return_val_if_fail (EXO_IS_HELPER (helper), FALSE);

  /* open the helpers.rc for writing */
  rc = xfce_rc_config_open (XFCE_RESOURCE_CONFIG, "xfce4/helpers.rc", FALSE);
  if (G_UNLIKELY (rc == NULL))
    {
      g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_IO, _("Failed to open helpers.rc for writing"));
      return FALSE;
    }

  /* save the new setting */
  key = exo_helper_category_to_string (category);
  xfce_rc_write_entry (rc, key, exo_helper_get_id (helper));
  xfce_rc_close (rc);
  g_free (key);

  return TRUE;
}



static gint
helper_compare (gconstpointer a,
                gconstpointer b)
{
  return g_utf8_collate (exo_helper_get_name (a), exo_helper_get_name (b));
}



/**
 * exo_helper_database_get_all:
 * @database : an #ExoHelperDatabase.
 * @category : an #ExoHelperCategory.
 *
 * Looks up all available helpers for @category
 * in @database, sorted in alphabetic order.
 *
 * The returned list keeps references on the included
 * helpers, so be sure to run
 * <informalexample><programlisting>
 *  g_list_foreach (list, (GFunc) g_object_unref, NULL);
 *  g_list_free (list);
 * </programlisting></informalexample>
 * when you are done.
 *
 * Return value: The list of all helpers available
 *               in @category.
 **/
GList*
exo_helper_database_get_all (ExoHelperDatabase *database,
                             ExoHelperCategory  category)
{
  ExoHelper *helper;
  GList     *helpers = NULL;
  gchar    **specs;
  gchar     *id;
  gchar     *s;
  guint      n;

  g_return_val_if_fail (EXO_IS_HELPER_DATABASE (database), NULL);
  g_return_val_if_fail (category < EXO_HELPER_N_CATEGORIES, NULL);

  xfce_resource_push_path (XFCE_RESOURCE_DATA, DATADIR);
  specs = xfce_resource_match (XFCE_RESOURCE_DATA, "xfce4/helpers/*.desktop", TRUE);
  xfce_resource_pop_path (XFCE_RESOURCE_DATA);

  for (n = 0; specs[n] != NULL; ++n)
    {
      s = strrchr (specs[n], '.');
      if (G_LIKELY (s != NULL))
        *s = '\0';

      id = strrchr (specs[n], '/');
      id = (id != NULL) ? id + 1 : specs[n];

      helper = exo_helper_database_lookup (database, category, id);
      if (G_LIKELY (helper != NULL))
        helpers = g_list_insert_sorted (helpers, helper, helper_compare);

      g_free (specs[n]);
    }
  g_free (specs);

  return helpers;
}



/**
 * exo_helper_database_get_custom:
 * @database : an #ExoHelperDatabase.
 * @category : an #ExoHelperCategory.
 *
 * Returns the custom #ExoHelper set for @database
 * or %NULL if no custom #ExoHelper is set for
 * @category.
 *
 * The caller is responsible to free the returned
 * object using g_object_unref() when no longer
 * needed.
 *
 * Return value: the custom #ExoHelper for @category
 *               in @database or %NULL.
 **/
ExoHelper*
exo_helper_database_get_custom (ExoHelperDatabase *database,
                                ExoHelperCategory  category)
{
  gchar *string;
  gchar  id[256];

  g_return_val_if_fail (EXO_IS_HELPER_DATABASE (database), NULL);
  g_return_val_if_fail (category < EXO_HELPER_N_CATEGORIES, NULL);

  /* determine the id for the custom helper */
  string = exo_helper_category_to_string (category);
  g_snprintf (id, sizeof (id), "custom-%s", string);
  g_free (string);

  return exo_helper_database_lookup (database, category, id);
}



/**
 * exo_helper_database_set_custom:
 * @database : an #ExoHelperDatabase.
 * @category : an #ExoHelperCategory.
 * @command  : the custom command.
 *
 * Sets the custom helper for @category in @database
 * to @command.
 **/
void
exo_helper_database_set_custom (ExoHelperDatabase *database,
                                ExoHelperCategory  category,
                                const gchar       *command)
{
  XfceRc *rc;
  gchar **argv;
  gchar  *category_string;
  gchar  *basename;
  gchar  *cmdline;
  gchar  *file;
  gchar   spec[256];

  g_return_if_fail (EXO_IS_HELPER_DATABASE (database));
  g_return_if_fail (category < EXO_HELPER_N_CATEGORIES);
  g_return_if_fail (command != NULL && *command != '\0');

  /* determine the spec for the custom helper */
  category_string = exo_helper_category_to_string (category);
  g_snprintf (spec, sizeof (spec), "xfce4/helpers/custom-%s.desktop", category_string);

  /* lookup the resource save location */
  file = xfce_resource_save_location (XFCE_RESOURCE_DATA, spec, TRUE);

  /* write the custom helper file */
  rc = xfce_rc_simple_open (file, FALSE);
  if (G_LIKELY (rc != NULL))
    {
      xfce_rc_set_group (rc, "Desktop Entry");
      xfce_rc_write_bool_entry (rc, "NoDisplay", TRUE);
      xfce_rc_write_entry (rc, "Version", "1.0");
      xfce_rc_write_entry (rc, "Encoding", "UTF-8");
      xfce_rc_write_entry (rc, "Type", "X-XFCE-Helper");
      xfce_rc_write_entry (rc, "X-XFCE-Category", category_string);

      /* check if the command includes a parameter */
      cmdline = (strstr (command, "%s") != NULL) ? g_strdup (command) : g_strconcat (command, " \"%s\"", NULL);

      /* use the command line for the CommandsWithParameter */
      xfce_rc_write_entry (rc, "X-XFCE-CommandsWithParameter", cmdline);

      /* try to parse the command line */
      if (g_shell_parse_argv (cmdline, NULL, &argv, NULL))
        {
          /* use the basename for Name and Icon */
          basename = g_path_get_basename (*argv);
          xfce_rc_write_entry (rc, "Icon", basename);
          xfce_rc_write_entry (rc, "Name", basename);
          g_free (basename);

          /* use only the binary for the Commands */
          xfce_rc_write_entry (rc, "X-XFCE-Commands", *argv);

          /* cleanup */
          g_strfreev (argv);
        }
      else
        {
          xfce_rc_write_entry (rc, "Name", command);
          xfce_rc_write_entry (rc, "X-XFCE-Commands", command);
        }

      /* save the helper file */
      xfce_rc_close (rc);

      /* cleanup */
      g_free (cmdline);
    }

  /* ditch any cached object */
  g_hash_table_remove (database->helpers, spec);

  /* cleanup */
  g_free (category_string);
  g_free (file);
}
