/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Copyright © 2002-2004 Marco Pesenti Gritti
 *  Copyright © 2003, 2004 Christian Persch
 *
 *  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 2, 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"
#include "ephy-bookmarks.h"

#include "ephy-bookmark-properties.h"
#include "ephy-bookmarks-export.h"
#include "ephy-bookmarks-import.h"
#include "ephy-bookmarks-type-builtins.h"
#include "ephy-debug.h"
#include "ephy-embed-shell.h"
#include "ephy-file-helpers.h"
#include "ephy-history-service.h"
#include "ephy-node-common.h"
#include "ephy-prefs.h"
#include "ephy-profile-utils.h"
#include "ephy-settings.h"
#include "ephy-shell.h"
#include "ephy-signal-accumulator.h"
#include "ephy-tree-model-node.h"

#include <avahi-common/error.h>
#include <avahi-gobject/ga-client.h>
#include <avahi-gobject/ga-enums.h>
#include <avahi-gobject/ga-service-browser.h>
#include <avahi-gobject/ga-service-resolver.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <string.h>

#define EPHY_BOOKMARKS_XML_ROOT    "ephy_bookmarks"
#define EPHY_BOOKMARKS_XML_VERSION "1.03"
#define BOOKMARKS_SAVE_DELAY 3 /* seconds */
#define UPDATE_URI_DATA_KEY "updated-uri"

static const char zeroconf_protos[3][6] =
{
  "http",
  "https",
  "ftp"
};

struct _EphyBookmarks {
  GObject parent_instance;

  gboolean init_defaults;
  gboolean dirty;
  guint save_timeout_id;
  char *xml_file;
  char *rdf_file;
  EphyNodeDb *db;
  EphyNode *bookmarks;
  EphyNode *keywords;
  EphyNode *notcategorized;
  EphyNode *smartbookmarks;
  EphyNode *lower_fav;
  double lower_score;

  /* Local sites */
  EphyNode *local;
  GaClient *ga_client;
  GaServiceBrowser *browse_handles[G_N_ELEMENTS (zeroconf_protos)];
  GHashTable *resolve_handles;
};

static const char *default_topics [] =
{
  N_("Entertainment"),
  N_("News"),
  N_("Shopping"),
  N_("Sports"),
  N_("Travel"),
  N_("Work")
};

/* Signals */
enum {
  TREE_CHANGED,
  LAST_SIGNAL
};

static guint ephy_bookmarks_signals[LAST_SIGNAL];

static void ephy_bookmarks_finalize (GObject *object);
static void ephy_local_bookmarks_start_client (EphyBookmarks *bookmarks);

G_DEFINE_TYPE (EphyBookmarks, ephy_bookmarks, G_TYPE_OBJECT)

static void
ephy_bookmarks_init_defaults (EphyBookmarks *eb)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (default_topics); i++) {
    ephy_bookmarks_add_keyword (eb, _(default_topics[i]));
  }

  ephy_bookmarks_import_rdf (eb, DATADIR "/default-bookmarks.rdf");
}

static void
ephy_bookmarks_class_init (EphyBookmarksClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = ephy_bookmarks_finalize;

  ephy_bookmarks_signals[TREE_CHANGED] =
    g_signal_new ("tree-changed",
                  G_OBJECT_CLASS_TYPE (object_class),
                  G_SIGNAL_RUN_LAST,
                  0, NULL, NULL, NULL,
                  G_TYPE_NONE,
                  0);
}

static gboolean
save_filter (EphyNode      *node,
             EphyBookmarks *bookmarks)
{
  return node != bookmarks->bookmarks &&
         node != bookmarks->notcategorized &&
         node != bookmarks->local;
}

static gboolean
save_filter_local (EphyNode      *node,
                   EphyBookmarks *bookmarks)
{
  return !ephy_node_has_child (bookmarks->local, node);
}

static void
ephy_bookmarks_save (EphyBookmarks *eb)
{
  LOG ("Saving bookmarks");

  ephy_node_db_write_to_xml_safe
    (eb->db,
    (xmlChar *)eb->xml_file,
    (xmlChar *)EPHY_BOOKMARKS_XML_ROOT,
    (xmlChar *)EPHY_BOOKMARKS_XML_VERSION,
    (xmlChar *)"Do not rely on this file, it's only for internal use. Use bookmarks.rdf instead.",
    eb->keywords, (EphyNodeFilterFunc)save_filter, eb,
    eb->bookmarks, (EphyNodeFilterFunc)save_filter_local, eb,
    NULL);

  /* Export bookmarks in rdf */
  ephy_bookmarks_export_rdf (eb, eb->rdf_file);
}

static gboolean
save_bookmarks_delayed (EphyBookmarks *bookmarks)
{
  ephy_bookmarks_save (bookmarks);
  bookmarks->dirty = FALSE;
  bookmarks->save_timeout_id = 0;

  return FALSE;
}

static void
ephy_bookmarks_save_delayed (EphyBookmarks *bookmarks, int delay)
{
  if (!bookmarks->dirty) {
    bookmarks->dirty = TRUE;

    if (delay > 0) {
      bookmarks->save_timeout_id =
        g_timeout_add_seconds (BOOKMARKS_SAVE_DELAY,
                               (GSourceFunc)save_bookmarks_delayed,
                               bookmarks);
      g_source_set_name_by_id (bookmarks->save_timeout_id, "[epiphany] save_bookmarks_delayed");
    } else {
      bookmarks->save_timeout_id =
        g_idle_add ((GSourceFunc)save_bookmarks_delayed,
                    bookmarks);
    }
  }
}

static void
icon_updated_cb (WebKitFaviconDatabase *favicon_database,
                 const char            *address,
                 const char            *icon,
                 EphyBookmarks         *eb)
{
  ephy_bookmarks_set_icon (eb, address, icon);
}

static void
ephy_setup_history_notifiers (EphyBookmarks *eb)
{
  WebKitFaviconDatabase *favicon_database;
  EphyEmbedShell *shell = ephy_embed_shell_get_default ();

  favicon_database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (shell));
  g_signal_connect (favicon_database, "favicon-changed",
                    G_CALLBACK (icon_updated_cb), eb);
}

static void
update_bookmark_keywords (EphyBookmarks *eb, EphyNode *bookmark)
{
  GPtrArray *children;
  guint i;
  GString *list;
  const char *title;
  char *normalized_keywords, *case_normalized_keywords;

  list = g_string_new (NULL);

  children = ephy_node_get_children (eb->keywords);
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;

    kid = g_ptr_array_index (children, i);

    if (kid != eb->notcategorized &&
        kid != eb->bookmarks &&
        kid != eb->local &&
        ephy_node_has_child (kid, bookmark)) {
      const char *topic;
      topic = ephy_node_get_property_string
                (kid, EPHY_NODE_KEYWORD_PROP_NAME);
      g_string_append (list, topic);
      g_string_append (list, " ");
    }
  }

  title = ephy_node_get_property_string
            (bookmark, EPHY_NODE_BMK_PROP_TITLE);
  g_string_append (list, " ");
  g_string_append (list, title);

  normalized_keywords = g_utf8_normalize (list->str, -1, G_NORMALIZE_ALL);
  case_normalized_keywords = g_utf8_casefold (normalized_keywords, -1);

  ephy_node_set_property_string (bookmark, EPHY_NODE_BMK_PROP_KEYWORDS,
                                 case_normalized_keywords);

  g_string_free (list, TRUE);
  g_free (normalized_keywords);
  g_free (case_normalized_keywords);
}

static void
bookmarks_changed_cb (EphyNode      *node,
                      EphyNode      *child,
                      guint          property_id,
                      EphyBookmarks *eb)
{
  if (property_id == EPHY_NODE_BMK_PROP_TITLE) {
    update_bookmark_keywords (eb, child);
  }

  ephy_bookmarks_save_delayed (eb, BOOKMARKS_SAVE_DELAY);
}

static void
bookmarks_removed_cb (EphyNode      *node,
                      EphyNode      *child,
                      guint          old_index,
                      EphyBookmarks *eb)
{
  ephy_bookmarks_save_delayed (eb, BOOKMARKS_SAVE_DELAY);
}

static gboolean
bookmark_is_categorized (EphyBookmarks *eb, EphyNode *bookmark)
{
  GPtrArray *children;
  guint i;

  children = ephy_node_get_children (eb->keywords);
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;

    kid = g_ptr_array_index (children, i);

    if (kid != eb->notcategorized &&
        kid != eb->bookmarks &&
        kid != eb->local &&
        ephy_node_has_child (kid, bookmark)) {
      return TRUE;
    }
  }

  return FALSE;
}

static void
topics_removed_cb (EphyNode      *node,
                   EphyNode      *child,
                   guint          old_index,
                   EphyBookmarks *eb)
{
  GPtrArray *children;
  guint i;

  children = ephy_node_get_children (child);
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;

    kid = g_ptr_array_index (children, i);

    if (!bookmark_is_categorized (eb, kid) &&
        !ephy_node_has_child (eb->notcategorized, kid)) {
      ephy_node_add_child
        (eb->notcategorized, kid);
    }

    update_bookmark_keywords (eb, kid);
  }
}

static void
fix_hierarchy_topic (EphyBookmarks *eb,
                     EphyNode      *topic)
{
  GPtrArray *children;
  EphyNode *bookmark;
  const char *name;
  char **split;
  guint i, j;

  children = ephy_node_get_children (topic);
  name = ephy_node_get_property_string (topic, EPHY_NODE_KEYWORD_PROP_NAME);
  split = g_strsplit (name, "->", -1);

  for (i = 0; split[i]; i++) {
    if (split[i][0] == '\0') continue;

    topic = ephy_bookmarks_find_keyword (eb, split[i], FALSE);
    if (topic == NULL) {
      topic = ephy_bookmarks_add_keyword (eb, split[i]);
    }
    for (j = 0; j < children->len; j++) {
      bookmark = g_ptr_array_index (children, j);
      ephy_bookmarks_set_keyword (eb, topic, bookmark);
    }
  }

  g_strfreev (split);
}

static void
fix_hierarchy (EphyBookmarks *eb)
{
  GPtrArray *topics;
  EphyNode *topic;
  const char *name;
  int i;

  topics = ephy_node_get_children (eb->keywords);
  for (i = (int)topics->len - 1; i >= 0; i--) {
    topic = (EphyNode *)g_ptr_array_index (topics, i);
    name = ephy_node_get_property_string
             (topic, EPHY_NODE_KEYWORD_PROP_NAME);
    if (strstr (name, "->") != NULL) {
      fix_hierarchy_topic (eb, topic);
      ephy_node_remove_child (eb->keywords, topic);
    }
  }
}

static void
backup_file (const char *original_filename, const char *extension)
{
  char *template, *backup_filename;
  int result = 0;

  if (g_file_test (original_filename, G_FILE_TEST_EXISTS) == FALSE) {
    return;
  }

  template = g_strconcat (original_filename, ".backup-XXXXXX", NULL);
  backup_filename = ephy_file_tmp_filename (template, extension);

  if (backup_filename != NULL) {
    result = rename (original_filename, backup_filename);
  }

  if (result >= 0) {
    g_message ("Your old bookmarks file was backed up as \"%s\".\n",
               backup_filename);
  } else {
    g_warning ("Backup failed! Your old bookmarks file was lost.\n");
  }

  g_free (template);
  g_free (backup_filename);
}

/* C&P adapted from gnome-vfs-dns-sd.c */
static GHashTable *
decode_txt_record (AvahiStringList *input_text)
{
  GHashTable *hash;
  guint i;
  int len;
  char *key, *value, *end;
  char *key_dup, *value_dup;
  char *raw_txt;
  size_t raw_txt_len;

  if (!input_text)
    return NULL;

  raw_txt_len = avahi_string_list_serialize (input_text, NULL, 0);
  raw_txt = g_malloc (raw_txt_len);
  raw_txt_len = avahi_string_list_serialize (input_text, raw_txt, raw_txt_len);

  if (raw_txt == NULL)
    return NULL;

  hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);

  i = 0;
  while (i < raw_txt_len) {
    len = raw_txt[i++];

    if (i + len > raw_txt_len) {
      break;
    }

    if (len == 0) {
      continue;
    }

    key = &raw_txt[i];
    end = &raw_txt[i + len];
    i += len;

    if (*key == '=') {
      /* 6.4 - silently ignore keys starting with = */
      continue;
    }

    value = memchr (key, '=', len);
    if (value) {
      key_dup = g_strndup (key, value - key);
      value++;                   /* Skip '=' */
      value_dup = g_strndup (value, end - value);
    } else {
      key_dup = g_strndup (key, len);
      value_dup = NULL;
    }
    if (!g_hash_table_lookup_extended (hash,
                                       key_dup,
                                       NULL, NULL)) {
      g_hash_table_insert (hash,
                           key_dup,
                           value_dup);
    } else {
      g_free (key_dup);
      g_free (value_dup);
    }
  }

  return hash;
}

/* End of copied code */

static char *
get_id_for_response (const char *type,
                     const char *domain,
                     const char *name)
{
  /* FIXME: limit length! */
  return g_strdup_printf ("%s\1%s\1%s",
                          type,
                          domain,
                          name);
}

typedef struct {
  EphyBookmarks *bookmarks;
  GaServiceResolver *resolver;
  EphyNode *node;
  char *name;
  char *type;
  char *domain;
} ResolveData;

static void
resolver_found_cb (GaServiceResolver  *resolver,
                   int                 interface,
                   GaProtocol          protocol,
                   const char         *name,
                   const char         *type,
                   const char         *domain,
                   const char         *host_name,
                   const AvahiAddress *address,
                   guint16             port,
                   AvahiStringList    *txt,
                   glong               flags,
                   ResolveData        *data)
{
  EphyBookmarks *bookmarks = data->bookmarks;
  GValue value = { 0, };
  const char *path = NULL;
  char host[128];
  GHashTable *text_table;
  char *url;
  gboolean was_immutable;
  gboolean is_new_node = FALSE;
  guint i;

  LOG ("resolver_found_cb resolver %p\n", resolver);

  was_immutable = ephy_node_db_is_immutable (bookmarks->db);
  ephy_node_db_set_immutable (bookmarks->db, FALSE);

  /* Find the protocol */
  for (i = 0; i < G_N_ELEMENTS (zeroconf_protos); ++i) {
    char proto[20];

    g_snprintf (proto, sizeof (proto), "_%s._tcp", zeroconf_protos[i]);
    if (strcmp (type, proto) == 0) break;
  }
  if (i == G_N_ELEMENTS (zeroconf_protos)) return;

  if (address == NULL) {
    g_warning ("Zeroconf failed to resolve host %s", name);
    return;
  }

  text_table = decode_txt_record (txt);

  if (text_table != NULL) {
    path = g_hash_table_lookup (text_table, "path");
  }
  if (path == NULL || path[0] == '\0') {
    path = "/";
  }

  avahi_address_snprint (host, sizeof (host), address);

  LOG ("0conf RESOLVED type=%s domain=%s name=%s => proto=%s host=%s port=%d path=%s\n",
       type, domain, name,
       zeroconf_protos[i], host, port, path);

  was_immutable = ephy_node_db_is_immutable (bookmarks->db);
  ephy_node_db_set_immutable (bookmarks->db, FALSE);

  if (data->node == NULL) {
    is_new_node = TRUE;

    data->node = ephy_node_new (bookmarks->db);
    g_assert (data->node != NULL);

    /* don't allow dragging this node */
    ephy_node_set_is_drag_source (data->node, FALSE);

    g_value_init (&value, G_TYPE_STRING);
    g_value_take_string (&value,
                         get_id_for_response (data->type,
                                              data->domain,
                                              data->name));
    ephy_node_set_property (data->node, EPHY_NODE_BMK_PROP_SERVICE_ID, &value);
    g_value_unset (&value);

    /* FIXME: limit length! */
    ephy_node_set_property_string (data->node,
                                   EPHY_NODE_BMK_PROP_TITLE,
                                   name);

    ephy_node_set_property_boolean (data->node,
                                    EPHY_NODE_BMK_PROP_IMMUTABLE,
                                    TRUE);
  }

  /* FIXME: limit length! */
  url = g_strdup_printf ("%s://%s:%d%s", zeroconf_protos[i], host, port, path);

  g_value_init (&value, G_TYPE_STRING);
  g_value_take_string (&value, url);
  ephy_node_set_property (data->node, EPHY_NODE_BMK_PROP_LOCATION, &value);
  g_value_unset (&value);

  if (is_new_node) {
    ephy_node_add_child (bookmarks->bookmarks, data->node);
    ephy_node_add_child (bookmarks->local, data->node);
  }

  ephy_node_db_set_immutable (bookmarks->db, was_immutable);

  if (text_table != NULL) {
    g_hash_table_unref (text_table);
  }
}

static void
resolver_failure_cb (GaServiceResolver *resolver,
                     GError            *error,
                     ResolveData       *data)
{
  LOG ("resolver_failure_cb resolver %p: %s\n", resolver, error ? error->message : "(null)");

  /* Remove the node, if present */
  if (data->node != NULL) {
    EphyBookmarks *bookmarks = data->bookmarks;
    gboolean was_immutable;

    was_immutable = ephy_node_db_is_immutable (bookmarks->db);
    ephy_node_db_set_immutable (bookmarks->db, FALSE);
    ephy_node_unref (data->node);
    data->node = NULL;
    ephy_node_db_set_immutable (bookmarks->db, was_immutable);
  }
}

static void
resolve_data_free (ResolveData *data)
{
  if (data->resolver)
    g_object_unref (data->resolver);

  g_free (data->type);
  g_free (data->name);
  g_free (data->domain);
  g_slice_free (ResolveData, data);
}

static void
browser_new_service_cb (GaServiceBrowser *browser,
                        int               interface,
                        GaProtocol        protocol,
                        const char       *name,
                        const char       *type,
                        const char       *domain,
                        glong             flags,
                        EphyBookmarks    *bookmarks)
{
  ResolveData *data;
  char *node_id;
  GError *error = NULL;

  node_id = get_id_for_response (type, domain, name);

  LOG ("0conf ADD: type=%s domain=%s name=%s\n",
       type, domain, name);

  if (g_hash_table_lookup (bookmarks->resolve_handles, node_id) != NULL) {
    g_free (node_id);
    return;
  }

  data = g_slice_new0 (ResolveData);
  data->bookmarks = bookmarks;
  data->node = NULL;
  data->type = g_strdup (type);
  data->name = g_strdup (name);
  data->domain = g_strdup (domain);

  data->resolver = ga_service_resolver_new (AVAHI_IF_UNSPEC,
                                            AVAHI_PROTO_UNSPEC,
                                            name, type, domain,
                                            AVAHI_PROTO_UNSPEC,
                                            GA_LOOKUP_USE_MULTICAST);
  g_signal_connect (data->resolver, "found",
                    G_CALLBACK (resolver_found_cb), data);
  g_signal_connect (data->resolver, "failure",
                    G_CALLBACK (resolver_failure_cb), data);
  if (!ga_service_resolver_attach (data->resolver,
                                   bookmarks->ga_client,
                                   &error)) {
    g_warning ("Unable to resolve Zeroconf service %s: %s", name, error ? error->message : "(null)");
    g_clear_error (&error);
    resolve_data_free (data);
    g_free (node_id);
    return;
  }

  g_hash_table_insert (bookmarks->resolve_handles,
                       node_id /* transfer ownership */, data);
}

static void
browser_removed_service_cb (GaServiceBrowser *browser,
                            int               interface,
                            GaProtocol        protocol,
                            const char       *name,
                            const char       *type,
                            const char       *domain,
                            glong             flags,
                            EphyBookmarks    *bookmarks)
{
  char *node_id;
  ResolveData *data;

  node_id = get_id_for_response (type, domain, name);
  data = g_hash_table_lookup (bookmarks->resolve_handles, node_id);
  /* shouldn't really happen, but let's play safe */
  if (!data) {
    g_free (node_id);
    return;
  }

  if (data->node != NULL) {
    gboolean was_immutable;

    was_immutable = ephy_node_db_is_immutable (bookmarks->db);
    ephy_node_db_set_immutable (bookmarks->db, FALSE);
    ephy_node_unref (data->node);
    data->node = NULL;
    ephy_node_db_set_immutable (bookmarks->db, was_immutable);
  }

  g_hash_table_remove (bookmarks->resolve_handles, node_id);
  g_free (node_id);
}

static void
start_browsing (GaClient      *ga_client,
                EphyBookmarks *bookmarks)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (zeroconf_protos); ++i) {
    GaServiceBrowser *browser = NULL;
    char proto[20];

    g_snprintf (proto, sizeof (proto), "_%s._tcp", zeroconf_protos[i]);

    browser = ga_service_browser_new (proto);
    g_signal_connect (browser, "new-service",
                      G_CALLBACK (browser_new_service_cb), bookmarks);
    g_signal_connect (browser, "removed-service",
                      G_CALLBACK (browser_removed_service_cb), bookmarks);
    if (!ga_service_browser_attach (browser,
                                    ga_client,
                                    NULL)) {
      g_warning ("Unable to start Zeroconf subsystem");
      g_object_unref (browser);
      return;
    }

    bookmarks->browse_handles[i] = browser;
  }
}

static void
ga_client_state_changed_cb (GaClient      *ga_client,
                            GaClientState  state,
                            EphyBookmarks *bookmarks)
{
  if (state == GA_CLIENT_STATE_FAILURE) {
    if (avahi_client_errno (ga_client->avahi_client) == AVAHI_ERR_DISCONNECTED) {
      g_object_unref (bookmarks->ga_client);
      bookmarks->ga_client = NULL;

      ephy_local_bookmarks_start_client (bookmarks);
    }
  }
  if (state == GA_CLIENT_STATE_S_RUNNING) {
    start_browsing (ga_client, bookmarks);
  }
}

static void
ephy_local_bookmarks_start_client (EphyBookmarks *bookmarks)
{
  GaClient *ga_client;

  ga_client = ga_client_new (GA_CLIENT_FLAG_NO_FAIL);
  g_signal_connect (ga_client, "state-changed",
                    G_CALLBACK (ga_client_state_changed_cb),
                    bookmarks);
  if (!ga_client_start (ga_client, NULL)) {
    g_warning ("Unable to start Zeroconf subsystem");
    g_object_unref (ga_client);
    return;
  }
  bookmarks->ga_client = ga_client;
}

static void
ephy_local_bookmarks_init (EphyBookmarks *bookmarks)
{
  bookmarks->resolve_handles = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                      g_free,
                                                      (GDestroyNotify)resolve_data_free);
  ephy_local_bookmarks_start_client (bookmarks);
}

static void
ephy_local_bookmarks_stop (EphyBookmarks *bookmarks)
{
  guint i;

  for (i = 0; i < G_N_ELEMENTS (zeroconf_protos); ++i) {
    if (bookmarks->browse_handles[i] != NULL) {
      g_object_unref (bookmarks->browse_handles[i]);
      bookmarks->browse_handles[i] = NULL;
    }
  }

  if (bookmarks->resolve_handles != NULL) {
    g_hash_table_destroy (bookmarks->resolve_handles);
    bookmarks->resolve_handles = NULL;
  }

  if (bookmarks->local != NULL) {
    ephy_node_unref (bookmarks->local);
    bookmarks->local = NULL;
  }

  if (bookmarks->ga_client != NULL) {
    g_object_unref (bookmarks->ga_client);
    bookmarks->ga_client = NULL;
  }
}

static void
ephy_bookmarks_init (EphyBookmarks *eb)
{
  EphyNodeDb *db;

  /* Translators: this topic contains all bookmarks */
  const char *bk_all = C_("bookmarks", "All");

  /* Translators: this topic contains the not categorized
     bookmarks */
  const char *bk_not_categorized = C_("bookmarks", "Not Categorized");

  /* Translators: this is an automatic topic containing local
   * websites bookmarks autodiscovered with zeroconf. */
  const char *bk_local_sites = C_("bookmarks", "Nearby Sites");

  db = ephy_node_db_new (EPHY_NODE_DB_BOOKMARKS);
  eb->db = db;

  eb->xml_file = g_build_filename (ephy_dot_dir (),
                                   EPHY_BOOKMARKS_FILE,
                                   NULL);
  eb->rdf_file = g_build_filename (ephy_dot_dir (),
                                   EPHY_BOOKMARKS_FILE_RDF,
                                   NULL);

  /* Bookmarks */
  eb->bookmarks = ephy_node_new_with_id (db, BOOKMARKS_NODE_ID);

  ephy_node_set_property_string (eb->bookmarks,
                                 EPHY_NODE_KEYWORD_PROP_NAME,
                                 bk_all);
  ephy_node_signal_connect_object (eb->bookmarks,
                                   EPHY_NODE_CHILD_REMOVED,
                                   (EphyNodeCallback)bookmarks_removed_cb,
                                   G_OBJECT (eb));
  ephy_node_signal_connect_object (eb->bookmarks,
                                   EPHY_NODE_CHILD_CHANGED,
                                   (EphyNodeCallback)bookmarks_changed_cb,
                                   G_OBJECT (eb));

  /* Keywords */
  eb->keywords = ephy_node_new_with_id (db, KEYWORDS_NODE_ID);
  ephy_node_set_property_int (eb->bookmarks,
                              EPHY_NODE_KEYWORD_PROP_PRIORITY,
                              EPHY_NODE_ALL_PRIORITY);

  ephy_node_signal_connect_object (eb->keywords,
                                   EPHY_NODE_CHILD_REMOVED,
                                   (EphyNodeCallback)topics_removed_cb,
                                   G_OBJECT (eb));

  ephy_node_add_child (eb->keywords,
                       eb->bookmarks);

  /* Not categorized */
  eb->notcategorized = ephy_node_new_with_id (db, BMKS_NOTCATEGORIZED_NODE_ID);


  ephy_node_set_property_string (eb->notcategorized,
                                 EPHY_NODE_KEYWORD_PROP_NAME,
                                 bk_not_categorized);

  ephy_node_set_property_int (eb->notcategorized,
                              EPHY_NODE_KEYWORD_PROP_PRIORITY,
                              EPHY_NODE_SPECIAL_PRIORITY);

  ephy_node_add_child (eb->keywords, eb->notcategorized);

  /* Local Websites */
  eb->local = ephy_node_new_with_id (db, BMKS_LOCAL_NODE_ID);

  /* don't allow drags to this topic */
  ephy_node_set_is_drag_dest (eb->local, FALSE);


  ephy_node_set_property_string (eb->local,
                                 EPHY_NODE_KEYWORD_PROP_NAME,
                                 bk_local_sites);
  ephy_node_set_property_int (eb->local,
                              EPHY_NODE_KEYWORD_PROP_PRIORITY,
                              EPHY_NODE_SPECIAL_PRIORITY);

  ephy_node_add_child (eb->keywords, eb->local);
  ephy_local_bookmarks_init (eb);

  /* Smart bookmarks */
  eb->smartbookmarks = ephy_node_new_with_id (db, SMARTBOOKMARKS_NODE_ID);

  if (g_file_test (eb->xml_file, G_FILE_TEST_EXISTS) == FALSE
      && g_file_test (eb->rdf_file, G_FILE_TEST_EXISTS) == FALSE) {
    eb->init_defaults = TRUE;
  } else if (ephy_node_db_load_from_file (eb->db, eb->xml_file,
                                          (xmlChar *)EPHY_BOOKMARKS_XML_ROOT,
                                          (xmlChar *)EPHY_BOOKMARKS_XML_VERSION) == FALSE) {
    /* save the corrupted files so the user can late try to
     * manually recover them. See bug #128308.
     */

    g_warning ("Could not read bookmarks file \"%s\", trying to "
               "re-import bookmarks from \"%s\"\n",
               eb->xml_file, eb->rdf_file);

    backup_file (eb->xml_file, "xml");

    if (ephy_bookmarks_import_rdf (eb, eb->rdf_file) == FALSE) {
      backup_file (eb->rdf_file, "rdf");

      eb->init_defaults = TRUE;
    }
  }

  if (eb->init_defaults) {
    ephy_bookmarks_init_defaults (eb);
  }

  fix_hierarchy (eb);

  g_settings_bind (EPHY_SETTINGS_LOCKDOWN,
                   EPHY_PREFS_LOCKDOWN_BOOKMARK_EDITING,
                   eb->db, "immutable",
                   G_SETTINGS_BIND_GET);

  ephy_setup_history_notifiers (eb);
}

static void
ephy_bookmarks_finalize (GObject *object)
{
  EphyBookmarks *eb = EPHY_BOOKMARKS (object);

  if (eb->save_timeout_id != 0) {
    g_source_remove (eb->save_timeout_id);
  }

  ephy_bookmarks_save (eb);

  ephy_local_bookmarks_stop (eb);

  ephy_node_unref (eb->bookmarks);
  ephy_node_unref (eb->keywords);
  ephy_node_unref (eb->notcategorized);
  ephy_node_unref (eb->smartbookmarks);

  g_object_unref (eb->db);

  g_free (eb->xml_file);
  g_free (eb->rdf_file);

  LOG ("Bookmarks finalized");

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

EphyBookmarks *
ephy_bookmarks_new (void)
{
  return EPHY_BOOKMARKS (g_object_new (EPHY_TYPE_BOOKMARKS, NULL));
}

static void
update_has_smart_address (EphyBookmarks *bookmarks, EphyNode *bmk, const char *address)
{
  EphyNode *smart_bmks;
  gboolean smart = FALSE, with_options = FALSE;

  smart_bmks = bookmarks->smartbookmarks;

  if (address) {
    smart = strstr (address, "%s") != NULL;
    with_options = strstr (address, "%s%{") != NULL;
  }

  /* if we have a smart bookmark with width specification,
   * remove from smart bookmarks first to force an update
   * in the toolbar
   */
  if (smart && with_options) {
    if (ephy_node_has_child (smart_bmks, bmk)) {
      ephy_node_remove_child (smart_bmks, bmk);
    }
    ephy_node_add_child (smart_bmks, bmk);
  } else if (smart) {
    if (!ephy_node_has_child (smart_bmks, bmk)) {
      ephy_node_add_child (smart_bmks, bmk);
    }
  } else {
    if (ephy_node_has_child (smart_bmks, bmk)) {
      ephy_node_remove_child (smart_bmks, bmk);
    }
  }
}

EphyNode *
ephy_bookmarks_add (EphyBookmarks *eb,
                    const char    *title,
                    const char    *url)
{
  EphyNode *bm;
  WebKitFaviconDatabase *favicon_database;
  EphyEmbedShell *shell = ephy_embed_shell_get_default ();

  bm = ephy_node_new (eb->db);

  if (bm == NULL) return NULL;

  if (url == NULL) return NULL;
  ephy_node_set_property_string (bm, EPHY_NODE_BMK_PROP_LOCATION, url);

  if (title == NULL || title[0] == '\0') {
    title = _("Untitled");
  }
  ephy_node_set_property_string (bm, EPHY_NODE_BMK_PROP_TITLE, title);

  favicon_database = webkit_web_context_get_favicon_database (ephy_embed_shell_get_web_context (shell));
  if (favicon_database != NULL) {
    char *icon = webkit_favicon_database_get_favicon_uri (favicon_database, url);
    if (icon != NULL) {
      ephy_node_set_property_string
        (bm, EPHY_NODE_BMK_PROP_ICON, icon);
      g_free (icon);
    }
  }

  update_has_smart_address (eb, bm, url);
  update_bookmark_keywords (eb, bm);

  ephy_node_add_child (eb->bookmarks, bm);
  ephy_node_add_child (eb->notcategorized, bm);

  ephy_bookmarks_save_delayed (eb, 0);

  return bm;
}

void
ephy_bookmarks_set_address (EphyBookmarks *eb,
                            EphyNode      *bookmark,
                            const char    *address)
{
  ephy_node_set_property_string (bookmark, EPHY_NODE_BMK_PROP_LOCATION,
                                 address);

  update_has_smart_address (eb, bookmark, address);
}

EphyNode *
ephy_bookmarks_find_bookmark (EphyBookmarks *eb,
                              const char    *url)
{
  GPtrArray *children;
  guint i;

  g_return_val_if_fail (EPHY_IS_BOOKMARKS (eb), NULL);
  g_return_val_if_fail (eb->bookmarks != NULL, NULL);
  g_return_val_if_fail (url != NULL, NULL);

  children = ephy_node_get_children (eb->bookmarks);
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;
    const char *location;

    kid = g_ptr_array_index (children, i);
    location = ephy_node_get_property_string
                 (kid, EPHY_NODE_BMK_PROP_LOCATION);

    if (location != NULL && strcmp (url, location) == 0) {
      return kid;
    }
  }

  return NULL;
}

static gboolean
is_similar (const char *url1, const char *url2)
{
  int i, j;

  for (i = 0; url1[i]; i++)
    if (url1[i] == '#' || url1[i] == '?')
      break;
  while (i > 0 && url1[i] == '/')
    i--;

  for (j = 0; url2[j]; j++)
    if (url2[j] == '#' || url2[j] == '?')
      break;
  while (j > 0 && url2[j] == '/')
    j--;

  if (i != j) return FALSE;
  if (strncmp (url1, url2, i) != 0) return FALSE;
  return TRUE;
}

gint
ephy_bookmarks_get_similar (EphyBookmarks *eb,
                            EphyNode      *bookmark,
                            GPtrArray     *identical,
                            GPtrArray     *similar)
{
  GPtrArray *children;
  const char *url;
  guint i, result;

  g_return_val_if_fail (EPHY_IS_BOOKMARKS (eb), -1);
  g_return_val_if_fail (eb->bookmarks != NULL, -1);
  g_return_val_if_fail (bookmark != NULL, -1);

  url = ephy_node_get_property_string
          (bookmark, EPHY_NODE_BMK_PROP_LOCATION);

  g_return_val_if_fail (url != NULL, -1);

  result = 0;

  children = ephy_node_get_children (eb->bookmarks);
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;
    const char *location;

    kid = g_ptr_array_index (children, i);
    if (kid == bookmark) {
      continue;
    }

    location = ephy_node_get_property_string
                 (kid, EPHY_NODE_BMK_PROP_LOCATION);

    if (location != NULL) {
      if (identical != NULL && strcmp (url, location) == 0) {
        g_ptr_array_add (identical, kid);
        result++;
      } else if (is_similar (url, location)) {
        if (similar != NULL) {
          g_ptr_array_add (similar, kid);
        }
        result++;
      }
    }
  }

  return result;
}

void
ephy_bookmarks_set_icon (EphyBookmarks *eb,
                         const char    *url,
                         const char    *icon)
{
  EphyNode *node;

  g_return_if_fail (icon != NULL);

  node = ephy_bookmarks_find_bookmark (eb, url);
  if (node == NULL) return;

  ephy_node_set_property_string (node, EPHY_NODE_BMK_PROP_ICON, icon);
}


void
ephy_bookmarks_set_usericon (EphyBookmarks *eb,
                             const char    *url,
                             const char    *icon)
{
  EphyNode *node;

  g_return_if_fail (icon != NULL);

  node = ephy_bookmarks_find_bookmark (eb, url);
  if (node == NULL) return;

  ephy_node_set_property_string (node, EPHY_NODE_BMK_PROP_USERICON,
                                 icon);
}


/* name must end with '=' */
static char *
get_option (char       *start,
            const char *name,
            char      **optionsend)
{
  char *end, *p;

  *optionsend = start;

  if (start == NULL || start[0] != '%' || start[1] != '{') return NULL;
  start += 2;

  end = strstr (start, "}");
  if (end == NULL) return NULL;
  *optionsend = end + 1;

  start = strstr (start, name);
  if (start == NULL || start > end) return NULL;
  start += strlen (name);

  /* Find end of option, either ',' or '}' */
  end = strstr (start, ",");
  if (end == NULL || end >= *optionsend) end = *optionsend - 1;

  /* limit option length and sanity-check it */
  if (end - start > 32) return NULL;
  for (p = start; p < end; ++p) {
    if (!g_ascii_isalnum (*p)) return NULL;
  }

  return g_strndup (start, end - start);
}

char *
ephy_bookmarks_resolve_address (EphyBookmarks *eb,
                                const char    *address,
                                const char    *content)
{
  GString *result;
  char *pos, *oldpos, *arg, *escaped_arg, *encoding, *optionsend;

  g_return_val_if_fail (EPHY_IS_BOOKMARKS (eb), NULL);
  g_return_val_if_fail (address != NULL, NULL);

  if (content == NULL) content = "";

  result = g_string_new_len (NULL, strlen (content) + strlen (address));

  /* substitute %s's */
  oldpos = (char *)address;
  while ((pos = strstr (oldpos, "%s")) != NULL) {
    g_string_append_len (result, oldpos, pos - oldpos);
    pos += 2;

    encoding = get_option (pos, "encoding=", &optionsend);
    if (encoding != NULL) {
      GError *error = NULL;

      arg = g_convert (content, strlen (content), encoding,
                       "UTF-8", NULL, NULL, &error);
      if (error != NULL) {
        g_warning ("Error when converting arg to encoding '%s': %s\n",
                   encoding, error->message);
        g_error_free (error);
      } else {
        escaped_arg = g_uri_escape_string (arg, NULL, TRUE);
        g_string_append (result, escaped_arg);
        g_free (escaped_arg);
        g_free (arg);
      }

      g_free (encoding);
    } else {
      arg = g_uri_escape_string (content, NULL, TRUE);
      g_string_append (result, arg);
      g_free (arg);
    }

    oldpos = optionsend;
  }
  g_string_append (result, oldpos);

  return g_string_free (result, FALSE);
}

guint
ephy_bookmarks_get_smart_bookmark_width (EphyNode *bookmark)
{
  const char *url;
  char *option, *end, *number;
  guint width;

  url = ephy_node_get_property_string (bookmark, EPHY_NODE_BMK_PROP_LOCATION);
  if (url == NULL) return 0;

  /* this takes the first %s, but that's okay since we only support one text entry box */
  option = strstr (url, "%s%{");
  if (option == NULL) return 0;
  option += 2;

  number = get_option (option, "width=", &end);
  if (number == NULL) return 0;

  width = (guint)g_ascii_strtoull (number, NULL, 10);
  g_free (number);

  return CLAMP (width, 1, 64);
}

EphyNode *
ephy_bookmarks_add_keyword (EphyBookmarks *eb,
                            const char    *name)
{
  EphyNode *key;

  key = ephy_node_new (eb->db);

  if (key == NULL) return NULL;

  ephy_node_set_property_string (key, EPHY_NODE_KEYWORD_PROP_NAME,
                                 name);
  ephy_node_set_property_int (key, EPHY_NODE_KEYWORD_PROP_PRIORITY,
                              EPHY_NODE_NORMAL_PRIORITY);

  ephy_node_add_child (eb->keywords, key);

  return key;
}

void
ephy_bookmarks_remove_keyword (EphyBookmarks *eb,
                               EphyNode      *keyword)
{
  ephy_node_remove_child (eb->keywords, keyword);
}

char *
ephy_bookmarks_get_topic_uri (EphyBookmarks *eb,
                              EphyNode      *node)
{
  char *uri;

  if (ephy_bookmarks_get_bookmarks (eb) == node) {
    uri = g_strdup ("topic://Special/All");
  } else if (ephy_bookmarks_get_not_categorized (eb) == node) {
    uri = g_strdup ("topic://Special/NotCategorized");
  } else if (ephy_bookmarks_get_local (eb) == node) {
    /* Note: do not change to "Nearby" because of existing custom toolbars */
    uri = g_strdup ("topic://Special/Local");
  } else {
    const char *name;

    name = ephy_node_get_property_string
             (node, EPHY_NODE_KEYWORD_PROP_NAME);

    uri = g_strdup_printf ("topic://%s", name);
  }

  return uri;
}

EphyNode *
ephy_bookmarks_find_keyword (EphyBookmarks *eb,
                             const char    *name,
                             gboolean       partial_match)
{
  EphyNode *node;
  GPtrArray *children;
  guint i;
  const char *topic_name;

  g_return_val_if_fail (name != NULL, NULL);

  topic_name = name;

  if (g_utf8_strlen (name, -1) == 0) {
    LOG ("Empty name, no keyword matches.");
    return NULL;
  }

  if (strcmp (name, "topic://Special/All") == 0) {
    return ephy_bookmarks_get_bookmarks (eb);
  } else if (strcmp (name, "topic://Special/NotCategorized") == 0) {
    return ephy_bookmarks_get_not_categorized (eb);
  } else if (strcmp (name, "topic://Special/Local") == 0) {
    return ephy_bookmarks_get_local (eb);
  } else if (g_str_has_prefix (name, "topic://")) {
    topic_name += strlen ("topic://");
  }

  children = ephy_node_get_children (eb->keywords);
  node = NULL;
  for (i = 0; i < children->len; i++) {
    EphyNode *kid;
    const char *key;

    kid = g_ptr_array_index (children, i);
    key = ephy_node_get_property_string (kid, EPHY_NODE_KEYWORD_PROP_NAME);

    if ((partial_match && g_str_has_prefix (key, topic_name) > 0) ||
        (!partial_match && strcmp (key, topic_name) == 0)) {
      node = kid;
    }
  }

  return node;
}

gboolean
ephy_bookmarks_has_keyword (EphyBookmarks *eb,
                            EphyNode      *keyword,
                            EphyNode      *bookmark)
{
  return ephy_node_has_child (keyword, bookmark);
}

void
ephy_bookmarks_set_keyword (EphyBookmarks *eb,
                            EphyNode      *keyword,
                            EphyNode      *bookmark)
{
  if (ephy_node_has_child (keyword, bookmark)) return;

  ephy_node_add_child (keyword, bookmark);

  if (ephy_node_has_child (eb->notcategorized, bookmark)) {
    LOG ("Remove from categorized bookmarks");
    ephy_node_remove_child
      (eb->notcategorized, bookmark);
  }

  update_bookmark_keywords (eb, bookmark);

  g_signal_emit (G_OBJECT (eb), ephy_bookmarks_signals[TREE_CHANGED], 0);
}

void
ephy_bookmarks_unset_keyword (EphyBookmarks *eb,
                              EphyNode      *keyword,
                              EphyNode      *bookmark)
{
  if (!ephy_node_has_child (keyword, bookmark)) return;

  ephy_node_remove_child (keyword, bookmark);

  if (!bookmark_is_categorized (eb, bookmark) &&
      !ephy_node_has_child (eb->notcategorized, bookmark)) {
    LOG ("Add to not categorized bookmarks");
    ephy_node_add_child
      (eb->notcategorized, bookmark);
  }

  update_bookmark_keywords (eb, bookmark);

  g_signal_emit (G_OBJECT (eb), ephy_bookmarks_signals[TREE_CHANGED], 0);
}

/**
 * ephy_bookmarks_get_smart_bookmarks:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_smart_bookmarks (EphyBookmarks *eb)
{
  return eb->smartbookmarks;
}

/**
 * ephy_bookmarks_get_keywords:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_keywords (EphyBookmarks *eb)
{
  return eb->keywords;
}

/**
 * ephy_bookmarks_get_bookmarks:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_bookmarks (EphyBookmarks *eb)
{
  return eb->bookmarks;
}

/**
 * ephy_bookmarks_get_local:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_local (EphyBookmarks *eb)
{
  return eb->local;
}

/**
 * ephy_bookmarks_get_not_categorized:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_not_categorized (EphyBookmarks *eb)
{
  return eb->notcategorized;
}

/**
 * ephy_bookmarks_get_from_id:
 *
 * Return value: (transfer none):
 **/
EphyNode *
ephy_bookmarks_get_from_id (EphyBookmarks *eb, long id)
{
  return ephy_node_db_get_node_from_id (eb->db, id);
}

int
ephy_bookmarks_compare_topics (gconstpointer a, gconstpointer b)
{
  EphyNode *node_a = (EphyNode *)a;
  EphyNode *node_b = (EphyNode *)b;
  const char *title1, *title2;
  int priority1, priority2;

  priority1 = ephy_node_get_property_int (node_a, EPHY_NODE_KEYWORD_PROP_PRIORITY);
  priority2 = ephy_node_get_property_int (node_b, EPHY_NODE_KEYWORD_PROP_PRIORITY);

  if (priority1 > priority2) return 1;
  if (priority1 < priority2) return -1;

  title1 = ephy_node_get_property_string (node_a, EPHY_NODE_KEYWORD_PROP_NAME);
  title2 = ephy_node_get_property_string (node_b, EPHY_NODE_KEYWORD_PROP_NAME);

  if (title1 == title2) return 0;
  if (title1 == NULL) return -1;
  if (title2 == NULL) return 1;
  return g_utf8_collate (title1, title2);
}

int
ephy_bookmarks_compare_topic_pointers (gconstpointer a, gconstpointer b)
{
  EphyNode *node_a = *(EphyNode **)a;
  EphyNode *node_b = *(EphyNode **)b;

  return ephy_bookmarks_compare_topics (node_a, node_b);
}

int
ephy_bookmarks_compare_bookmarks (gconstpointer a, gconstpointer b)
{
  EphyNode *node_a = (EphyNode *)a;
  EphyNode *node_b = (EphyNode *)b;
  const char *title1, *title2;

  title1 = ephy_node_get_property_string (node_a, EPHY_NODE_BMK_PROP_TITLE);
  title2 = ephy_node_get_property_string (node_b, EPHY_NODE_BMK_PROP_TITLE);

  if (title1 == title2) return 0;
  if (title1 == NULL) return -1;
  if (title2 == NULL) return 1;
  return g_utf8_collate (title1, title2);
}

int
ephy_bookmarks_compare_bookmark_pointers (gconstpointer a, gconstpointer b)
{
  EphyNode *node_a = *(EphyNode **)a;
  EphyNode *node_b = *(EphyNode **)b;

  return ephy_bookmarks_compare_bookmarks (node_a, node_b);
}
