/*
 * Copyright (C) 2007-2008 Guillaume Desmottes
 *
 * This library 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 2.1 of the License, 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 Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors: Guillaume Desmottes <gdesmott@gnome.org>
 */

#include <config.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

#include "empathy-utils.h"
#include "empathy-irc-network-manager.h"

#define DEBUG_FLAG EMPATHY_DEBUG_IRC
#include "empathy-debug.h"

#define IRC_NETWORKS_DTD_FILENAME "empathy-irc-networks.dtd"
#define SAVE_TIMER 4

#define GET_PRIV(obj) EMPATHY_GET_PRIV (obj, EmpathyIrcNetworkManager)
typedef struct {
  GHashTable *networks;

  gchar *global_file;
  gchar *user_file;
  guint last_id;

  /* Do we have to save modifications to the user file ? */
  gboolean have_to_save;
  /* Are we loading networks from XML files ? */
  gboolean loading;
  /* source id of the autosave timer */
  gint save_timer_id;
} EmpathyIrcNetworkManagerPriv;

/* properties */
enum
{
  PROP_GLOBAL_FILE = 1,
  PROP_USER_FILE,
  LAST_PROPERTY
};

G_DEFINE_TYPE (EmpathyIrcNetworkManager, empathy_irc_network_manager,
    G_TYPE_OBJECT);

static void irc_network_manager_load_servers (
    EmpathyIrcNetworkManager *manager);
static gboolean irc_network_manager_file_parse (
    EmpathyIrcNetworkManager *manager, const gchar *filename,
    gboolean user_defined);
static gboolean irc_network_manager_file_save (
    EmpathyIrcNetworkManager *manager);

static void
empathy_irc_network_manager_get_property (GObject *object,
                                          guint property_id,
                                          GValue *value,
                                          GParamSpec *pspec)
{
  EmpathyIrcNetworkManager *self = EMPATHY_IRC_NETWORK_MANAGER (object);
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  switch (property_id)
    {
      case PROP_GLOBAL_FILE:
        g_value_set_string (value, priv->global_file);
        break;
      case PROP_USER_FILE:
        g_value_set_string (value, priv->user_file);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}

static void
empathy_irc_network_manager_set_property (GObject *object,
                                          guint property_id,
                                          const GValue *value,
                                          GParamSpec *pspec)
{
  EmpathyIrcNetworkManager *self = EMPATHY_IRC_NETWORK_MANAGER (object);
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  switch (property_id)
    {
      case PROP_GLOBAL_FILE:
        g_free (priv->global_file);
        priv->global_file = g_value_dup_string (value);
        break;
      case PROP_USER_FILE:
        g_free (priv->user_file);
        priv->user_file = g_value_dup_string (value);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}

static GObject *
empathy_irc_network_manager_constructor (GType type,
                                         guint n_props,
                                         GObjectConstructParam *props)
{
  GObject *obj;
  EmpathyIrcNetworkManager *self;

  /* Parent constructor chain */
  obj = G_OBJECT_CLASS (empathy_irc_network_manager_parent_class)->
        constructor (type, n_props, props);

  self = EMPATHY_IRC_NETWORK_MANAGER (obj);
  irc_network_manager_load_servers (self);

  return obj;
}

static void
empathy_irc_network_manager_finalize (GObject *object)
{
  EmpathyIrcNetworkManager *self = EMPATHY_IRC_NETWORK_MANAGER (object);
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  if (priv->save_timer_id > 0)
    {
      g_source_remove (priv->save_timer_id);
    }

  if (priv->have_to_save)
    {
      irc_network_manager_file_save (self);
    }

  g_free (priv->global_file);
  g_free (priv->user_file);

  g_hash_table_destroy (priv->networks);

  G_OBJECT_CLASS (empathy_irc_network_manager_parent_class)->finalize (object);
}

static void
empathy_irc_network_manager_init (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
      EMPATHY_TYPE_IRC_NETWORK_MANAGER, EmpathyIrcNetworkManagerPriv);

  self->priv = priv;

  priv->networks = g_hash_table_new_full (g_str_hash, g_str_equal,
      (GDestroyNotify) g_free, (GDestroyNotify) g_object_unref);

  priv->last_id = 0;

  priv->have_to_save = FALSE;
  priv->loading = FALSE;
  priv->save_timer_id = 0;
}

static void
empathy_irc_network_manager_class_init (EmpathyIrcNetworkManagerClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GParamSpec *param_spec;

  object_class->constructor = empathy_irc_network_manager_constructor;
  object_class->get_property = empathy_irc_network_manager_get_property;
  object_class->set_property = empathy_irc_network_manager_set_property;

  g_type_class_add_private (object_class, sizeof (EmpathyIrcNetworkManagerPriv));

  object_class->finalize = empathy_irc_network_manager_finalize;

  param_spec = g_param_spec_string (
      "global-file",
      "path of the global networks file",
      "The path of the system-wide filename from which we have to load"
      " the networks list",
      NULL,
      G_PARAM_CONSTRUCT_ONLY |
      G_PARAM_READWRITE |
      G_PARAM_STATIC_NAME |
      G_PARAM_STATIC_NICK |
      G_PARAM_STATIC_BLURB);
  g_object_class_install_property (object_class, PROP_GLOBAL_FILE, param_spec);

  param_spec = g_param_spec_string (
      "user-file",
      "path of the user networks file",
      "The path of user's  filename from which we have to load"
      " the networks list and to which we'll save his modifications",
      NULL,
      G_PARAM_CONSTRUCT_ONLY |
      G_PARAM_READWRITE |
      G_PARAM_STATIC_NAME |
      G_PARAM_STATIC_NICK |
      G_PARAM_STATIC_BLURB);
  g_object_class_install_property (object_class, PROP_USER_FILE, param_spec);
}

/**
 * empathy_irc_network_manager_new:
 * @global_file: the path of the global networks file, or %NULL
 * @user_file: the path of the user networks file, or %NULL
 *
 * Creates a new #EmpathyIrcNetworkManager
 *
 * Returns: a new #EmpathyIrcNetworkManager
 */
EmpathyIrcNetworkManager *
empathy_irc_network_manager_new (const gchar *global_file,
                                 const gchar *user_file)
{
  EmpathyIrcNetworkManager *manager;

  manager = g_object_new (EMPATHY_TYPE_IRC_NETWORK_MANAGER,
      "global-file", global_file,
      "user-file", user_file,
      NULL);

  return manager;
}

static gboolean
save_timeout (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  priv->save_timer_id = 0;
  irc_network_manager_file_save (self);

  return FALSE;
}

static void
reset_save_timeout (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  if (priv->save_timer_id > 0)
    {
      g_source_remove (priv->save_timer_id);
    }

  priv->save_timer_id = g_timeout_add_seconds (SAVE_TIMER,
      (GSourceFunc) save_timeout, self);
}

static void
network_modified (EmpathyIrcNetwork *network,
                  EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  network->user_defined = TRUE;

  if (!priv->loading)
    {
      priv->have_to_save = TRUE;
      reset_save_timeout (self);
    }
}

static void
add_network (EmpathyIrcNetworkManager *self,
             EmpathyIrcNetwork *network,
             const gchar *id)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  g_hash_table_insert (priv->networks, g_strdup (id), g_object_ref (network));

  g_signal_connect (network, "modified", G_CALLBACK (network_modified), self);
}

/**
 * empathy_irc_network_manager_add:
 * @manager: an #EmpathyIrcNetworkManager
 * @network: the #EmpathyIrcNetwork to add
 *
 * Add an #EmpathyIrcNetwork to the given #EmpathyIrcNetworkManager.
 *
 */
void
empathy_irc_network_manager_add (EmpathyIrcNetworkManager *self,
                                 EmpathyIrcNetwork *network)
{
  EmpathyIrcNetworkManagerPriv *priv;
  gchar *id = NULL;

  g_return_if_fail (EMPATHY_IS_IRC_NETWORK_MANAGER (self));
  g_return_if_fail (EMPATHY_IS_IRC_NETWORK (network));

  priv = GET_PRIV (self);

  /* generate an id for this network */
  do
    {
      g_free (id);
      id = g_strdup_printf ("id%u", ++priv->last_id);
    } while (g_hash_table_lookup (priv->networks, id) != NULL &&
        priv->last_id < G_MAXUINT);

  if (priv->last_id == G_MAXUINT)
    {
      DEBUG ("Can't add network: too many networks using a similar ID");
      return;
    }

  DEBUG ("add server with \"%s\" as ID", id);

  network->user_defined = TRUE;
  add_network (self, network, id);

  priv->have_to_save = TRUE;
  reset_save_timeout (self);

  g_free (id);
}

/**
 * empathy_irc_network_manager_remove:
 * @manager: an #EmpathyIrcNetworkManager
 * @network: the #EmpathyIrcNetwork to remove
 *
 * Remove an #EmpathyIrcNetwork from the given #EmpathyIrcNetworkManager.
 *
 */
void
empathy_irc_network_manager_remove (EmpathyIrcNetworkManager *self,
                                    EmpathyIrcNetwork *network)
{
  EmpathyIrcNetworkManagerPriv *priv;

  g_return_if_fail (EMPATHY_IS_IRC_NETWORK_MANAGER (self));
  g_return_if_fail (EMPATHY_IS_IRC_NETWORK (network));

  priv = GET_PRIV (self);

  network->user_defined = TRUE;
  network->dropped = TRUE;

  priv->have_to_save = TRUE;
  reset_save_timeout (self);
}

static void
append_network_to_list (const gchar *id,
                        EmpathyIrcNetwork *network,
                        GSList **list)
{
  if (network->dropped)
    return;

  *list = g_slist_prepend (*list, g_object_ref (network));
}

/**
 * empathy_irc_network_manager_get_networks:
 * @manager: an #EmpathyIrcNetworkManager
 *
 * Get the list of #EmpathyIrcNetwork associated with the given
 * manager.
 *
 * Returns: a new #GSList of refed #EmpathyIrcNetwork
 */
GSList *
empathy_irc_network_manager_get_networks (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv;
  GSList *irc_networks = NULL;

  g_return_val_if_fail (EMPATHY_IS_IRC_NETWORK_MANAGER (self), NULL);

  priv = GET_PRIV (self);

  g_hash_table_foreach (priv->networks, (GHFunc) append_network_to_list,
      &irc_networks);

  return irc_networks;
}

/*
 * API to save/load and parse the irc_networks file.
 */

static void
load_global_file (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  if (priv->global_file == NULL)
    return;

  if (!g_file_test (priv->global_file, G_FILE_TEST_EXISTS))
    {
      DEBUG ("Global networks file %s doesn't exist", priv->global_file);
      return;
    }

  irc_network_manager_file_parse (self, priv->global_file, FALSE);
}

static void
load_user_file (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  if (priv->user_file == NULL)
    return;

  if (!g_file_test (priv->user_file, G_FILE_TEST_EXISTS))
    {
      DEBUG ("User networks file %s doesn't exist", priv->global_file);
      return;
    }

  irc_network_manager_file_parse (self, priv->user_file, TRUE);
}

static void
irc_network_manager_load_servers (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);

  priv->loading = TRUE;

  load_global_file (self);
  load_user_file (self);

  priv->loading = FALSE;
  priv->have_to_save = FALSE;
}

static void
irc_network_manager_parse_irc_server (EmpathyIrcNetwork *network,
                                      xmlNodePtr node)
{
  xmlNodePtr server_node;

  for (server_node = node->children; server_node;
      server_node = server_node->next)
    {
      gchar *address = NULL, *port = NULL, *ssl = NULL;

      if (strcmp ((const gchar *) server_node->name, "server") != 0)
        continue;

      address = (gchar *) xmlGetProp (server_node, (const xmlChar *) "address");
      port = (gchar *) xmlGetProp (server_node, (const xmlChar *) "port");
      ssl = (gchar *) xmlGetProp (server_node, (const xmlChar *) "ssl");

      if (address != NULL)
        {
          gint port_nb = 0;
          gboolean have_ssl = FALSE;
          EmpathyIrcServer *server;

          if (port != NULL)
            port_nb = strtol (port, NULL, 10);

          if (port_nb <= 0 || port_nb > G_MAXUINT16)
            port_nb = 6667;

          if (ssl == NULL || strcmp (ssl, "TRUE") == 0)
            have_ssl = TRUE;

          DEBUG ("parsed server %s port %d ssl %d", address, port_nb, have_ssl);

          server = empathy_irc_server_new (address, port_nb, have_ssl);
          empathy_irc_network_append_server (network, server);
        }

      if (address)
        xmlFree (address);
      if (port)
        xmlFree (port);
      if (ssl)
        xmlFree (ssl);
    }
}

static void
irc_network_manager_parse_irc_network (EmpathyIrcNetworkManager *self,
                                       xmlNodePtr node,
                                       gboolean user_defined)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);
  EmpathyIrcNetwork  *network;
  xmlNodePtr child;
  gchar *str;
  gchar *id, *name;

  id = (gchar *) xmlGetProp (node, (const xmlChar *) "id");
  if (xmlHasProp (node, (const xmlChar *) "dropped"))
    {
      if (!user_defined)
        {
          DEBUG ("the 'dropped' attribute shouldn't be used in the global file");
        }

      network = g_hash_table_lookup (priv->networks, id);
      if (network != NULL)
        {
          network->dropped = TRUE;
          network->user_defined = TRUE;
        }
       xmlFree (id);
      return;
    }

  if (!xmlHasProp (node, (const xmlChar *) "name"))
    return;

  name = (gchar *) xmlGetProp (node, (const xmlChar *) "name");
  network = empathy_irc_network_new (name);

  if (xmlHasProp (node, (const xmlChar *) "network_charset"))
    {
      gchar *charset;
      charset = (gchar *) xmlGetProp (node, (const xmlChar *) "network_charset");
      g_object_set (network, "charset", charset, NULL);
      xmlFree (charset);
    }

  add_network (self, network, id);
  DEBUG ("add network %s (id %s)", name, id);

  for (child = node->children; child; child = child->next)
    {
      gchar *tag;

      tag = (gchar *) child->name;
      str = (gchar *) xmlNodeGetContent (child);

      if (!str)
        continue;

      if (strcmp (tag, "servers") == 0)
        {
          irc_network_manager_parse_irc_server (network, child);
        }

      xmlFree (str);
    }

  network->user_defined = user_defined;
  g_object_unref (network);
  xmlFree (name);
  xmlFree (id);
}

static gboolean
irc_network_manager_file_parse (EmpathyIrcNetworkManager *self,
                                const gchar *filename,
                                gboolean user_defined)
{
  EmpathyIrcNetworkManagerPriv *priv;
  xmlParserCtxtPtr ctxt;
  xmlDocPtr doc;
  xmlNodePtr networks;
  xmlNodePtr node;

  priv = GET_PRIV (self);

  DEBUG ("Attempting to parse file:'%s'...", filename);

  ctxt = xmlNewParserCtxt ();

  /* Parse and validate the file. */
  doc = xmlCtxtReadFile (ctxt, filename, NULL, 0);
  if (!doc)
    {
      g_warning ("Failed to parse file:'%s'", filename);
      xmlFreeParserCtxt (ctxt);
      return FALSE;
    }

  if (!empathy_xml_validate (doc, IRC_NETWORKS_DTD_FILENAME)) {
    g_warning ("Failed to validate file:'%s'", filename);
    xmlFreeDoc (doc);
    xmlFreeParserCtxt (ctxt);
    return FALSE;
  }

  /* The root node, networks. */
  networks = xmlDocGetRootElement (doc);

  for (node = networks->children; node; node = node->next)
    {
      irc_network_manager_parse_irc_network (self, node, user_defined);
    }

  xmlFreeDoc (doc);
  xmlFreeParserCtxt (ctxt);

  return TRUE;
}

static void
write_network_to_xml (const gchar *id,
                      EmpathyIrcNetwork *network,
                      xmlNodePtr root)
{
  xmlNodePtr network_node, servers_node;
  GSList *servers, *l;
  gchar *name, *charset;

  if (!network->user_defined)
    /* no need to write this network to the XML */
    return;

  network_node = xmlNewChild (root, NULL, (const xmlChar *) "network", NULL);
  xmlNewProp (network_node, (const xmlChar *) "id", (const xmlChar *) id);

  if (network->dropped)
    {
      xmlNewProp (network_node, (const xmlChar *) "dropped",
          (const xmlChar *)  "1");
      return;
    }

  g_object_get (network,
      "name", &name,
      "charset", &charset,
      NULL);
  xmlNewProp (network_node, (const xmlChar *) "name", (const xmlChar *) name);
  xmlNewProp (network_node, (const xmlChar *) "network_charset",
      (const xmlChar *) charset);
  g_free (name);
  g_free (charset);

  servers = empathy_irc_network_get_servers (network);

  servers_node = xmlNewChild (network_node, NULL, (const xmlChar *) "servers",
      NULL);
  for (l = servers; l != NULL; l = g_slist_next (l))
    {
      EmpathyIrcServer *server;
      xmlNodePtr server_node;
      gchar *address, *tmp;
      guint port;
      gboolean ssl;

      server = l->data;

      server_node = xmlNewChild (servers_node, NULL, (const xmlChar *) "server",
          NULL);

      g_object_get (server,
          "address", &address,
          "port", &port,
          "ssl", &ssl,
          NULL);

      xmlNewProp (server_node, (const xmlChar *) "address",
          (const xmlChar *) address);

      tmp = g_strdup_printf ("%u", port);
      xmlNewProp (server_node, (const xmlChar *) "port",
          (const xmlChar *) tmp);
      g_free (tmp);

      xmlNewProp (server_node, (const xmlChar *) "ssl",
          ssl ? (const xmlChar *) "TRUE": (const xmlChar *) "FALSE");

      g_free (address);
    }

  /* free the list */
  g_slist_foreach (servers, (GFunc) g_object_unref, NULL);
  g_slist_free (servers);
}

static gboolean
irc_network_manager_file_save (EmpathyIrcNetworkManager *self)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);
  xmlDocPtr doc;
  xmlNodePtr root;

  if (priv->user_file == NULL)
    {
      DEBUG ("can't save: no user file defined");
      return FALSE;
    }

  DEBUG ("Saving IRC networks");

  doc = xmlNewDoc ((const xmlChar *)  "1.0");
  root = xmlNewNode (NULL, (const xmlChar *) "networks");
  xmlDocSetRootElement (doc, root);

  g_hash_table_foreach (priv->networks, (GHFunc) write_network_to_xml, root);

  /* Make sure the XML is indented properly */
  xmlIndentTreeOutput = 1;

  xmlSaveFormatFileEnc (priv->user_file, doc, "utf-8", 1);
  xmlFreeDoc (doc);

  xmlMemoryDump ();

  priv->have_to_save = FALSE;

  return TRUE;
}

static gboolean
find_network_by_address (const gchar *id,
                         EmpathyIrcNetwork *network,
                         const gchar *address)
{
  GSList *servers, *l;
  gboolean found = FALSE;

  if (network->dropped)
    return FALSE;

  servers = empathy_irc_network_get_servers (network);

  for (l = servers; l != NULL && !found; l = g_slist_next (l))
    {
      EmpathyIrcServer *server = l->data;
      gchar *_address;

      g_object_get (server, "address", &_address, NULL);
      found = (_address != NULL && strcmp (address, _address) == 0);

      g_free (_address);
    }

  g_slist_foreach (servers, (GFunc) g_object_unref, NULL);
  g_slist_free (servers);

  return found;
}

/**
 * empathy_irc_network_manager_find_network_by_address:
 * @manager: an #EmpathyIrcNetworkManager
 * @address: the server address to look for
 *
 * Find the #EmpathyIrcNetwork which owns an #EmpathyIrcServer
 * that has the given address.
 *
 * Returns: the found #EmpathyIrcNetwork, or %NULL if not found.
 */
EmpathyIrcNetwork *
empathy_irc_network_manager_find_network_by_address (
    EmpathyIrcNetworkManager *self,
    const gchar *address)
{
  EmpathyIrcNetworkManagerPriv *priv = GET_PRIV (self);
  EmpathyIrcNetwork *network;

  g_return_val_if_fail (address != NULL, NULL);

  network = g_hash_table_find (priv->networks,
      (GHRFunc) find_network_by_address, (gchar *) address);

  return network;
}
