/*
 * Copyright (C) 2010 Red Hat, Inc.
 * Copyright (C) 2012 Canonical
 *
 * This program 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 Licence, 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 General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Matthias Clasen <mclasen@redhat.com>
 *          Didier Roche <didier.roche@canonical.com>
 */

#include <sys/stat.h>

#include <glib.h>
#include <gio/gio.h>
#include <errno.h>
#include <locale.h>

#define PROGRAM_NAME "user-session-migration"
#define MIGRATION_FILENAME_BASE "user_session_migration-"
#define SKIP_EXIT_STATUS 77

static gboolean verbose = FALSE;
static gboolean dry_run = FALSE;

static gchar*
get_migration_filename ()
{
  const char *legacy_desktop_session;
  g_autofree char *full_session_name = NULL;

  if ((legacy_desktop_session = g_getenv ("DESKTOP_SESSION")) &&
      *legacy_desktop_session != '\0')
    {
      g_autofree char *legacy_full_session_name = NULL;
      g_autofree char *legacy_filename = NULL;

      legacy_full_session_name = g_strdup_printf ("%s%s", MIGRATION_FILENAME_BASE,
                                                  legacy_desktop_session);
      legacy_filename = g_build_filename (g_get_user_data_dir (),
                                          legacy_full_session_name, NULL);

      if (g_file_test (legacy_filename, G_FILE_TEST_EXISTS))
        return g_steal_pointer (&legacy_filename);
    }

  full_session_name = g_strdup_printf ("%s.state",
                                       g_getenv ("XDG_CURRENT_DESKTOP"));

  return g_build_filename (g_get_user_data_dir (), PROGRAM_NAME,
                           full_session_name, NULL);
}

static gboolean
migrate_from_file (const gchar *script_path)
{
  g_autoptr (GError) error = NULL;
  g_autofree char *stdout = NULL;
  g_autofree char *stderr = NULL;
  gint wait_status;

  if G_UNLIKELY (verbose)
    g_print ("Executing: %s\n", script_path);

  if G_UNLIKELY (dry_run)
    return TRUE;

  if (!g_spawn_command_line_sync (script_path, &stdout, &stderr, &wait_status, &error))
    {
      g_printerr ("%s\nstdout: %s\nstderr: %s\n", error->message, stdout, stderr);
      return FALSE;
    }

  if (WIFEXITED (wait_status) && WEXITSTATUS (wait_status) == SKIP_EXIT_STATUS)
    {
      if G_UNLIKELY (verbose)
        {
          g_print ("'%s' exited with SKIP exit code\nstdout: %s\nstderr: %s\n",
                   script_path, stdout, stderr);
        }

      return FALSE;
    }

  if (!g_spawn_check_wait_status (wait_status, &error))
    {
      g_printerr ("'%s' exited with an error: %s\nstdout: %s\nstderr: %s\n",
                  script_path, error->message, stdout, stderr);
      return FALSE;
    }

  return TRUE;
}

static gboolean
migrate_from_dir (const gchar *dirname,
                  time_t       stored_mtime,
                  GHashTable  *migrated,
                  gboolean    *changed)
{
  time_t dir_mtime;
  struct stat statbuf;
  const gchar *name;
  gchar *filename;
  g_autoptr (GDir) dir = NULL;
  g_autoptr (GPtrArray) migration_scripts = NULL;
  g_autoptr (GError) error = NULL;

  *changed = FALSE;

  /* If the directory is not newer, exit */
  if (stat (dirname, &statbuf) == 0)
    dir_mtime = statbuf.st_mtime;
  else
    {
      if G_UNLIKELY (verbose)
        g_print ("Directory '%s' does not exist, nothing to do\n", dirname);
      return TRUE;
    }

  if (dir_mtime <= stored_mtime)
    {
      if G_UNLIKELY (verbose)
        g_print ("Directory '%s' all uptodate, nothing to do\n", dirname);
      return TRUE;
    }

  dir = g_dir_open (dirname, 0, &error);
  if (dir == NULL)
    {
      g_printerr ("Failed to open '%s': %s\n", dirname, error->message);
      return FALSE;
    }

  if G_UNLIKELY (verbose)
      g_print ("Using '%s' directory\n", dirname);

  while ((name = g_dir_read_name (dir)) != NULL)
    {
      if (!name)
        continue;

      if (g_hash_table_lookup (migrated, name))
        {
          if G_UNLIKELY (verbose)
            g_print ("File '%s already migrated, skipping\n", name);
          continue;
        }

      if (!migration_scripts)
        migration_scripts = g_ptr_array_new ();

      g_ptr_array_add (migration_scripts, (gpointer) name);
    }

  if (migration_scripts == NULL)
    return TRUE;

  g_ptr_array_sort_values (migration_scripts, (GCompareFunc) strcmp);

  for (guint idx = 0; idx < migration_scripts->len; ++idx)
    {
      const char *script_name = g_ptr_array_index (migration_scripts, idx);
      g_autofree char *filename = NULL;

      filename = g_build_filename (dirname, script_name, NULL);

      if (migrate_from_file (filename))
        {
          g_autofree char *myname = g_strdup (script_name);

          /* add the file to the migrated list */
          g_hash_table_add (migrated, g_steal_pointer (&myname));
          *changed = TRUE;
        }
    }

  return TRUE;
}

/* get_string_set() and set_string_set() could be GKeyFile API */
static GHashTable *
get_string_set (GKeyFile     *keyfile,
                const gchar  *group,
                const gchar  *key,
                GError      **error)
{
  g_autoptr (GHashTable) migrated = NULL;
  g_auto (GStrv) list = NULL;
  gint i;

  list = g_key_file_get_string_list (keyfile, group, key, NULL, error);

  if (list == NULL)
    return NULL;

  migrated = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
  for (i = 0; list[i]; i++)
    g_hash_table_add (migrated, g_steal_pointer (&list[i]));

  return g_steal_pointer (&migrated);
}

static void
set_string_set (GKeyFile    *keyfile,
                const gchar *group,
                const gchar *key,
                GHashTable  *set)
{
  const char * const *values = NULL;
  guint n_values = 0;

  values = (const char * const*) g_hash_table_get_keys_as_array (set, &n_values);

  g_key_file_set_string_list (keyfile, group, key, values, n_values);
}

static GHashTable *
load_state (time_t *mtime)
{
  g_autoptr (GKeyFile) keyfile = NULL;
  g_autoptr (GHashTable) migrated = NULL;
  g_autoptr (GHashTable) saved_migrated = NULL;
  g_autoptr (GError) error = NULL;
  g_autofree char *filename = NULL;
  g_autofree char *timestamp = NULL;

  migrated = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);

  filename = get_migration_filename ();
  if G_UNLIKELY (verbose)
    g_print ("Using state file '%s'\n", filename);

  /* ensure file exists */
  if (!g_file_test (filename, G_FILE_TEST_EXISTS))
    return g_steal_pointer (&migrated);

  keyfile = g_key_file_new();
  if (!g_key_file_load_from_file (keyfile, filename, 0, &error))
    {
      g_printerr ("Failed to load state file %s: %s\n", filename, error->message);
      return g_steal_pointer (&migrated);
    }

  if ((timestamp = g_key_file_get_string (keyfile, "State", "timestamp", &error)) == NULL)
    {
      g_printerr ("Failed to load timestamp: %s\n", error->message);
      g_clear_error (&error);
    }
  else
    {
      *mtime = (time_t) g_ascii_strtoll (timestamp, NULL, 0);
    }

  if ((saved_migrated = get_string_set (keyfile, "State", "migrated", &error)) == NULL)
    {
      g_printerr ("Failed to load migrated scripts: %s\n", error->message);
      return g_steal_pointer (&migrated);
    }

  return g_steal_pointer (&saved_migrated);
}

static gboolean
save_state (GHashTable *migrated)
{
  g_autofree char *str = NULL;
  g_autofree char *filename = NULL;
  g_autofree char *dirname = NULL;
  g_autoptr (GKeyFile) keyfile = NULL;
  g_autoptr (GError) error = NULL;

  filename = get_migration_filename();

  /* Make sure the state directory exists */
  dirname = g_path_get_dirname (filename);
  if (g_mkdir_with_parents (dirname, 0700))
    {
      g_printerr ("Failed to create directory %s: %s\n",
                  dirname, g_strerror (errno));
      return FALSE;
    }

  keyfile = g_key_file_new ();

  str = g_strdup_printf ("%lld", (long long)time (NULL));
  g_key_file_set_string (keyfile,
                         "State", "timestamp", str);

  set_string_set (keyfile, "State", "migrated", migrated);

  g_clear_pointer (&str, g_free);
  str = g_key_file_to_data (keyfile, NULL, NULL);

  if (!g_file_set_contents (filename, str, -1, &error))
    {
      g_printerr ("Writing '%s': %s\n", filename, error->message);
      return FALSE;
    }

  return TRUE;
}

int
main (int argc, char *argv[])
{
  g_autoptr (GOptionContext) context = NULL;
  g_autoptr (GHashTable) migrated = NULL;
  g_autoptr (GError) error = NULL;
  time_t stored_mtime = 0;
  const gchar * const *data_dirs;
  const gchar *extra_file = NULL;
  gboolean changed = FALSE;
  int i;

  GOptionEntry entries[] = {
    { "verbose", 0, 0, G_OPTION_ARG_NONE, &verbose, "show verbose messages", NULL },
    { "dry-run", 0, 0, G_OPTION_ARG_NONE, &dry_run, "do not perform any changes", NULL },
    { "file", 0, 0, G_OPTION_ARG_STRING, &extra_file, "Force a migration from this file only (no storage of migrated status)", NULL },
    { NULL }
  };

  setlocale (LC_ALL, "");
  g_set_prgname (PROGRAM_NAME);

  context = g_option_context_new ("");
  g_option_context_set_summary (context,
    "Migrate in user session settings.");
  g_option_context_add_main_entries (context, entries, NULL);

  if (!g_option_context_parse (context, &argc, &argv, &error))
    {
      g_printerr ("%s\n", error->message);
      return EXIT_FAILURE;
    }

  migrated = load_state (&stored_mtime);

  if (extra_file)
    {
      if (!migrate_from_file (extra_file))
        return EXIT_FAILURE;

      return EXIT_SUCCESS;
    }

  data_dirs = g_get_system_data_dirs ();
  for (i = 0; data_dirs[i]; i++)
    {
      g_autofree char *migration_dir = NULL;
      gboolean changed_in_dir;

      migration_dir = g_build_filename (data_dirs[i], PROGRAM_NAME, "scripts", NULL);

      if (!migrate_from_dir (migration_dir, stored_mtime, migrated, &changed_in_dir))
        return EXIT_FAILURE;

      changed = changed | changed_in_dir;
    }

  if (changed && G_LIKELY (!dry_run))
    {
      if (!save_state (migrated))
        return EXIT_FAILURE;
    }

  return EXIT_SUCCESS;
}
