/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
#include <string.h>
#include <stdlib.h>
#include <gconf/gconf.h>
#include <gconf/gconf-client.h>

#include "shell-activities.h"
#include "shell-global.h"
#include "shell-marshal.h"

#define ACTIVITIES_GCONF_DIR SHELL_GCONF_DIR"/activities"

struct _ShellActivities {
  GObject parent;

  GHashTable *table; /* Associate an ID with its ShellActivity */
  GSList *timeline; /* List of ActivityEvents, to track activity switching */
  GArray *by_workspace; /* Associate a workspaces with its activity */
  GConfClient *gconf_client;
  guint gconf_notify;
};


/* Internal, used in the timeline list
 * to identify moments when activities were switched */
struct ActivityEvent {
  guint32 timestamp;
  int activity; /* The activity that became current at that point */
};

typedef struct ActivityEvent ActivityEvent;

/* Signals */
enum
{
  ACTIVITY_SWITCHED,
  ACTIVITY_RENAMED,
  ASSOCIATION_CHANGED,

  LAST_SIGNAL
};

G_DEFINE_TYPE(ShellActivities, shell_activities, G_TYPE_OBJECT);

static guint signals [LAST_SIGNAL] = { 0 };


static void conf_changed (GConfClient *client,
                          guint        cnxn_id,
                          GConfEntry  *entry,
                          gpointer     user_data);

static void on_workspace_switched (int      index,
                                   guint32  timestamp,
                                   gpointer activities);

static void on_workspace_added (int      index,
                                gpointer activities);

static void on_workspace_removed (int      index,
                                  gpointer activities);


static void
shell_activities_init (ShellActivities *self)
{
  GError *err = NULL;
  GSList *id_list;
  ShellActivity *activity;
  ActivityEvent *event;
  gchar *string_id;
  gchar *key;
  MetaScreen *screen;
  int n_workspaces;
  int *stored_id;
  GSList *workspaces_list;
  GSList *l;

  self->table = g_hash_table_new_full (g_direct_hash, g_direct_equal,
                                       g_free, g_free);
  
  /* Create the default activity */
  activity = g_new (ShellActivity, 1);
  activity->id = 0;
  activity->name = "Default";
  g_hash_table_replace (self->table, 0, activity);
  
  /* Set origin of timeline to default activity */
  event = g_new (ActivityEvent, 1);
  event->timestamp = 0;
  event->activity = 0;
  self->timeline = g_slist_prepend (NULL, event);

  /* Restore workspace <-> activity associations. We assume the window manager
   * has already created enough workspaces for us. Workspaces that are not
   * associated to an activity will be set to 0 (Default activity)
  g_object_get (G_OBJECT (shell_global_get ()), "screen", &screen, NULL);
  n_workspaces = meta_screen_get_n_workspaces (screen);
  self->by_workspace = g_array_sized_new (FALSE, TRUE, sizeof (int), n_workspaces);
  g_array_set_size (self->by_workspace, n_workspaces); */
  self->by_workspace = g_array_sized_new (FALSE, TRUE, sizeof (int), 3); /* To remove */

  self->gconf_client = gconf_client_get_default ();
  gconf_client_add_dir (self->gconf_client, ACTIVITIES_GCONF_DIR,
                        GCONF_CLIENT_PRELOAD_RECURSIVE, &err);
  if (err)
    goto error;
  
  self->gconf_notify =
    gconf_client_notify_add (self->gconf_client, ACTIVITIES_GCONF_DIR,
                             conf_changed, self, NULL, &err);
  if (err)
    goto error;
      
  id_list = gconf_client_get_list (self->gconf_client, ACTIVITIES_GCONF_DIR"/id_list",
                                   GCONF_VALUE_INT, &err);
  if (err)
    goto error;
  
  while (id_list)
    {
      activity = g_new (ShellActivity, 1);
      activity->id = GPOINTER_TO_INT (id_list->data);

      string_id = g_strdup_printf ("%i", activity->id);
      key = g_build_filename (ACTIVITIES_GCONF_DIR, string_id, "name", NULL);
      activity->name = gconf_client_get_string (self->gconf_client, key, NULL);
      g_free (key);

      /* If the activity was associated to workspaces,
       * fill the workspaces array accordingly */
      key = g_build_filename (ACTIVITIES_GCONF_DIR, string_id,
                              "workspaces", NULL);
      workspaces_list = gconf_client_get_list (self->gconf_client, key,
                                               GCONF_VALUE_INT, NULL);
      for (l = workspaces_list; l; l = l->next)
        {
          /* l->data is the index of the workspace,
           * *stored_id is the activity ID associated with it */
          stored_id = &g_array_index (self->by_workspace, int,
                                      GPOINTER_TO_INT (l->data));
          *stored_id = activity->id;
        }
      g_slist_free (workspaces_list);
      g_free (key);

      g_free (string_id);
      g_hash_table_insert (self->table, GINT_TO_POINTER (activity->id), activity);
      id_list = g_slist_next (id_list);
    }
  g_slist_free (id_list);

  /* Watch workspaces from window manager */
  g_signal_connect (screen, "workspace-switched",
                    G_CALLBACK (on_workspace_switched), self);
  g_signal_connect (screen, "workspace-added",
                    G_CALLBACK (on_workspace_added), self);
  g_signal_connect (screen, "workspace-removed",
                    G_CALLBACK (on_workspace_removed), self);
    
  return;
  
  error:
    g_warning ("Could not load activities from GConf: %s", err->message);
    g_error_free (err);
}

static void
shell_activities_finalize (GObject *object)
{
  ShellActivities *self = SHELL_ACTIVITIES (object);
  
  g_object_unref (self->gconf_client);
  g_hash_table_destroy (self->table);
}

static ShellActivity *
copy_activity (ShellActivity *old)
{
  ShellActivity *new;
  new = g_new (ShellActivity, 1);
  new->id = old->id;
  new->running = old->running;
  new->name = g_strdup (old->name);
  return new;
}

static void
free_activity (ShellActivity *activity)
{
  g_free (activity->name);
  g_free (activity);
}

GType
shell_activity_get_type (void)
{
  static GType our_type = 0;

  if (our_type == 0)
    our_type = g_boxed_type_register_static ("ShellActivity",
                                             (GBoxedCopyFunc) copy_activity,
                                             (GBoxedFreeFunc) free_activity);

  return our_type;
}


static void
shell_activities_class_init (ShellActivitiesClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->finalize = shell_activities_finalize;

  signals[ACTIVITY_SWITCHED] =
    g_signal_new ("activity-switched",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  _shell_marshal_VOID__INT_INT,
                  G_TYPE_NONE, 2,
                  G_TYPE_INT,
                  G_TYPE_INT);
  
  signals[ACTIVITY_RENAMED] =
    g_signal_new ("activity-renamed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  _shell_marshal_VOID__INT_STRING,
                  G_TYPE_NONE, 2,
                  G_TYPE_INT,
                  G_TYPE_STRING);

  signals[ASSOCIATION_CHANGED] =
    g_signal_new ("association-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  _shell_marshal_VOID__INT_INT,
                  G_TYPE_NONE, 2,
                  G_TYPE_INT,
                  G_TYPE_INT);
}

ShellActivities *
shell_activities_new (void)
{
  return g_object_new (SHELL_TYPE_ACTIVITIES, NULL);
}

void
conf_changed (GConfClient *client,
              guint        cnxn_id,
              GConfEntry  *entry,
              gpointer     activities)
{
  ShellActivities *self = activities;
  GConfValue *value;
  const char *key;
  ShellActivity *activity;
  GSList *id_list;
  int id;
  gchar *id_string;
  char *tmp;
  char *key_name;
  GSList *workspaces_list;
  GSList *l;
  int *stored_id;
  int i;

  value = gconf_entry_get_value (entry);
  key = gconf_entry_get_key (entry);
  key_name = g_path_get_basename (key);

  if (strcmp (key_name, "current") == 0) /* Current change (FIXME) */
    {
      GTimeVal tv;
      g_get_current_time (&tv);
      shell_activities_set_current (self, gconf_value_get_int (value), tv.tv_sec);
    }
  if (strcmp (key_name, "id_list") == 0) /* FIXME: handle removed activities too? */
    {
      id_list = gconf_value_get_list (value);
      while (id_list)
        {
          id = gconf_value_get_int (id_list->data);
          if (!g_hash_table_lookup (self->table, GINT_TO_POINTER (id))) /* New activity */
            {
              activity = g_new (ShellActivity, 1);
              activity->id = id;
              activity->name = NULL;
              /* Create activity, fill properties in other callbacks */
              g_hash_table_insert (self->table, GINT_TO_POINTER (activity->id),
                                   activity);
            }
          gconf_value_free (id_list->data); /* FIXME: wrong? */
          id_list = g_slist_next (id_list);
        }
      gconf_value_free (value);
    }
  
  /* If we're here, the key is an activity property */
  
  /* Get the name of parent folder, which is the activity ID */
  tmp = g_path_get_dirname (key);
  id_string = g_path_get_basename (tmp);
  g_free (tmp);
  activity = g_hash_table_lookup (self->table, GINT_TO_POINTER (atoi (id_string)));
  g_free (id_string);
  
  /* Activities must be added to the ID list first, else ignore them */
  if (activity)
    {
      if (strcmp (key_name, "name") == 0)
        {
          activity->name = g_strdup (gconf_value_get_string (value));
          g_signal_emit (self, signals[ACTIVITY_RENAMED], 0,
                     activity->id, activity->name);
        }
      else if (strcmp (key_name, "workspaces") == 0)
        {
          workspaces_list = gconf_value_get_list (value);
          for (l = workspaces_list; l; l = l->next)
            {
              /* l->data stores the index of the workspace,
               * *stored_id is the activity ID associated with it */
              i = gconf_value_get_int ((GConfValue*) l->data);
              stored_id = &g_array_index (self->by_workspace, int, i);
              *stored_id = activity->id;
            }
          gconf_value_free (value);
        }
    }
  g_free (key_name);
}

static
gboolean find_by_name (gpointer key,
                       gpointer value,
                       gpointer user_data)
{
  ShellActivity *activity = (ShellActivity *)value;
  
  if (strcmp (activity->name, (char *) user_data) == 0)
    return TRUE;
  else
    return FALSE;
}

/**
 * shell_activities_get_current:
 * Get the ID of the activity that was the current one at a point in time.
 * There's no guarantee that all the activities history is kept during a whole
 * session: this function is mainly intended to avoid confusion with very
 * close X events.
 *
 * @activities the activities object instance
 * @timestamp the timestamp identifying the moment (usually get from X events)
 * Return value: the (positive) identifier for this activity, or the ID for the
 *     current activity if @timestamp is in the future, or -1 if @timestamp
 *     is either invalid or too old.
 */
int
shell_activities_get_current (ShellActivities *activities,
                              guint32          timestamp)
{
  ActivityEvent *event;
  GSList *l;

  for (l = activities->timeline; l; l = l->next)
    {
      event = l->data;
      if (event->timestamp <= timestamp)
          return event->activity;
    }

  return -1;
}

/**
 * shell_activities_set_current:
 * Set the current activity to the given ID, starting from a point in time.
 *
 * @activities the activities object instance
 * @activity the ID of the activity
 * @timestamp the timestamp identifying the moment (usually got from X events)
 * Return value: %TRUE on success, %FALSE if:
 *     1) no activity exists with such an identifier, or
 *     2) if @timestamp is either invalid or too old so that current activity
       has already been set since then
 */
gboolean
shell_activities_set_current (ShellActivities *activities,
                              int              activity,
                              guint32          timestamp)
{
  g_message ("Current activity: %i at %i", activity, timestamp);
  
  /* Most recent node in the list */
  ActivityEvent *event = activities->timeline->data;

  /* Too late, already set from elsewhere with a more recent timestamp */
  if (event->timestamp >= timestamp)
    return FALSE;

  /* Invalid ID */
  if (!g_hash_table_lookup (activities->table, GINT_TO_POINTER (activity)))
    return FALSE;

  event = g_new (ActivityEvent, 1);
  event->activity = activity;
  event->timestamp = timestamp;
  activities->timeline = g_slist_prepend (activities->timeline, event);

  return TRUE;
}

/**
 * shell_activities_create:
 * Create a new activity, that will be saved over sessions.
 *
 * @activities the activities object instance
 * @name the name you want to give to this activity, free yourself when needed
 * Return value: the (positive) identifier for this activity, or -1 if an
 *   activity with this name already exists.
 */
int
shell_activities_create (ShellActivities *activities,
                         char            *name)
{
  ShellActivity *activity;
  GSList *list = NULL;
  GSList *l;
  GHashTableIter iter;
  gpointer id;
  gchar *key;
  gchar *id_string;
  
  if (g_hash_table_find (activities->table, (GHRFunc) find_by_name, name))
    return -1;
  
  activity = g_new (ShellActivity, 1);
  activity->name = g_strdup (name);
  do /* Get a unique identifier, avoid the risk of having seen it recently */
    {
      activity->id = g_random_int_range (1, G_MAXINT);
    }
  while (g_hash_table_lookup (activities->table, GINT_TO_POINTER (activity->id)));
  g_hash_table_insert (activities->table, GINT_TO_POINTER (activity->id), activity);
  
  /* Update IDs list in GConf */
  g_hash_table_iter_init (&iter, activities->table);
  while (g_hash_table_iter_next (&iter, &id, NULL))
      list = g_slist_prepend (list, id);
  list = g_slist_reverse (list);
  gconf_client_set_list (activities->gconf_client, ACTIVITIES_GCONF_DIR"/id_list",
                         GCONF_VALUE_INT, list, NULL);
  for (l = list; l; l = l->next)
    g_free (l->data);
  g_slist_free (list);

  /* Update activity name in GConf */
  id_string = g_strdup_printf ("%i", activity->id);
  key = g_build_filename (ACTIVITIES_GCONF_DIR, id_string, "name", NULL);
  gconf_client_set_string (activities->gconf_client, key, name, NULL);
  g_free (key);
  g_free (id_string);

  return activity->id;
}

/**
 * shell_activities_delete:
 * Delete an activity.
 *
 * @activities the activities object instance
 * @id the identifier of the activity to delete
 * Return value: %TRUE on success, %FALSE if no activity exists with such an identifier.
 */
gboolean
shell_activities_delete (ShellActivities *activities,
                         int              id)
{
  ShellActivity *activity;
  GSList *list = NULL;
  GSList *l;
  GHashTableIter iter;
  gpointer stored_id;
  gchar *key;
  gchar *id_string;

  activity = g_hash_table_lookup (activities->table, GINT_TO_POINTER (id));
  if (!activity)
    return FALSE;

  g_hash_table_remove (activities->table, GINT_TO_POINTER (id));
  
  /* Update IDs list in GConf */
  g_hash_table_iter_init (&iter, activities->table);
  while (g_hash_table_iter_next (&iter, &stored_id, NULL))
      list = g_slist_prepend (list, stored_id);
  list = g_slist_reverse (list);
  gconf_client_set_list (activities->gconf_client, ACTIVITIES_GCONF_DIR"/id_list",
                         GCONF_VALUE_INT, list, NULL);
  for (l = list; l; l = l->next)
    g_free (l->data);
  g_slist_free (list);

  /* Unset activity name in GConf */
  id_string = g_strdup_printf ("%i", activity->id);
  key = g_build_filename (ACTIVITIES_GCONF_DIR, id_string, "name", NULL);
  gconf_client_unset(activities->gconf_client, key, NULL);
  g_free (key);
  g_free (id_string);

  return activity->id;
}
  
/**
 * shell_activities_rename:
 * Rename an activity.
 *
 * @activities the activities object instance
 * @id the identifier of the activity to rename
 * @name the name you want to give to this activity, free yourself when needed
 * Return value: %TRUE on success, %FALSE if no activity exists with such an identifier.
 */
gboolean
shell_activities_rename (ShellActivities *activities,
                         int              id,
                         const char      *name)
{
  ShellActivity *activity;
  char *key;
  char *id_string;

  activity = g_hash_table_lookup (activities->table, GINT_TO_POINTER (id));
  if (!activity)
    return FALSE;
  
  g_free (activity->name);
  activity->name = g_strdup (name);
  
  /* Update GConf data */
  id_string = g_strdup_printf ("%i", id);
  key = g_build_filename (ACTIVITIES_GCONF_DIR, id_string, "name", NULL);
  gconf_client_set_string (activities->gconf_client, key, name, NULL);
  g_free (key);
  g_free (id_string);
  
  g_signal_emit (activities, signals[ACTIVITY_RENAMED], 0,
                 activity->id, activity->name);
  
  return TRUE;
}

/**
 * shell_activities_get_name:
 * Get the name of the activity identified by @id.
 *
 * @activities the activities object instance
 * @activity_id the ID of the activity
 * Return value: The name of the activity,as stored internally,
 *     or %NULL if the ID is invalid.
 */
const char *
shell_activities_get_name (ShellActivities *activities,
                           int              id)
{
  ShellActivity *activity;

  activity = g_hash_table_lookup (activities->table, GINT_TO_POINTER (id));
  if (!activity)
    return NULL;

  return activity->name; 
}

/**
 * shell_activities_associate_workspace:
 * Associate an activity to a workspace, replacing the old association if needed.
 *
 * @activities the activities object instance
 * @activity_id the ID of the activity
 * @workspace_index the index of the workspace
 * Return value: %TRUE if @activity_id and @workspace_index are valid, %FALSE otherwise.
 */
gboolean
shell_activities_associate_workspace (ShellActivities *activities,
                                      int              activity_id,
                                      int              workspace_index)
{
  int *stored_id;
  char *string_id;
  char *key;
  GSList *list = NULL;
  int i;

  if (!g_hash_table_lookup (activities->table, GINT_TO_POINTER (activity_id)))
    return FALSE;

  stored_id = &g_array_index (activities->by_workspace, int, workspace_index);
  if (!stored_id)
    return FALSE;

  *stored_id = activity_id;

  /* Go though all workspaces to find which are associated to this activity,
   * and update the list in GConf with the result */
  for (i = 0; i < activities->by_workspace->len; i++)
    {
      if (activity_id == g_array_index (activities->by_workspace, int, i))
        list = g_slist_prepend (list, GINT_TO_POINTER (i));
    }
  string_id = g_strdup_printf ("%i", activity_id);
  key = g_build_filename (ACTIVITIES_GCONF_DIR, string_id,
                          "workspaces", NULL);
  gconf_client_set_list (activities->gconf_client, key, GCONF_VALUE_INT,
                         list, NULL);
  g_slist_free (list);
  g_free (string_id);
  g_free (key);

  g_signal_emit (activities, signals[ASSOCIATION_CHANGED], 0,
                 activity_id, workspace_index);

  return TRUE;
}

/**
 * shell_activities_get_from_workspace:
 * Get the activity associated to the workspace identified by the given index.
 *
 * @activities the activities object instance
 * @workspace_index the index of the workspace
 * Return value: The ID of the corresponding activity.
 */
int
shell_activities_get_from_workspace (ShellActivities *activities,
                                     int              workspace_index)
{
  if (workspace_index >= activities->by_workspace->len) /* FIXME: error */
    return 0;

  return g_array_index (activities->by_workspace, int, workspace_index);
}

/**
 * shell_activities_get_list:
 * Get a list of all registered activities, in the form of #ShellActivity structs
 * containing their ID, name and other properties.
 *
 * @activities the activities object instance
 * Return value: (element-type ShellActivity) (transfer full): List of all activities
 */
GSList *
shell_activities_get_list (ShellActivities *activities)
{
  GHashTableIter iter;
  GSList *list = NULL;
  ShellActivity *old;
  ShellActivity *new;

  g_hash_table_iter_init (&iter, activities->table);
  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &old))
    {
      new = g_new (ShellActivity, 1);
      new = copy_activity (old);
      list = g_slist_prepend (list, new);
    }

  return list;
}

static void
on_workspace_switched (int      index,
                       guint32  timestamp,
                       gpointer activities)
{
  ShellActivities *self = activities;
  int id;

  /* Get the activity associated with the current workspace */
  id = g_array_index (self->by_workspace, int, index);
  shell_activities_set_current (self, id, timestamp);
}

static void
on_workspace_added (int      index,
                    gpointer activities)
{
  ShellActivities *self = activities;
  int id = 0; /* Default activity */

  g_array_insert_val (self->by_workspace, index, id);
}

static void
on_workspace_removed (int      index,
                      gpointer activities)
{
  ShellActivities *self = activities;

  g_array_remove_index (self->by_workspace, index);
}
