/*
 * Mail Notification
 * Copyright (C) 2003-2008 Jean-Yves Lefort <jylefort@brutele.be>
 *
 * 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 3 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.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

%headertop{
#include <time.h>
#include "mn-mailbox.h"
%}

%h{
typedef enum
{
  MN_MESSAGE_NEW	= 1 << 0	/* unseen message */
} MNMessageFlags;

typedef struct _MNMessageAction MNMessageAction;

typedef void (*MNMessageActionResultCallback) (MNMessageAction *action, GError *err, gpointer data);

typedef struct
{
  MNMessageAction		*action;
  MNMessage			*message;
  MNMessageActionResultCallback	callback;
  gpointer			data;
} MNMessageActionRequest;

struct _MNMessageAction
{
  const char	*name;
  const char	*icon;
  const char	*label;
  const char	*error_message;

  gboolean (*can_perform) (MNMessage *message);
  void (*perform) (MNMessage *message, MNMessageActionRequest *request);
  void (*done) (MNMessage *message, GError *err);
};

#define MN_MESSAGE_ACTION_ERROR		(mn_message_action_error_quark())

typedef enum
{
  MN_MESSAGE_ACTION_ERROR_OTHER,
  MN_MESSAGE_ACTION_ERROR_CANCELLED
} MNMessageActionError;
%}

%privateheader{
#include "mn-xml.h"

typedef enum
{
  /* include in the XML summary and allow as a command format */
  MN_MESSAGE_PARAM_EXPORT	= MN_XML_PARAM_EXPORT,
} MNMessageParamFlags;

typedef GError *(*MNMessageActionPerformCallback) (MNMessage *message, gpointer data);
%}

%{
#include <errno.h>
#include <glib/gi18n.h>
#include <libgnome/libgnome.h>
#include <libgnomevfs/gnome-vfs.h>
#include "mn-conf.h"
#include "mn-util.h"

typedef struct
{
  MNMessageActionRequest		*request;
  MNMessageActionPerformCallback	callback;
  gpointer				user_data;
} PerformInfo;
%}

%afterdecls{
static const MNMessageAction message_actions[] = {
  {
    "open",
    "mail-open",
    /* translators: header capitalization */
    N_("Open"),
    N_("Unable to open message"),
    self_builtin_can_open,
    self_builtin_open,
    self_open_done
  },
  {
    "mark-as-read",
    "mark",
    /* translators: header capitalization */
    N_("Mark as Read"),
    N_("Unable to mark message as read"),
    self_builtin_can_mark_as_read,
    self_builtin_mark_as_read,
    self_mark_as_read_done
  },
  {
    "mark-as-spam",
    "spam",
    /* translators: header capitalization */
    N_("Mark as Spam"),
    N_("Unable to mark message as spam"),
    self_builtin_can_mark_as_spam,
    self_builtin_mark_as_spam,
    self_mark_as_spam_done
  },
  {
    "delete",
    "delete",
    /* translators: header capitalization */
    N_("Delete"),
    N_("Unable to mark message as spam"),
    self_builtin_can_delete,
    self_builtin_delete,
    self_delete_done
  }
};
%}

class MN:Message from G:Object
{
  /*
   * In order to not create reference cycles, we do not hold a
   * reference to the mailbox. The code is arranged so that a message
   * cannot survive its containing mailbox (whenever the mailbox is
   * removed, subsystems handle the messages-changed signal and
   * dereference the mailbox messages).
   */
  public MNMailbox *mailbox;
  property POINTER mailbox (flags = CONSTRUCT_ONLY, link, type = MNMailbox *);

  /* sent time, may be 0 */
  public time_t sent_time;
  property ULONG sent_time (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT, link, type = time_t);

  /*
   * The application-wise message identifier. It is used by various
   * subsystems to test the equality of two messages (the MNMessage
   * instance pointers cannot be compared since they can change across
   * checks).
   *
   * Uniqueness is highly desired but not required. Nothing
   * catastrophical will happen if an ID clash occurs.
   *
   * This field is never NULL.
   */
  public char *id destroywith g_free;
  property STRING id (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);

  /*
   * The mailbox-wise message identifier. It is used by MNMailbox to
   * cache the message. It is not cached using the application-wise ID
   * because for most backends, retrieving that ID requires to read
   * the message, which obviously defeats the purpose of caching.
   *
   * If set, it should be unique across all the messages of the
   * containing mailbox, but nothing catastrophical will happen if an
   * ID clash occurs.
   *
   * If NULL, the message will not be cached.
   */
  public char *mid destroywith g_free;
  property STRING mid (link, flags = CONSTRUCT_ONLY);

  /* always set */
  public char *from destroywith g_free;
  property STRING from (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);

  /* always set */
  public char *subject destroywith g_free;
  property STRING subject (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);

  /* may be NULL */
  public char *uri destroywith g_free;
  property STRING uri (link, flags = CONSTRUCT_ONLY | MN_MESSAGE_PARAM_EXPORT);

  /* may be NULL */
  property STRING filename (flags = MN_MESSAGE_PARAM_EXPORT)
    get {
      g_value_take_string(VAL, self->uri ? gnome_vfs_get_local_path_from_uri(self->uri) : NULL);
    };

  public MNMessageFlags flags;
  property UINT flags (link, flags = CONSTRUCT_ONLY);

  public MNMessageAction *
    get_action (const char *name (check null))
  {
    static GHashTable *actions = NULL;

    if (! actions)
      {
	int i;

	actions = g_hash_table_new(g_str_hash, g_str_equal);

	for (i = 0; i < G_N_ELEMENTS(message_actions); i++)
	  {
	    const MNMessageAction *action = &message_actions[i];

	    g_hash_table_insert(actions, (gpointer) action->name, (gpointer) action);
	  }
      }

    return g_hash_table_lookup(actions, name);
  }

  constructor (self)
  {
    g_assert(MN_IS_MAILBOX(self->mailbox));

    if (! self->id)
      {
	GString *id;

	/* no ID was provided, try to generate a persistent one */

	id = g_string_new(NULL);

	if (self->sent_time > 0)
	  g_string_append_printf(id, ":sent-time:%i:", (int) self->sent_time);
	if (self->from)
	  g_string_append_printf(id, ":from:%s:", self->from);
	if (self->subject)
	  g_string_append_printf(id, ":subject:%s:", self->subject);

	if (! *id->str)
	  {
	    static int unique = 0;

	    /*
	     * We could not generate a persistent ID. Fallback to a
	     * non-persistent one.
	     */

	    g_string_append_printf(id, "%i", g_atomic_int_exchange_and_add(&unique, 1));
	  }

	self->id = g_string_free(id, FALSE);
      }

    /* these fields must only be filled after we have generated an ID */

    if (! self->from)
      self->from = g_strdup("");
    if (! self->subject)
      self->subject = g_strdup("");
  }

  private gboolean
    subst_command_cb (const char *name, char **value, gpointer data)
  {
    Self *self = data;
    GParamSpec **properties;
    unsigned int n_properties;
    gboolean status = FALSE;
    int i;

    properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(self), &n_properties);
    for (i = 0; i < n_properties; i++)
      if ((properties[i]->flags & MN_MESSAGE_PARAM_EXPORT) != 0
	  && ! strcmp(g_param_spec_get_name(properties[i]), name))
	{
	  GValue gvalue = { 0, };

	  g_value_init(&gvalue, G_PARAM_SPEC_VALUE_TYPE(properties[i]));
	  g_object_get_property(G_OBJECT(self), name, &gvalue);

	  *value = mn_g_value_to_string(&gvalue);
	  g_value_unset(&gvalue);

	  status = TRUE;
	  break;
	}
    g_free(properties);

    return status;
  }

  private gboolean
    execute_command_real (self,
			  const char *command (check null),
			  GError **err)
  {
    char *subst;
    int status;

    subst = mn_subst_command(command, self_subst_command_cb, self, err);
    if (! subst)
      return FALSE;

    status = gnome_execute_shell(NULL, subst);
    g_free(subst);

    if (status < 0)
      {
	g_set_error(err, 0, 0, "%s", g_strerror(errno));
	return FALSE;
      }

    return TRUE;
  }

  /*
   * Returns TRUE if a custom action was found. Sets @err is the
   * execution of the custom action failed.
   */
  private gboolean
    execute_command (self, const char *id (check null), GError **err)
  {
    char *command;
    GError *tmp_err = NULL;

    command = mn_mailbox_get_command(self->mailbox, id);
    if (! command)
      return FALSE;

    if (! self_execute_command_real(self, command, &tmp_err))
      {
	g_set_error(err, 0, 0, _("Unable to execute \"%s\": %s."), command, tmp_err->message);
	g_error_free(tmp_err);
      }

    g_free(command);
    return TRUE;
  }

  public gboolean
    can_perform_action (self, MNMessageAction *action (check null))
  {
    return mn_mailbox_has_command(self->mailbox, action->name)
      || action->can_perform(self);
  }

  public void
    perform_action (self,
		    MNMessageAction *action (check null),
		    MNMessageActionResultCallback callback (check null),
		    gpointer data)
  {
    GError *err = NULL;

    if (self_execute_command(self, action->name, &err))
      self_action_done_real(self, action, err, callback, data);
    else
      {
	MNMessageActionRequest *request;

	request = g_new0(MNMessageActionRequest, 1);
	request->message = g_object_ref(self);
	request->action = action;
	request->callback = callback;
	request->data = data;

	action->perform(self, request);
      }
  }

  protected void
    perform_action_in_thread (MNMessageActionRequest *request (check null),
			      MNMessageActionPerformCallback callback (check null),
			      gpointer user_data)
  {
    PerformInfo *info;

    info = g_new0(PerformInfo, 1);
    info->request = request;
    info->callback = callback;
    info->user_data = user_data;

    g_object_ref(request->message);
    g_object_ref(request->message->mailbox);

    mn_thread_create((GThreadFunc) self_perform_action_in_thread_cb, info);
  }

  private void
    perform_action_in_thread_cb (PerformInfo *info)
  {
    GError *err;

    err = info->callback(info->request->message, info->user_data);

    GDK_THREADS_ENTER();

    self_action_done(info->request, err);

    g_object_unref(info->request->message->mailbox);
    g_object_unref(info->request->message);

    gdk_flush();
    GDK_THREADS_LEAVE();
  }

  private void
    action_done_real (self,
		      MNMessageAction *action (check null),
		      GError *err,
		      MNMessageActionResultCallback callback,
		      gpointer data)
  {
    action->done(self, err);
    callback(action, err, data);
  }

  protected void
    action_done (MNMessageActionRequest *request (check null), GError *err)
  {
    Self *self = request->message;

    self_action_done_real(self, request->action, err, request->callback, request->data);

    g_object_unref(request->message);
    g_free(request);
  }

  public GQuark
    action_error_quark (void)
  {
    return g_quark_from_static_string("mn-message-action-error");
  }

  virtual private gboolean
    builtin_can_open (self)
  {
    return self->uri != NULL;
  }

  virtual private void
    builtin_open (self, MNMessageActionRequest *request)
  {
    GError *err = NULL;

    gnome_url_show(self->uri, &err);

    self_action_done(request, err);
  }

  private void
    open_done (self, GError *err)
  {
    if (! err)
      self_consider_as_read(self); /* [1] */
  }

  virtual private gboolean
    builtin_can_mark_as_read (self)
  {
    return SELF_GET_CLASS(self)->builtin_mark_as_read != NULL;
  }

  virtual private void
    builtin_mark_as_read (self, MNMessageActionRequest *request);

  private void
    mark_as_read_done (self, GError *err)
  {
    if (! err)
      self_consider_as_read(self); /* [1] */
  }

  virtual private gboolean
    builtin_can_mark_as_spam (self)
  {
    return SELF_GET_CLASS(self)->builtin_mark_as_spam != NULL;
  }

  virtual private void
    builtin_mark_as_spam (self, MNMessageActionRequest *request);

  private void
    mark_as_spam_done (self, GError *err)
  {
    if (! err)
      self_consider_as_read(self); /* [1] */
  }

  virtual private gboolean
    builtin_can_delete (self)
  {
    return SELF_GET_CLASS(self)->builtin_delete != NULL;
  }

  virtual private void
    builtin_delete (self, MNMessageActionRequest *request);

  private void
    delete_done (self, GError *err)
  {
    if (! err)
      self_consider_as_read(self); /* [1] */
  }

  public void
    consider_as_read (self)
  {
    GSList *list;
    GSList *l;
    gboolean exists = FALSE;

    list = mn_conf_get_string_list(MN_CONF_MESSAGES_CONSIDERED_AS_READ);

    MN_LIST_FOREACH(l, list)
      {
	const char *id = l->data;

	if (! strcmp(id, self->id))
	  {
	    exists = TRUE;
	    break;
	  }
      }

    if (! exists)
      {
	list = g_slist_prepend(list, g_strdup(self->id));

	mn_conf_set_string_list(MN_CONF_MESSAGES_CONSIDERED_AS_READ, list);
      }

    mn_g_slist_free_deep(list);
  }

  /*
   * Atomically considers a list of messages as read, setting the
   * GConf list only once rather than for each message.
   */
  public void
    consider_as_read_list (GList *messages)
  {
    GHashTable *set;
    unsigned int old_size;
    GList *l;

    set = mn_conf_get_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ);

    old_size = g_hash_table_size(set);

    MN_LIST_FOREACH(l, messages)
      {
	MNMessage *message = l->data;

	g_hash_table_replace(set, g_strdup(message->id), GINT_TO_POINTER(TRUE));
      }

    if (g_hash_table_size(set) != old_size)
      mn_conf_set_string_hash_set(MN_CONF_MESSAGES_CONSIDERED_AS_READ, set);

    g_hash_table_destroy(set);
  }

  public MNMessage *
    new (MN:Mailbox *mailbox (check null type),
	 time_t sent_time,
	 const char *id,
	 const char *mid,
	 const char *from,
	 const char *subject,
	 const char *uri,
	 MNMessageFlags flags)
  {
    return GET_NEW_VARG(MN_MESSAGE_PROP_MAILBOX(mailbox),
			MN_MESSAGE_PROP_SENT_TIME(sent_time),
			MN_MESSAGE_PROP_ID((char *) id),
			MN_MESSAGE_PROP_MID((char *) mid),
			MN_MESSAGE_PROP_FROM((char *) from),
			MN_MESSAGE_PROP_SUBJECT((char *) subject),
			MN_MESSAGE_PROP_URI((char *) uri),
			MN_MESSAGE_PROP_FLAGS(flags),
			NULL);
  }

  public xmlNode *
    xml_node_new (self)
  {
    xmlNode *node;

    node = xmlNewNode(NULL, "message");

    xmlSetProp(node, "mailbox", self->mailbox->runtime_name);

    if ((self->flags & MN_MESSAGE_NEW) != 0)
      xmlSetProp(node, "new", "true");

    mn_xml_export_properties(G_OBJECT(self), node);

    return node;
  }
}

/*
 * [1]: there can be a slight (or large if polling is in effect) delay
 * between executing an action which should cause a message to
 * disappear from MN (open it, mark it as read, etc) and having the
 * next mail check catch the change. By adding the message to the
 * considered-as-read GConf list, this delay is concealed from the
 * user.
 */
