/*
 * 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 "mn-mailbox.h"
%}

%{
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdarg.h>
#include <errno.h>
#include <glib/gi18n.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include "mn-conf.h"
#include "mn-message.h"
#include "mn-mailbox-private.h"
#include "mn-test-mailbox.h"
#include "mn-shell.h"
#include "mn-util.h"
%}

class MN:Mailboxes from G:Object
{
  public GList *list;		/* freed in finalize */

  /* sorted by sent time, most recent first */
  public GPtrArray *messages = {g_ptr_array_new()} destroywith mn_g_object_ptr_array_free;

  /* the references to the messages are held by the GPtrArray */
  public GHashTable *messages_hash_table = {g_hash_table_new(g_str_hash, g_str_equal)} destroywith g_hash_table_destroy;

  private GSList *add_queue destroywith mn_g_object_slist_free;
  private GSList *remove_queue destroywith mn_g_object_slist_free;
  private unsigned int queue_idle_id;

  /*
   * Can be set by mailboxes to indicate that mailboxes.xml must be
   * saved after having been loaded. Used by MNAuthenticatedMailbox to
   * remove the plain text passwords saved by older versions of MN.
   */
  public gboolean must_save_after_load;

  /**
   * mailbox-added:
   * @self: the object which received the signal
   * @mailbox: the mailbox which was added
   *
   * This signal gets emitted after a mailbox is added to the list.
   **/
  signal first private NONE (OBJECT)
    void mailbox_added (self, MN:Mailbox *mailbox (check null type))
  {
    self_connect_mailbox_signals(self, mailbox);

    /* emit the "added" signal on the mailbox */
    mn_mailbox_added(mailbox);
  }

  /**
   * mailbox-removed:
   * @self: the object which received the signal
   * @mailbox: the mailbox which was removed
   *
   * This signal gets emitted after a mailbox is removed from the
   * list.
   **/
  signal first private NONE (OBJECT)
    void mailbox_removed (self, MN:Mailbox *mailbox (check null type))
  {
    self_disconnect_mailbox_signals(self, mailbox);

    /* emit the "removed" signal on the mailbox */
    mn_mailbox_removed(mailbox);

    /* messages and error have possibly changed */
    self_messages_changed(self, FALSE);
    self_error_changed(self);
  }

  //[G_GNUC_UNUSED] /* we use g_signal_emit_by_name(), for passing a detail */
  signal (DETAILED) private NONE (OBJECT, POINTER)
    void mailbox_notify (self,
			 MN:Mailbox *mailbox (check null type),
			 GParamSpec *pspec (check null));

  /**
   * list-changed:
   * @self: the object which received the signal
   *
   * This signal gets emitted after the mailbox list changes (but more
   * than one mailbox may have been added, removed or have changed
   * between two emissions of this signal).
   **/
  signal first private NONE (NONE)
    void list_changed (self)
  {
    /* manually-checkable has possibly changed */
    g_object_notify(G_OBJECT(self), "manually-checkable");
  }

  /**
   * messages-changed:
   * @self: the object which received the signal
   * @has_new: whether a new message has been received or not
   *
   * This signal gets emitted whenever the messages member has
   * potentially changed, either because one of the mailboxes messages
   * property has changed, or because a mailbox has been removed from
   * the list.
   *
   * Note: messages are only compared by id (in
   * mn_mailbox_filter_messages()), therefore two messages having the
   * same id and different data are not considered different.
   **/
  signal first private NONE (BOOLEAN)
    void messages_changed (self, gboolean has_new)
  {
    GList *l;

    mn_g_object_ptr_array_free(self->messages);
    self->messages = g_ptr_array_new();

    g_hash_table_remove_all(self->messages_hash_table);

    MN_LIST_FOREACH(l, self->list)
      {
	MNMailbox *mailbox = l->data;

	g_hash_table_foreach(mailbox->messages, (GHFunc) self_messages_changed_cb, self);
      }

    g_ptr_array_sort(self->messages, (GCompareFunc) self_messages_sort_cb);
  }

  private void
    messages_changed_cb (const char *id,
			 MNMessage *message,
			 Self *self)
  {
    g_ptr_array_add(self->messages, g_object_ref(message));
    g_hash_table_insert(self->messages_hash_table, message->id, message);
  }

  private int
    messages_sort_cb (MNMessage **a, MNMessage **b)
  {
    /* sort by sent time in descending order */
    return (*b)->sent_time - (*a)->sent_time;
  }

  /**
   * error-changed:
   * @self: the object which received the signal
   *
   * This signal gets emitted whenever the global error state has
   * possibly changed, either because one of the mailboxes error
   * property has changed, or because a mailbox has been removed from
   * the list.
   **/
  signal private NONE (NONE)
    void error_changed (self);

  property BOOLEAN manually_checkable (export)
    get
    {
      GList *l;
      gboolean value = FALSE;

      MN_LIST_FOREACH(l, self->list)
	{
	  MNMailbox *mailbox = l->data;

	  if (mn_mailbox_get_manually_checkable(mailbox))
	    {
	      value = TRUE;
	      break;
	    }
	}

      g_value_set_boolean(VAL, value);
    };

  init (self)
  {
    char *filename;
    gboolean exists;

    mn_shell->mailboxes = self;

    filename = g_build_filename(mn_conf_dot_dir, "mailboxes.xml", NULL);
    exists = g_file_test(filename, G_FILE_TEST_EXISTS);
    g_free(filename);

    if (exists)
      self_load(self);
    else if (mn_conf_is_set(MN_CONF_OBSOLETE_MAILBOXES))
      {
	GSList *gconf_mailboxes;
	GSList *l;
	GSList *invalid_uri_list = NULL;
	gboolean list_changed = FALSE;

	gconf_mailboxes = mn_conf_get_string_list(MN_CONF_OBSOLETE_MAILBOXES);
	MN_LIST_FOREACH(l, gconf_mailboxes)
	  {
	    const char *uri = l->data;
	    MNMailbox *mailbox;

	    mailbox = mn_mailbox_new_from_obsolete_uri(uri);
	    if (mailbox)
	      {
		mn_mailbox_seal(mailbox);
		self_add_real(self, mailbox);
		g_object_unref(mailbox);

		list_changed = TRUE;
	      }
	    else
	      invalid_uri_list = g_slist_append(invalid_uri_list, (gpointer) uri);
	  }

	if (list_changed)
	  {
	    self_list_changed(self);
	    self_save(self);	/* save the imported mailboxes */
	  }

	if (invalid_uri_list)
	  {
	    mn_show_invalid_uri_list_dialog(NULL, _("An error has occurred while importing old mailboxes"), invalid_uri_list);
	    g_slist_free(invalid_uri_list);
	  }

	mn_g_slist_free_deep(gconf_mailboxes);
      }
  }

  finalize (self)
  {
    GList *l;

    /*
     * We need to disconnect the mailbox signals because on exit, a
     * mailbox can survive the MNMailboxes object (if a check thread
     * is running).
     */
    MN_LIST_FOREACH(l, self->list)
      self_disconnect_mailbox_signals(self, l->data);

    mn_g_object_list_free(self->list);

    if (selfp->queue_idle_id)
      g_source_remove(selfp->queue_idle_id);
  }

  private void
    connect_mailbox_signals (self, MN:Mailbox *mailbox (check null type))
  {
    g_object_connect(mailbox,
		     "signal::messages-changed", self_mailbox_messages_changed_h, self,
		     "signal::notify", self_mailbox_notify_h, self,
		     "signal::notify::error", self_mailbox_notify_error_h, self,
		     "signal::notify::manually-checkable", self_mailbox_notify_manually_checkable_h, self,
		     NULL);
  }

  private void
    disconnect_mailbox_signals (self, MN:Mailbox *mailbox (check null type))
  {
    g_object_disconnect(mailbox,
			"any-signal", self_mailbox_messages_changed_h, self,
			"any-signal", self_mailbox_notify_h, self,
			"any-signal", self_mailbox_notify_error_h, self,
			"any-signal", self_mailbox_notify_manually_checkable_h, self,
			NULL);
  }

  private void
    load (self)
  {
    GError *err = NULL;

    if (! self_load_real(self, &err))
      {
	mn_show_error_dialog(NULL, _("Unable to load the mailboxes configuration"), "%s", err->message);
	g_error_free(err);
      }

    if (self->must_save_after_load)
      self_save(self);
  }

  private void
    add_error (GString **errors (check null),
	       int *n_errors (check null),
	       const char *format,
	       ...)
    attr {G_GNUC_PRINTF(3, 4)}
  {
    char *message;

    if (*errors)
      g_string_append_c(*errors, '\n');
    else
      *errors = g_string_new(NULL);

    MN_STRDUP_VPRINTF(message, format);
    g_string_append(*errors, message);
    g_free(message);

    (*n_errors)++;
  }

  private gboolean
    load_real (self, GError **err)
  {
    char *filename;
    xmlDoc *doc;
    xmlNode *root;
    xmlNode *node;
    gboolean list_changed = FALSE;
    gboolean status = TRUE;
    GString *errors = NULL;
    int n_errors = 0;

    filename = g_build_filename(mn_conf_dot_dir, "mailboxes.xml", NULL);
    doc = xmlParseFile(filename);
    g_free(filename);

    if (! doc)
      {
	g_set_error(err, 0, 0, _("Unable to parse the XML document."));
	return FALSE;
      }

    root = xmlDocGetRootElement(doc);
    if (! root)
      {
	g_set_error(err, 0, 0, _("The root element is missing."));
	goto error;
      }

    if (strcmp(root->name, "mailboxes"))
      {
	g_set_error(err, 0, 0, _("The root element \"%s\" is invalid."), root->name);
	goto error;
      }

    for (node = root->children; node; node = node->next)
      if (node->type == XML_ELEMENT_NODE)
	{
	  if (! strcmp(node->name, "mailbox"))
	    {
	      MNMailbox *mailbox;
	      GError *tmp_err = NULL;

	      mailbox = mn_mailbox_new_from_xml_node(node, &tmp_err);
	      if (mailbox)
		{
		  mn_mailbox_seal(mailbox);
		  self_add_real(self, mailbox);
		  g_object_unref(mailbox);
		  list_changed = TRUE;
		}
	      else
		{
		  self_add_error(&errors, &n_errors, _("On line %i: %s."), node->line, tmp_err->message);
		  g_error_free(tmp_err);
		}
	    }
	  else
	    self_add_error(&errors, &n_errors, _("On line %i: unknown element \"%s\"."), node->line, node->name);
	}

    if (list_changed)
      self_list_changed(self);

    if (errors)
      {
	mn_show_error_dialog(NULL,
			     ngettext("An error has occurred while loading the mailboxes configuration",
				      "Errors have occurred while loading the mailboxes configuration",
				      n_errors),
			     "%s", errors->str);
	g_string_free(errors, TRUE);
      }

    goto end;

  error:
    status = FALSE;

  end:
    xmlFreeDoc(doc);

    return status;
  }

  private void
    save (self)
  {
    GError *err = NULL;

    if (! self_save_real(self, &err))
      {
	mn_show_error_dialog(NULL, _("Unable to save the mailboxes configuration"), "%s", err->message);
	g_error_free(err);
      }
  }

  private gboolean
    save_real (self, GError **err)
  {
    int indent;
    xmlDoc *doc;
    xmlNode *root;
    GList *l;
    char *filename;
    char *tmp_filename;
    char *old_filename;
    int fd = -1;
    FILE *f = NULL;
    gboolean old_exists;
    gboolean status = TRUE;

    indent = xmlIndentTreeOutput;
    xmlIndentTreeOutput = 1;

    doc = xmlNewDoc("1.0");
    root = xmlNewNode(NULL, "mailboxes");
    xmlDocSetRootElement(doc, root);

    MN_LIST_FOREACH(l, self->list)
      {
	MNMailbox *mailbox = l->data;
	xmlNode *node;

	if (! MN_IS_TEST_MAILBOX(mailbox))
	  {
	    node = mn_mailbox_xml_node_new(mailbox);
	    xmlAddChild(root, node); /* owns node */
	  }
      }

    filename = g_build_filename(mn_conf_dot_dir, "mailboxes.xml", NULL);
    tmp_filename = g_strconcat(filename, ".tmp", NULL);
    old_filename = g_strconcat(filename, ".old", NULL);

    if (g_file_test(tmp_filename, G_FILE_TEST_EXISTS) && unlink(tmp_filename) < 0)
      {
	g_set_error(err, 0, 0, _("Unable to remove %s: %s."), tmp_filename, g_strerror(errno));
	goto error;
      }

    /* the file may contain passwords; restrict permissions (600) */
    fd = open(tmp_filename, O_WRONLY | O_CREAT | O_EXCL, S_IRUSR | S_IWUSR);
    if (fd < 0)
      {
	g_set_error(err, 0, 0, _("Unable to create %s: %s."), tmp_filename, g_strerror(errno));
	goto error;
      }

    f = fdopen(fd, "w");
    if (! f)
      {
	g_set_error(err, 0, 0, _("Unable to open %s for writing: %s."), tmp_filename, g_strerror(errno));
	goto error;
      }
    fd = -1;			/* now owned by f */

    if (xmlDocFormatDump(f, doc, 1) < 0)
      {
	g_set_error(err, 0, 0, _("Unable to write the XML document."));
	goto error;
      }

    if (fclose(f) != 0)
      {
	g_set_error(err, 0, 0, _("Unable to close %s: %s."), tmp_filename, g_strerror(errno));
	goto error;
      }
    f = NULL;

    old_exists = g_file_test(filename, G_FILE_TEST_EXISTS);
    if (old_exists)
      {
	if (rename(filename, old_filename) < 0)
	  {
	    g_set_error(err, 0, 0, _("Unable to rename %s to %s: %s."), filename, old_filename, g_strerror(errno));
	    goto error;
	  }
      }

    if (rename(tmp_filename, filename) < 0)
      {
	g_set_error(err, 0, 0, _("Unable to rename %s to %s: %s."), tmp_filename, filename, g_strerror(errno));
	goto error;
      }

    if (old_exists)
      if (unlink(old_filename) < 0) /* non fatal */
	g_warning(_("unable to delete %s: %s"), old_filename, g_strerror(errno));

    goto end;			/* success */

  error:
    status = FALSE;

  end:
    xmlFreeDoc(doc);
    xmlIndentTreeOutput = indent;

    g_free(filename);
    g_free(tmp_filename);
    g_free(old_filename);

    if (fd >= 0)
      close(fd);
    if (f)
      fclose(f);

    return status;
  }

  private void
    mailbox_messages_changed_h (MNMailbox *mailbox,
				gboolean has_new,
				gpointer user_data)
  {
    Self *self = user_data;
    int num_messages;

    num_messages = g_hash_table_size(mailbox->messages);

    mn_info(ngettext("%s has %i new message", "%s has %i new messages", num_messages),
	    mailbox->runtime_name, num_messages);

    self_messages_changed(self, has_new);
  }

  private void
    mailbox_notify_h (GObject *object, GParamSpec *pspec, gpointer user_data)
  {
    Self *self = user_data;
    char *detailed_signal;

    detailed_signal = g_strconcat("mailbox-notify::", g_param_spec_get_name(pspec), NULL);
    g_signal_emit_by_name(self, detailed_signal, object, pspec);
    g_free(detailed_signal);
  }

  private void
    mailbox_notify_error_h (GObject *object,
			    GParamSpec *pspec,
			    gpointer user_data)
  {
    Self *self = user_data;
    MNMailbox *mailbox = MN_MAILBOX(object);

    if (mailbox->error)
      mn_info(_("%s reported an error: %s"), mailbox->runtime_name, mailbox->error);

    self_error_changed(self);
  }

  private void
    mailbox_notify_manually_checkable_h (GObject *object,
					 GParamSpec *pspec,
					 gpointer user_data)
  {
    Self *self = user_data;

    /* manually-checkable has possibly changed */
    g_object_notify(G_OBJECT(self), "manually-checkable");
  }

  public void
    check (self)
  {
    GList *l;

    MN_LIST_FOREACH(l, self->list)
      {
	MNMailbox *mailbox = l->data;

	if (mn_mailbox_get_manually_checkable(mailbox))
	  mn_mailbox_check(mailbox);
      }
  }

  private void
    add_real (self, MN:Mailbox *mailbox (check null type))
  {
    g_object_ref(mailbox);
    self->list = g_list_insert_sorted(self->list, mailbox, self_compare_by_name_func);
    self_mailbox_added(self, mailbox);
  }

  public void
    add (self, MN:Mailbox *mailbox (check null type))
  {
    self_add_real(self, mailbox);
    self_list_changed(self);

    if (! MN_IS_TEST_MAILBOX(mailbox))
      self_save(self);
  }

  public void
    queue_add (self, MN:Mailbox *mailbox (check null type))
  {
    g_object_ref(mailbox);
    selfp->add_queue = g_slist_append(selfp->add_queue, mailbox);

    if (! selfp->queue_idle_id)
      selfp->queue_idle_id = gdk_threads_add_idle(self_queue_idle_cb, self);
  }

  private void
    remove_real (self, MN:Mailbox *mailbox (check null type))
  {
    self->list = g_list_remove(self->list, mailbox);
    self_mailbox_removed(self, mailbox);
    g_object_unref(mailbox);
  }

  public void
    remove (self, MN:Mailbox *mailbox (check null type))
  {
    self_remove_real(self, mailbox);
    self_list_changed(self);
    if (! MN_IS_TEST_MAILBOX(mailbox))
      self_save(self);
  }

  public void
    queue_remove (self, MN:Mailbox *mailbox (check null type))
  {
    g_object_ref(mailbox);
    selfp->remove_queue = g_slist_append(selfp->remove_queue, mailbox);

    if (! selfp->queue_idle_id)
      selfp->queue_idle_id = gdk_threads_add_idle(self_queue_idle_cb, self);
  }

  private gboolean
    queue_idle_cb (gpointer data)
  {
    Self *self = data;
    GSList *l;

    MN_LIST_FOREACH(l, selfp->add_queue)
      self_add_real(self, l->data);

    mn_g_object_slist_clear(&selfp->add_queue);

    MN_LIST_FOREACH(l, selfp->remove_queue)
      self_remove_real(self, l->data);

    mn_g_object_slist_clear(&selfp->remove_queue);

    self_list_changed(self);
    self_save(self);

    selfp->queue_idle_id = 0;
    return FALSE;		/* remove */
  }

  public int
    compare_by_name_func (gconstpointer a, gconstpointer b)
  {
    MNMailbox *mailbox_a = (MNMailbox *) a;
    MNMailbox *mailbox_b = (MNMailbox *) b;

    return g_utf8_collate(mailbox_a->runtime_name, mailbox_b->runtime_name);
  }

  public MNMailboxes *
    new (void)
  {
    return GET_NEW;
  }
}
