/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

/* e-book-backend-file.c - File contact backend.
 *
 * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * 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 Lesser 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.
 *
 * Authors: Nat Friedman <nat@novell.com>
 *          Chris Toshok <toshok@ximian.com>
 *          Hans Petter Jansson <hpj@novell.com>
 */

#include <config.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <errno.h>
#include "db.h"
#include <sys/stat.h>
#include <sys/time.h>

#include <glib/gstdio.h>
#include <glib/gi18n-lib.h>

#include "libebackend/e-dbhash.h"
#include "libebackend/e-db3-utils.h"

#include "libedataserver/e-data-server-util.h"
#include "libedataserver/e-flag.h"

#include "libebook/e-contact.h"

#include "libedata-book/e-book-backend-sexp.h"
#include "libedata-book/e-book-backend-sqlitedb.h"
#include "libedata-book/e-data-book.h"
#include "libedata-book/e-data-book-view.h"

#include "e-book-backend-file.h"

#define d(x)

#define CHANGES_DB_SUFFIX ".changes.db"

#define E_BOOK_BACKEND_FILE_VERSION_NAME "PAS-DB-VERSION"
#define E_BOOK_BACKEND_FILE_VERSION "0.2"

#define PAS_ID_PREFIX "pas-id-"

#define SQLITEDB_EMAIL_ID    "addressbook@localbackend.com"
#define SQLITEDB_FOLDER_ID   "folder_id"
#define SQLITEDB_FOLDER_NAME "folder"

#define EDB_ERROR(_code) e_data_book_create_error (E_DATA_BOOK_STATUS_ ## _code, NULL)
#define EDB_ERROR_EX(_code, _msg) e_data_book_create_error (E_DATA_BOOK_STATUS_ ## _code, _msg)
#define EDB_NOT_OPENED_ERROR EDB_ERROR(NOT_OPENED)

G_DEFINE_TYPE (EBookBackendFile, e_book_backend_file, E_TYPE_BOOK_BACKEND_SYNC)

struct _EBookBackendFilePrivate {
	gchar     *dirname;
	gchar     *filename;
	DB        *file_db;
	DB_ENV    *env;

	EBookBackendSqliteDB *sqlitedb;

	/* for future use */
	gpointer reserved1;
	gpointer reserved2;
	gpointer reserved3;
	gpointer reserved4;
};

G_LOCK_DEFINE_STATIC (db_environments);
static GHashTable *db_environments = NULL;
typedef struct {
	gint ref_count;
	DB_ENV *env;
} global_env;

static void
db_error_to_gerror (const gint db_error,
                    GError **perror)
{
	if (db_error && perror && *perror)
		g_clear_error (perror);

	switch (db_error) {
	case 0:
		return;
	case DB_NOTFOUND:
		g_propagate_error (perror, EDB_ERROR (CONTACT_NOT_FOUND));
		return;
	case EACCES:
		g_propagate_error (perror, EDB_ERROR (PERMISSION_DENIED));
		return;
	default:
		g_propagate_error (perror, e_data_book_create_error_fmt (E_DATA_BOOK_STATUS_OTHER_ERROR, "db error 0x%x (%s)", db_error, db_strerror (db_error) ? db_strerror (db_error) : "Unknown error"));
		return;
	}
}

static void
string_to_dbt (const gchar *str,
               DBT *dbt)
{
	memset (dbt, 0, sizeof (*dbt));
	dbt->data = (gpointer) str;
	dbt->size = strlen (str) + 1;
	dbt->flags = DB_DBT_USERMEM;
}

static gboolean
remove_file (const gchar *filename,
             GError **error)
{
	if (-1 == g_unlink (filename)) {
		if (errno == EACCES || errno == EPERM) {
			g_propagate_error (error, EDB_ERROR (PERMISSION_DENIED));
		} else {
			g_propagate_error (error, e_data_book_create_error_fmt
					   (E_DATA_BOOK_STATUS_OTHER_ERROR,
					    "Failed to remove file '%s': %s", 
					    filename, g_strerror (errno)));
		}
		return FALSE;
	}

	return TRUE;
}

static gboolean
create_directory (const gchar *dirname,
                  GError **error)
{
	gint rv;

	rv = g_mkdir_with_parents (dirname, 0700);
	if (rv == -1 && errno != EEXIST) {
		g_warning ("failed to make directory %s: %s", dirname, g_strerror (errno));
		if (errno == EACCES || errno == EPERM)
			g_propagate_error (error, EDB_ERROR (PERMISSION_DENIED));
		else
			g_propagate_error (error,
					   e_data_book_create_error_fmt (E_DATA_BOOK_STATUS_OTHER_ERROR,
									 "Failed to make directory %s: %s", 
									 dirname, g_strerror (errno)));
		return FALSE;
	}
	return TRUE;
}

static EContact *
create_contact (const gchar *uid,
                const gchar *vcard)
{
	return e_contact_new_from_vcard_with_uid (vcard, uid);
}

static gchar *
load_vcard (EBookBackendFile *bf,
            const gchar *uid,
            GError **error)
{
	DB     *db = bf->priv->file_db;
	DBT     id_dbt, vcard_dbt;
	gchar  *vcard;
	gint    db_error;

	/* Get the old contact from the db and compare the photo fields */
	string_to_dbt (uid, &id_dbt);
	memset (&vcard_dbt, 0, sizeof (vcard_dbt));
	vcard_dbt.flags = DB_DBT_MALLOC;

	db_error = db->get (db, NULL, &id_dbt, &vcard_dbt, 0);

	if (db_error == 0) {
		vcard = vcard_dbt.data;
	} else {
		g_warning (G_STRLOC ": db->get failed with %s", db_strerror (db_error));
		g_propagate_error (error, EDB_ERROR (CONTACT_NOT_FOUND));
		return NULL;
	}

	return vcard;
}

static gboolean
build_sqlitedb (EBookBackendFilePrivate *bfpriv)
{
	DB             *db = bfpriv->file_db;
	DBC            *dbc;
	gint            db_error;
	DBT             id_dbt, vcard_dbt;
	GSList         *contacts = NULL;
	GError         *error = NULL;
	gboolean        skipped_version = FALSE;

	if (!db) {
		g_warning (G_STRLOC ": Not opened yet");
		return FALSE;
	}

	db_error = db->cursor (db, NULL, &dbc, 0);

	if (db_error != 0) {
		g_warning (G_STRLOC ": db->cursor failed with %s", db_strerror (db_error));
		return FALSE;
	}

	memset (&vcard_dbt, 0, sizeof (vcard_dbt));
	memset (&id_dbt, 0, sizeof (id_dbt));
	db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_FIRST);

	while (db_error == 0) {
		/* don't include the version in the list of cards */
		if (skipped_version || strcmp (id_dbt.data, E_BOOK_BACKEND_FILE_VERSION_NAME)) {
			EContact *contact = create_contact (id_dbt.data, vcard_dbt.data);

			contacts = g_slist_prepend (contacts, contact);
		} else {
			skipped_version = TRUE;
		}

		db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_NEXT);

	}

	dbc->c_close (dbc);

	/* Detect error case */
	if (db_error != DB_NOTFOUND) {
		g_warning (G_STRLOC ": dbc->c_get failed with %s", db_strerror (db_error));
		e_util_free_object_slist (contacts);
		return FALSE;
	}

	if (!e_book_backend_sqlitedb_add_contacts (bfpriv->sqlitedb,
						   SQLITEDB_FOLDER_ID,
						   contacts, FALSE, &error)) {
		g_warning ("Failed to build contact summary: %s", error->message);
		g_error_free (error);
		e_util_free_object_slist (contacts);
		return FALSE;
	}

	e_util_free_object_slist (contacts);

	if (!e_book_backend_sqlitedb_set_is_populated (bfpriv->sqlitedb, SQLITEDB_FOLDER_ID, TRUE, &error)) {
		g_warning ("Failed to set the sqlitedb populated flag: %s", error->message);
		g_error_free (error);
		return FALSE;
	}

	return TRUE;
}

static gchar *
e_book_backend_file_create_unique_id (void)
{
	/* use a 32 counter and the 32 bit timestamp to make an id.
	 * it's doubtful 2^32 id's will be created in a second, so we
	 * should be okay. */
	static guint c = 0;
	return g_strdup_printf (PAS_ID_PREFIX "%08lX%08X", time(NULL), c++);
}

static void
set_revision (EContact *contact)
{
	gchar time_string[100] = {0};
	const struct tm *tm = NULL;
	time_t t;

	t = time (NULL);
	tm = gmtime (&t);
	if (tm)
		strftime (time_string, 100, "%Y-%m-%dT%H:%M:%SZ", tm);
	e_contact_set (contact, E_CONTACT_REV, time_string);

}

/**
 * This method will return TRUE if all the contacts were properly created.
 * If at least one contact fails, the method will return FALSE, all
 * changes will be reverted (the @contacts list will stay empty) and
 * @perror will be set.
 */
static gboolean
do_create (EBookBackendFile *bf,
          const GSList *vcards_req,
          GSList **contacts,
          GError **perror)
{
	DB *db = bf->priv->file_db;
	DB_ENV *env = bf->priv->env;
	DB_TXN *txn = NULL;
	GSList *slist = NULL;
	const GSList *l;
	gint db_error = 0;

	g_assert (bf);
	g_assert (vcards_req);

	if (!db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return FALSE;
	}

	/* Begin transaction */
	db_error = env->txn_begin(env, NULL, &txn, 0);
	if (db_error != 0) {
		g_warning (G_STRLOC ": env->txn_begin failed with %s", db_strerror (db_error));
		db_error_to_gerror (db_error, perror);
		return FALSE;
	}

	for (l = vcards_req; l != NULL; l = l->next) {
		DBT            id_dbt, vcard_dbt;
		gchar           *id;
		gchar           *vcard;
		const gchar *rev;
		const gchar *vcard_req;
		EContact *contact;

		vcard_req = (const gchar*) l->data;

		id = e_book_backend_file_create_unique_id ();

		string_to_dbt (id, &id_dbt);

		contact = e_contact_new_from_vcard_with_uid (vcard_req, id);

		rev = e_contact_get_const (contact, E_CONTACT_REV);
		if (!(rev && *rev))
			set_revision (contact);

		vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);

		string_to_dbt (vcard, &vcard_dbt);

		db_error = db->put (db, txn, &id_dbt, &vcard_dbt, 0);

		g_free (vcard);
		g_free (id);

		if (db_error == 0) {
			/* Contact was added successfully, add it to the return list */
			if (contacts != NULL)
				slist = g_slist_prepend (slist, contact);
		} else {
			/* Contact could not be added */
			g_warning (G_STRLOC ": db->put failed with %s", db_strerror (db_error));
			g_object_unref (contact);
			db_error_to_gerror (db_error, perror);

			/* Abort as soon as an error occurs */
			break;
		}
	}

	if (db_error == 0) {
		/* Commit transaction */
		db_error = txn->commit (txn, 0);
		if (db_error == 0) {
			/* Flush cache information to disk */
			if (db->sync (db, 0) != 0) {
				g_warning ("db->sync failed with %s", db_strerror (db_error));
			}
		} else {
			g_warning (G_STRLOC ": txn->commit failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
		}
	} else {
		/* Rollback transaction */
		txn->abort (txn);
	}

	if (db_error == 0) {
		if (contacts != NULL)
			*contacts = g_slist_reverse (slist);

		return TRUE;
	} else {
		if (contacts != NULL)
			*contacts = NULL;

		e_util_free_object_slist (slist);
		return FALSE;
	}
}

static void
e_book_backend_file_create_contacts (EBookBackendSync *backend,
                                     EDataBook *book,
                                     GCancellable *cancellable,
                                     const GSList *vcards,
                                     GSList **added_contacts,
                                     GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);

	if (do_create (bf, vcards, added_contacts, perror)) {
		GError *error = NULL;

		if (!e_book_backend_sqlitedb_add_contacts (bf->priv->sqlitedb,
							  SQLITEDB_FOLDER_ID,
							  *added_contacts, FALSE, &error)) {
			g_warning ("Failed to add contacts to summary: %s", error->message);
			g_error_free (error);
		}
	}
}

static void
e_book_backend_file_remove_contacts (EBookBackendSync *backend,
                                     EDataBook *book,
                                     GCancellable *cancellable,
                                     const GSList *id_list,
                                     GSList **ids,
                                     GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	DB               *db = bf->priv->file_db;
	DB_ENV           *env = bf->priv->env;
	DB_TXN           *txn = NULL;
	gint              db_error;
	GSList           *removed_ids = NULL;
	const GSList     *l;

	if (!db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return;
	}

	/* Begin transaction */
	db_error = env->txn_begin(env, NULL, &txn, 0);
	if (db_error != 0) {
		g_warning (G_STRLOC ": env->txn_begin failed with %s", db_strerror (db_error));
		db_error_to_gerror (db_error, perror);
		return;
	}

	for (l = id_list; l != NULL; l = l->next) {
		const gchar *id;
		DBT id_dbt;

		id = l->data;

		string_to_dbt (id, &id_dbt);

		db_error = db->del (db, txn, &id_dbt, 0);
		if (db_error != 0) {
			g_warning (G_STRLOC ": db->del failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
			/* Abort as soon as a removal fails */
			break;
		}

		removed_ids = g_slist_prepend (removed_ids, g_strdup (id));
	}

	if (db_error == 0) {
		/* Commit transaction */
		db_error = txn->commit (txn, 0);
		if (db_error == 0) {
			/* Flush cache information to disk */
			if (db->sync (db, 0) != 0) {
				g_warning ("db->sync failed with %s", db_strerror (db_error));
			}
		} else {
			g_warning (G_STRLOC ": txn->commit failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
		}
	} else {
		/* Rollback transaction */
		txn->abort (txn);
	}

	if (db_error == 0) {
		GError *error = NULL;
		/* Remove from summary as well */
		if (!e_book_backend_sqlitedb_remove_contacts (bf->priv->sqlitedb,
						      SQLITEDB_FOLDER_ID,
						      removed_ids, &error)) {
			g_warning ("Failed to remove contacts from the summary: %s", error->message);
			g_error_free (error);
		}

		*ids = removed_ids;
	} else {
		*ids = NULL;
		e_util_free_string_slist (removed_ids);
	}
}

static void
e_book_backend_file_modify_contacts (EBookBackendSync *backend,
                                     EDataBook *book,
                                     GCancellable *cancellable,
                                     const GSList *vcards,
                                     GSList **contacts,
                                     GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	DB               *db = bf->priv->file_db;
	DB_ENV           *env = bf->priv->env;
	DB_TXN           *txn = NULL;
	gint              db_error;
	const GSList     *l;
	GSList           *modified_contacts = NULL;
	GSList           *ids = NULL;

	if (!db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return;
	}

	/* Begin transaction */
	db_error = env->txn_begin(env, NULL, &txn, 0);
	if (db_error != 0) {
		g_warning (G_STRLOC ": env->txn_begin failed with %s", db_strerror (db_error));
		db_error_to_gerror (db_error, perror);
		return;
	}

	for (l = vcards; l != NULL; l = l->next) {
		gchar *id, *lookup_id;
		gchar *vcard_with_rev;
		DBT id_dbt, vcard_dbt;
		EContact *contact;

		contact = e_contact_new_from_vcard (l->data);
		id = e_contact_get (contact, E_CONTACT_UID);

		if (id == NULL) {
			g_propagate_error (perror, EDB_ERROR_EX (OTHER_ERROR, "No UID in the contact"));
			break;
		}

		/* update the revision (modified time of contact) */
		set_revision (contact);
		vcard_with_rev = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);

		/* This is disgusting, but for a time cards were added with
		 * ID's that are no longer used (they contained both the uri
		 * and the id.) If we recognize it as a uri (file:///...) trim
		 * off everything before the last '/', and use that as the
		 * id.*/
		if (!strncmp (id, "file:///", strlen ("file:///"))) {
			lookup_id = strrchr (id, '/') + 1;
		}
		else
			lookup_id = id;

		string_to_dbt (lookup_id, &id_dbt);

		/* Make sure the record to update already exists */
		memset (&vcard_dbt, 0, sizeof (vcard_dbt));
		vcard_dbt.flags = DB_DBT_MALLOC;
		db_error = db->get (db, txn, &id_dbt, &vcard_dbt, 0);
		if (db_error != 0) {
			g_warning (G_STRLOC ": db->get failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
			/* Abort as soon as a modification fails */
			break;
		}
		g_free (vcard_dbt.data);

		string_to_dbt (vcard_with_rev, &vcard_dbt);

		db_error = db->put (db, txn, &id_dbt, &vcard_dbt, 0);
		g_free (vcard_with_rev);

		if (db_error != 0) {
			g_warning (G_STRLOC ": db->put failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
			/* Abort as soon as a modification fails */
			break;
		}

		modified_contacts = g_slist_prepend (modified_contacts, contact);
		ids = g_slist_prepend (ids, id);
	}

	if (db_error == 0) {
		/* Commit transaction */
		db_error = txn->commit (txn, 0);
		if (db_error == 0) {
			/* Flush cache information to disk */
			if (db->sync (db, 0) != 0) {
				g_warning ("db->sync failed with %s", db_strerror (db_error));
			}
		} else {
			g_warning (G_STRLOC ": txn->commit failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
		}
	} else {
		/* Rollback transaction */
		txn->abort (txn);
	}

	if (db_error == 0) {
		GError *error = NULL;
		/* Update summary as well */
		if (!e_book_backend_sqlitedb_remove_contacts (bf->priv->sqlitedb,
					                      SQLITEDB_FOLDER_ID,
							      ids, &error)) {
			g_warning ("Failed to remove contacts from the summary: %s", error->message);
			g_error_free (error);
		} else if (!e_book_backend_sqlitedb_add_contacts (bf->priv->sqlitedb,
								  SQLITEDB_FOLDER_ID,
								  modified_contacts, FALSE, &error)) {
			g_warning ("Failed to add contacts to summary: %s", error->message);
			g_error_free (error);
		}

		*contacts = g_slist_reverse (modified_contacts);
	} else {
		*contacts = NULL;
		e_util_free_object_slist (modified_contacts);
	}

	e_util_free_string_slist (ids);
}

static void
e_book_backend_file_get_contact (EBookBackendSync *backend,
                                 EDataBook *book,
                                 GCancellable *cancellable,
                                 const gchar *id,
                                 gchar **vcard,
                                 GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);

	if (!bf || !bf->priv || !bf->priv->file_db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return;
	}

	*vcard = load_vcard (bf, id, perror);
}

static void
e_book_backend_file_get_contact_list (EBookBackendSync *backend,
                                      EDataBook *book,
                                      GCancellable *cancellable,
                                      const gchar *query,
                                      GSList **contacts,
                                      GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	DB             *db = bf->priv->file_db;
	DBC            *dbc;
	gint            db_error;
	DBT  id_dbt, vcard_dbt;
	EBookBackendSExp *card_sexp = NULL;
	gboolean search_needed;
	const gchar *search = query;
	GSList *contact_list = NULL, *l;
	GSList *summary_list = NULL;
	gboolean searched_summary = FALSE;
	gboolean with_all_required_fields = FALSE;

	d(printf ("e_book_backend_file_get_contact_list (%s)\n", search));

	if (!db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return;
	}

	summary_list = e_book_backend_sqlitedb_search (bf->priv->sqlitedb,
						       SQLITEDB_FOLDER_ID,
						       search, NULL,
						       &searched_summary,
						       &with_all_required_fields, NULL);

	if (summary_list) {

		for (l = summary_list; l; l = l->next) {
			EbSdbSearchData *data = l->data;

			if (with_all_required_fields) {
				contact_list = g_slist_prepend (contact_list, data->vcard);
				data->vcard  = NULL;
			} else {
				/* In this case the sqlitedb helped us with the query, but
				 * the return information is incomplete so we need to load it up.
				 */
				gchar *vcard;

				vcard = load_vcard (bf, data->uid, perror);

				/* Break out on the first BDB error */
				if (!vcard)
					break;

				contact_list = g_slist_prepend (contact_list, vcard);
			}
		}

		g_slist_foreach (summary_list, (GFunc) e_book_backend_sqlitedb_search_data_free, NULL);
		g_slist_free (summary_list);

	} else {
		search_needed = TRUE;
		if (!strcmp (search, "(contains \"x-evolution-any-field\" \"\")"))
			search_needed = FALSE;

		card_sexp = e_book_backend_sexp_new (search);
		if (!card_sexp) {
			g_propagate_error (perror, EDB_ERROR (INVALID_QUERY));
			return;
		}

		db_error = db->cursor (db, NULL, &dbc, 0);

		if (db_error != 0) {
			g_warning (G_STRLOC ": db->cursor failed with %s", db_strerror (db_error));
			/* XXX this needs to be some CouldNotOpen error */
			db_error_to_gerror (db_error, perror);
			return;
		}

		memset (&vcard_dbt, 0, sizeof (vcard_dbt));
		vcard_dbt.flags = DB_DBT_MALLOC;
		memset (&id_dbt, 0, sizeof (id_dbt));
		db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_FIRST);

		while (db_error == 0) {

			/* don't include the version in the list of cards */
			if (id_dbt.size != strlen (E_BOOK_BACKEND_FILE_VERSION_NAME) + 1
			    || strcmp (id_dbt.data, E_BOOK_BACKEND_FILE_VERSION_NAME)) {

				if ((!search_needed) || (card_sexp != NULL && e_book_backend_sexp_match_vcard  (card_sexp, vcard_dbt.data))) {
					contact_list = g_slist_prepend (contact_list, vcard_dbt.data);
				} else {
					free (vcard_dbt.data);
				}
			} else {
				free (vcard_dbt.data);
			}

			db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_NEXT);

		}
		g_object_unref (card_sexp);

		if (db_error == DB_NOTFOUND) {
			/* Success */
		} else {
			g_warning (G_STRLOC ": dbc->c_get failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
		}

		db_error = dbc->c_close (dbc);
		if (db_error != 0) {
			g_warning (G_STRLOC ": dbc->c_close failed with %s", db_strerror (db_error));
		}
	}

	*contacts = contact_list;
}

static void
e_book_backend_file_get_contact_list_uids (EBookBackendSync *backend,
                                           EDataBook *book,
                                           GCancellable *cancellable,
                                           const gchar *query,
                                           GSList **contacts_uids,
                                           GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	DB             *db = bf->priv->file_db;
	DBC            *dbc;
	gint            db_error;
	DBT  id_dbt, vcard_dbt;
	EBookBackendSExp *card_sexp = NULL;
	gboolean search_needed;
	const gchar *search = query;
	GSList *uids = NULL;
	gboolean searched = FALSE;

	d(printf ("e_book_backend_file_get_contact_list (%s)\n", search));

	if (!db) {
		g_propagate_error (perror, EDB_NOT_OPENED_ERROR);
		return;
	}

	uids = e_book_backend_sqlitedb_search_uids (bf->priv->sqlitedb,
						    SQLITEDB_FOLDER_ID,
						    search, &searched, NULL);

	if (!searched) {
		search_needed = TRUE;
		if (!strcmp (search, "(contains \"x-evolution-any-field\" \"\")"))
			search_needed = FALSE;

		card_sexp = e_book_backend_sexp_new (search);
		if (!card_sexp) {
			g_propagate_error (perror, EDB_ERROR (INVALID_QUERY));
			return;
		}

		db_error = db->cursor (db, NULL, &dbc, 0);

		if (db_error != 0) {
			g_warning (G_STRLOC ": db->cursor failed with %s", db_strerror (db_error));
			/* XXX this needs to be some CouldNotOpen error */
			db_error_to_gerror (db_error, perror);
			return;
		}

		memset (&vcard_dbt, 0, sizeof (vcard_dbt));
		vcard_dbt.flags = DB_DBT_MALLOC;
		memset (&id_dbt, 0, sizeof (id_dbt));
		db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_FIRST);

		while (db_error == 0) {

			/* don't include the version in the list of cards */
			if (id_dbt.size != strlen (E_BOOK_BACKEND_FILE_VERSION_NAME) + 1
			    || strcmp (id_dbt.data, E_BOOK_BACKEND_FILE_VERSION_NAME)) {

				if ((!search_needed) || (card_sexp != NULL && e_book_backend_sexp_match_vcard  (card_sexp, vcard_dbt.data))) {
					uids = g_slist_prepend (uids, g_strdup (id_dbt.data));
				}
			}

			g_free (vcard_dbt.data);

			db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_NEXT);

		}
		g_object_unref (card_sexp);

		if (db_error == DB_NOTFOUND) {
			/* Success */
		} else {
			g_warning (G_STRLOC ": dbc->c_get failed with %s", db_strerror (db_error));
			db_error_to_gerror (db_error, perror);
		}

		db_error = dbc->c_close (dbc);
		if (db_error != 0) {
			g_warning (G_STRLOC ": dbc->c_close failed with %s", db_strerror (db_error));
		}
	}

	*contacts_uids = g_slist_reverse (uids);
}

typedef struct {
	EBookBackendFile *bf;
	GThread *thread;
	EFlag *running;
} FileBackendSearchClosure;

static void
closure_destroy (FileBackendSearchClosure *closure)
{
	d(printf ("destroying search closure\n"));
	e_flag_free (closure->running);
	g_free (closure);
}

static FileBackendSearchClosure *
init_closure (EDataBookView *book_view,
              EBookBackendFile *bf)
{
	FileBackendSearchClosure *closure = g_new (FileBackendSearchClosure, 1);

	closure->bf = bf;
	closure->thread = NULL;
	closure->running = e_flag_new ();

	g_object_set_data_full (G_OBJECT (book_view), "EBookBackendFile.BookView::closure",
				closure, (GDestroyNotify) closure_destroy);

	return closure;
}

static FileBackendSearchClosure *
get_closure (EDataBookView *book_view)
{
	return g_object_get_data (G_OBJECT (book_view), "EBookBackendFile.BookView::closure");
}

static void
notify_update_vcard (EDataBookView *book_view,
                     gboolean prefiltered,
                     const gchar *id,
                     const gchar *vcard)
{
	if (prefiltered)
		e_data_book_view_notify_update_prefiltered_vcard (book_view, id, vcard);
	else
		e_data_book_view_notify_update_vcard (book_view, id, vcard);
}

static gpointer
book_view_thread (gpointer data)
{
	EDataBookView *book_view;
	FileBackendSearchClosure *closure;
	EBookBackendFile *bf;
	const gchar *query;
	DB  *db;
	DBT id_dbt, vcard_dbt;
	gint db_error;
	gboolean allcontacts;
	GSList *summary_list, *l;
	GHashTable *fields_of_interest;
	gboolean searched = FALSE;
	gboolean with_all_required_fields = FALSE;

	g_return_val_if_fail (E_IS_DATA_BOOK_VIEW (data), NULL);

	book_view = data;
	closure = get_closure (book_view);
	if (!closure) {
		g_warning (G_STRLOC ": NULL closure in book view thread");
		return NULL;
	}
	bf = closure->bf;

	d(printf ("starting initial population of book view\n"));

	/* ref the book view because it'll be removed and unrefed
	 * when/if it's stopped */
	e_data_book_view_ref (book_view);

	db                 = bf->priv->file_db;
	query              = e_data_book_view_get_card_query (book_view);
	fields_of_interest = e_data_book_view_get_fields_of_interest (book_view);

	if (!db) {
		e_data_book_view_notify_complete (book_view, EDB_NOT_OPENED_ERROR);
		e_data_book_view_unref (book_view);
		return NULL;
	}

	if ( !strcmp (query, "(contains \"x-evolution-any-field\" \"\")")) {
		e_data_book_view_notify_progress (book_view, -1, _("Loading..."));
		allcontacts = TRUE;
	} else {
		e_data_book_view_notify_progress (book_view, -1, _("Searching..."));
		allcontacts = FALSE;
	}

	d(printf ("signalling parent thread\n"));
	e_flag_set (closure->running);

	summary_list = e_book_backend_sqlitedb_search (bf->priv->sqlitedb,
						       SQLITEDB_FOLDER_ID,
						       query, fields_of_interest,
						       &searched, &with_all_required_fields, NULL);

	if (searched) {

		for (l = summary_list; l; l = l->next) {
			EbSdbSearchData *data = l->data;
			gchar *vcard = NULL;

			if (with_all_required_fields) {
				vcard = data->vcard;
				data->vcard = NULL;
			} else {
				GError *error = NULL;

				/* The sqlitedb summary did not satisfy 'fields-of-interest',
				 * load the complete vcard here. */
				vcard = load_vcard (bf, data->uid, &error);

				if (error) {
					g_warning ("Error loading contact %s: %s",
						   data->uid, error->message);
					g_error_free (error);
				}

				if (!vcard)
					continue;

			}

			notify_update_vcard (book_view, TRUE, data->uid, vcard);
			g_free (vcard);
		}

		g_slist_foreach (summary_list, (GFunc) e_book_backend_sqlitedb_search_data_free, NULL);
		g_slist_free (summary_list);
	} else {
		/* iterate over the db and do the query there */
		DBC    *dbc;

		memset (&id_dbt, 0, sizeof (id_dbt));
		memset (&vcard_dbt, 0, sizeof (vcard_dbt));
		vcard_dbt.flags = DB_DBT_MALLOC;

		db_error = db->cursor (db, NULL, &dbc, 0);
		if (db_error == 0) {

			db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_FIRST);
			while (db_error == 0) {

				if (!e_flag_is_set (closure->running))
					break;

				/* don't include the version in the list of cards */
				if (strcmp (id_dbt.data, E_BOOK_BACKEND_FILE_VERSION_NAME)) {
					notify_update_vcard (book_view, allcontacts,
							     id_dbt.data, vcard_dbt.data);
				}

				g_free (vcard_dbt.data);
				db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_NEXT);
			}

			dbc->c_close (dbc);
			if (db_error && db_error != DB_NOTFOUND)
				g_warning ("e_book_backend_file_search: error building list: %s",
					   db_strerror (db_error));
		}
		else if (db_error == DB_RUNRECOVERY) {
			g_warning ("e_book_backend_file_search: error getting the cursor for %s",
				   bf->priv->filename);
			abort ();
		}

	}

	if (e_flag_is_set (closure->running))
		e_data_book_view_notify_complete (book_view, NULL /* Success */);

	e_data_book_view_unref (book_view);

	d(printf ("finished population of book view\n"));

	return NULL;
}

static void
e_book_backend_file_start_book_view (EBookBackend *backend,
                                     EDataBookView *book_view)
{
	FileBackendSearchClosure *closure = init_closure (book_view, E_BOOK_BACKEND_FILE (backend));

	d(printf ("starting book view thread\n"));
	closure->thread = g_thread_create (book_view_thread, book_view, TRUE, NULL);

	e_flag_wait (closure->running);

	/* at this point we know the book view thread is actually running */
	d(printf ("returning from start_book_view\n"));
}

static void
e_book_backend_file_stop_book_view (EBookBackend *backend,
                                    EDataBookView *book_view)
{
	FileBackendSearchClosure *closure = get_closure (book_view);
	gboolean need_join;

	if (!closure)
		return;

	d(printf ("stopping query\n"));
	need_join = e_flag_is_set (closure->running);
	e_flag_clear (closure->running);

	if (need_join)
		g_thread_join (closure->thread);
}

static gchar *
e_book_backend_file_extract_path_from_source (ESource *source)
{
	gchar *filename = NULL;
	const gchar *absolute_uri;

	absolute_uri = e_source_peek_absolute_uri (source);

	if (absolute_uri && g_str_has_prefix (absolute_uri, "local://")) {
		gchar *uri;

		uri = g_strconcat ("file://", absolute_uri + 8, NULL);
		filename = g_filename_from_uri (uri, NULL, NULL);
		g_free (uri);

		if (!g_file_test (filename, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_DIR)) {
			g_free (filename);
			filename = NULL;
		}
	}

	if (!filename) {
		const gchar *user_data_dir;
		const gchar *source_dir;
		gchar *mangled_source_dir;

		user_data_dir = e_get_user_data_dir ();
		source_dir = e_source_peek_relative_uri (source);

		if (!source_dir || !g_str_equal (source_dir, "system"))
			source_dir = e_source_peek_uid (source);

		/* Mangle the URI to not contain invalid characters. */
		mangled_source_dir = g_strdelimit (g_strdup (source_dir), ":/", '_');

		filename = g_build_filename (
			user_data_dir, "addressbook", mangled_source_dir, NULL);

		g_free (mangled_source_dir);
	}

	return filename;
}

static void
e_book_backend_file_authenticate_user (EBookBackendSync *backend,
                                       GCancellable *cancellable,
                                       ECredentials *credentials,
                                       GError **perror)
{
	/* Success */
}

/*
** versions:
**
** 0.0 just a list of cards
**
** 0.1 same as 0.0, but with the version tag
**
** 0.2 not a real format upgrade, just a hack to fix broken ids caused
**     by a bug in early betas, but we only need to convert them if
**     the previous version is 0.1, since the bug existed after 0.1
**     came about.
*/
static gboolean
e_book_backend_file_upgrade_db (EBookBackendFile *bf,
                                gchar *old_version)
{
	DB  *db = bf->priv->file_db;
	gint db_error;
	DBT version_name_dbt, version_dbt;

	if (!db) {
		g_warning (G_STRLOC ": No DB opened");
		return FALSE;
	}

	if (strcmp (old_version, "0.0")
	    && strcmp (old_version, "0.1")) {
		g_warning ("unsupported version '%s' found in PAS backend file\n",
			   old_version);
		return FALSE;
	}

	if (!strcmp (old_version, "0.1")) {
		/* we just loop through all the cards in the db,
		 * giving them valid ids if they don't have them */
		DBT  id_dbt, vcard_dbt;
		DBC *dbc;
		gint  card_failed = 0;

		db_error = db->cursor (db, NULL, &dbc, 0);
		if (db_error != 0) {
			g_warning (G_STRLOC ": db->cursor failed with %s", db_strerror (db_error));
			return FALSE;
		}

		memset (&id_dbt, 0, sizeof (id_dbt));
		memset (&vcard_dbt, 0, sizeof (vcard_dbt));

		db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_FIRST);

		while (db_error == 0) {
			if (id_dbt.size != strlen (E_BOOK_BACKEND_FILE_VERSION_NAME) + 1
			    || strcmp (id_dbt.data, E_BOOK_BACKEND_FILE_VERSION_NAME)) {
				EContact *contact;

				contact = create_contact (id_dbt.data, vcard_dbt.data);

				/* the cards we're looking for are
				 * created with a normal id dbt, but
				 * with the id field in the vcard set
				 * to something that doesn't match.
				 * so, we need to modify the card to
				 * have the same id as the the dbt. */
				if (strcmp (id_dbt.data, e_contact_get_const (contact, E_CONTACT_UID))) {
					gchar *vcard;

					e_contact_set (contact, E_CONTACT_UID, id_dbt.data);

					vcard = e_vcard_to_string (E_VCARD (contact), EVC_FORMAT_VCARD_30);
					string_to_dbt (vcard, &vcard_dbt);

					db_error = db->put (db, NULL,
							    &id_dbt, &vcard_dbt, 0);

					g_free (vcard);

					if (db_error != 0)
						card_failed++;
				}

				g_object_unref (contact);
			}

			db_error = dbc->c_get (dbc, &id_dbt, &vcard_dbt, DB_NEXT);
		}

		dbc->c_close (dbc);

		if (card_failed) {
			g_warning ("failed to update %d cards", card_failed);
			return FALSE;
		}
	}

	string_to_dbt (E_BOOK_BACKEND_FILE_VERSION_NAME, &version_name_dbt);
	string_to_dbt (E_BOOK_BACKEND_FILE_VERSION, &version_dbt);

	db_error = db->put (db, NULL, &version_name_dbt, &version_dbt, 0);
	if (db_error == 0)
		return TRUE;
	else
		return FALSE;
}

static gboolean
e_book_backend_file_maybe_upgrade_db (EBookBackendFile *bf)
{
	DB   *db = bf->priv->file_db;
	DBT  version_name_dbt, version_dbt;
	gint  db_error;
	gchar *version;
	gboolean ret_val = TRUE;

	if (!db) {
		g_warning (G_STRLOC ": No DB opened");
		return FALSE;
	}

	string_to_dbt (E_BOOK_BACKEND_FILE_VERSION_NAME, &version_name_dbt);
	memset (&version_dbt, 0, sizeof (version_dbt));
	version_dbt.flags = DB_DBT_MALLOC;

	db_error = db->get (db, NULL, &version_name_dbt, &version_dbt, 0);
	if (db_error == 0) {
		/* success */
		version = version_dbt.data;
	}
	else {
		/* key was not in file */
		version = g_strdup ("0.0");
	}

	if (strcmp (version, E_BOOK_BACKEND_FILE_VERSION))
		ret_val = e_book_backend_file_upgrade_db (bf, version);

	g_free (version);

	return ret_val;
}

#ifdef CREATE_DEFAULT_VCARD
# include <libedata-book/ximian-vcard.h>
#endif

static void
#if DB_VERSION_MAJOR == 4 && DB_VERSION_MINOR >= 3
file_errcall (const DB_ENV *env,
              const gchar *buf1,
              const gchar *buf2)
#else
file_errcall (const gchar *buf1,
              gchar *buf2)
#endif
{
	g_warning ("libdb error: %s", buf2);
}

static void
e_book_backend_file_open (EBookBackendSync *backend,
                          EDataBook *book,
                          GCancellable *cancellable,
                          gboolean only_if_exists,
                          GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	gchar            *dirname, *filename;
	gboolean          readonly = TRUE;
	ESource          *source;
	gint              db_error;
	DB               *db;
	DB_ENV           *env;
	GError           *local_error = NULL;
	global_env       *genv = NULL;

#ifdef CREATE_DEFAULT_VCARD
	gboolean create_default_vcard = FALSE;
#endif

	source = e_backend_get_source (E_BACKEND (backend));
	dirname = e_book_backend_file_extract_path_from_source (source);
	filename = g_build_filename (dirname, "addressbook.db", NULL);

	db_error = e_db3_utils_maybe_recover (filename);
	if (db_error != 0) {
		g_warning ("db recovery failed with %s", db_strerror (db_error));
		g_free (dirname);
		g_free (filename);
		db_error_to_gerror (db_error, perror);
		return;
	}

	G_LOCK (db_environments);
	if (db_environments) {
		genv = g_hash_table_lookup (db_environments, dirname);
	}
	if (genv && genv->ref_count > 0) {
		genv->ref_count++;
		env = genv->env;
	} else {
		db_error = db_env_create (&env, 0);
		if (db_error != 0) {
			g_warning ("db_env_create failed with %s", db_strerror (db_error));
			G_UNLOCK (db_environments);
			g_free (dirname);
			g_free (filename);
			db_error_to_gerror (db_error, perror);
			return;
		}

		env->set_errcall (env, file_errcall);

		/* Set the allocation routines to the non-aborting GLib functions */
		env->set_alloc (env, (gpointer (*)(gsize)) g_try_malloc,
				(gpointer (*)(gpointer , gsize)) g_try_realloc,
				g_free);

		/* Make sure the database directory is created
		   or env->open will fail */
		if (!only_if_exists) {
			if (!create_directory (dirname, perror)) {
				g_warning ("failed to create directory at %s", dirname);
				G_UNLOCK (db_environments);
				g_free (dirname);
				g_free (filename);
				return;
			}
		}

		/*
		 * DB_INIT_TXN enables transaction support. It requires DB_INIT_LOCK to
		 * initialize the locking subsystem and DB_INIT_LOG for the logging
		 * subsystem.
		 *
		 * DB_INIT_MPOOL enables the in-memory cache.
		 *
		 * Note that we need either DB_INIT_CDB or DB_INIT_LOCK, because we will
		 * have multiple threads reading and writing concurrently without
		 * any locking above libdb. Right now DB_INIT_LOCK is used because
		 * DB_INIT_TXN conflicts with DB_INIT_CDB.
		 */
		db_error = (*env->open) (env, dirname, DB_INIT_LOCK | DB_INIT_TXN | DB_INIT_LOG | DB_CREATE | DB_INIT_MPOOL | DB_PRIVATE | DB_THREAD, 0);
		if (db_error != 0) {
			env->close (env, 0);
			g_warning ("db_env_open failed with %s", db_strerror (db_error));
			G_UNLOCK (db_environments);
			g_free (dirname);
			g_free (filename);
			db_error_to_gerror (db_error, perror);
			return;
		}

		/* Insert in the db_environments hash table */
		if (!db_environments) {
			db_environments = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
		}
		genv = g_malloc0 (sizeof (global_env));
		genv->ref_count = 1;
		genv->env = env;
		g_hash_table_insert (db_environments, g_strdup (dirname), genv);
	}
	G_UNLOCK (db_environments);

	bf->priv->env = env;

	db_error = db_create (&db, env, 0);
	if (db_error != 0) {
		g_warning ("db_create failed with %s", db_strerror (db_error));
		g_free (dirname);
		g_free (filename);
		db_error_to_gerror (db_error, perror);
		return;
	}

	db_error = (*db->open) (db, NULL, filename, NULL, DB_HASH, DB_THREAD | DB_AUTO_COMMIT, 0666);

	if (db_error == DB_OLD_VERSION) {
		db_error = e_db3_utils_upgrade_format (filename);

		if (db_error != 0) {
			g_warning ("db format upgrade failed with %s", db_strerror (db_error));
			g_free (dirname);
			g_free (filename);
			db_error_to_gerror (db_error, perror);
			return;
		}

		db->close (db, 0);
		db_error = db_create (&db, env, 0);
		if (db_error != 0) {
			g_warning ("db_create failed with %s", db_strerror (db_error));
			g_free (dirname);
			g_free (filename);
			db_error_to_gerror (db_error, perror);
			return;
		}

		db_error = (*db->open) (db, NULL, filename, NULL, DB_HASH, DB_THREAD | DB_AUTO_COMMIT, 0666);
	}

	if (db_error == 0) {
		readonly = FALSE;
	} else {
		db->close (db, 0);
		db_error = db_create (&db, env, 0);
		if (db_error != 0) {
			g_warning ("db_create failed with %s", db_strerror (db_error));
			g_free (dirname);
			g_free (filename);
			db_error_to_gerror (db_error, perror);
			return;
		}

		db_error = (*db->open) (db, NULL, filename, NULL, DB_HASH, DB_RDONLY | DB_THREAD | DB_AUTO_COMMIT, 0666);

		if (db_error != 0 && !only_if_exists) {

			/* the database didn't exist, so we create the
			 * directory then the .db */
			db->close (db, 0);

			if (!create_directory (dirname, perror)) {
				g_free (dirname);
				g_free (filename);
				return;
			}

			db_error = db_create (&db, env, 0);
			if (db_error != 0) {
				g_warning ("db_create failed with %s", db_strerror (db_error));
				g_free (dirname);
				g_free (filename);
				db_error_to_gerror (db_error, perror);
				return;
			}

			db_error = (*db->open) (db, NULL, filename, NULL, DB_HASH, DB_CREATE | DB_THREAD | DB_AUTO_COMMIT, 0666);
			if (db_error != 0) {
				db->close (db, 0);
				g_warning ("db->open (... %s ... DB_CREATE ...) failed with %s", filename, db_strerror (db_error));
			}
			else {
#ifdef CREATE_DEFAULT_VCARD
				create_default_vcard = TRUE;
#endif

				readonly = FALSE;
			}
		}
	}

	bf->priv->file_db = db;

	if (db_error != 0) {
		bf->priv->file_db = NULL;
		g_free (dirname);
		g_free (filename);
		db_error_to_gerror (db_error, perror);
		return;
	}

#ifdef CREATE_DEFAULT_VCARD
	if (create_default_vcard) {
		GSList l;
		l.data = XIMIAN_VCARD;
		l.next = NULL;

		if (!do_create (bf, &l, NULL, NULL))
			g_warning ("Cannot create default contact");
	}
#endif

	if (!e_book_backend_file_maybe_upgrade_db (bf)) {
		db->close (db, 0);
		bf->priv->file_db = NULL;
		g_free (dirname);
		g_free (filename);
		g_propagate_error (perror, EDB_ERROR_EX (OTHER_ERROR, "e_book_backend_file_maybe_upgrade_db failed"));
		return;
	}

	g_free (bf->priv->dirname);
	g_free (bf->priv->filename);
	bf->priv->dirname = dirname;
	bf->priv->filename = filename;

	bf->priv->sqlitedb = e_book_backend_sqlitedb_new (bf->priv->dirname,
							  SQLITEDB_EMAIL_ID,
							  SQLITEDB_FOLDER_ID,
							  SQLITEDB_FOLDER_NAME,
							  FALSE,
							  perror);
	if (!bf->priv->sqlitedb)
		return;

	if (!e_book_backend_sqlitedb_get_is_populated (bf->priv->sqlitedb,
						       SQLITEDB_FOLDER_ID,
						       &local_error)) {
		if (local_error) {
			g_propagate_error (perror, local_error);
			return;
		} else if (!build_sqlitedb (bf->priv)) {
			g_propagate_error (perror, e_data_book_create_error_fmt (E_DATA_BOOK_STATUS_OTHER_ERROR,
                                    "Failed to build summary for an address book %s",
				     bf->priv->filename));
		}
	}

	e_book_backend_notify_online (E_BOOK_BACKEND (backend), TRUE);
	e_book_backend_notify_readonly (E_BOOK_BACKEND (backend), readonly);
	e_book_backend_notify_opened (E_BOOK_BACKEND (backend), NULL /* Success */);
}

static gboolean
select_changes (const gchar *name)
{
	gchar *p;

	if (strlen (name) < strlen (CHANGES_DB_SUFFIX))
		return FALSE;

	p = strstr (name, CHANGES_DB_SUFFIX);
	if (!p)
		return FALSE;

	if (strlen (p) != strlen (CHANGES_DB_SUFFIX))
		return FALSE;

	return TRUE;
}

static void
e_book_backend_file_remove (EBookBackendSync *backend,
                            EDataBook *book,
                            GCancellable *cancellable,
                            GError **perror)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	GDir *dir;

	if (!remove_file (bf->priv->filename, perror))
		return;

	if (!e_book_backend_sqlitedb_remove (bf->priv->sqlitedb, perror))
		return;

	/* unref the sqlitedb before we remove the file so it's not written out again */
	g_object_unref (bf->priv->sqlitedb);
	bf->priv->sqlitedb = NULL;

	dir = g_dir_open (bf->priv->dirname, 0, NULL);
	if (dir) {
		const gchar *name;

		while ((name = g_dir_read_name (dir))) {
			if (select_changes (name)) {
				gchar *full_path = g_build_filename (bf->priv->dirname, name, NULL);
				if (-1 == g_unlink (full_path)) {
					g_warning ("failed to remove change db `%s': %s", full_path, g_strerror (errno));
				}
				g_free (full_path);
			}
		}

		g_dir_close (dir);
	}

	if (-1 == g_rmdir (bf->priv->dirname))
		g_warning ("failed to remove directory `%s`: %s", bf->priv->dirname, g_strerror (errno));

	/* we may not have actually succeeded in removing the
	 * backend's files/dirs, but there's nothing we can do about
	 * it here..  the only time we should return failure is if we
	 * failed to remove the actual data.  a failure should mean
	 * that the addressbook is still valid */
}

static gboolean
e_book_backend_file_get_backend_property (EBookBackendSync *backend,
                                          EDataBook *book,
                                          GCancellable *cancellable,
                                          const gchar *prop_name,
                                          gchar **prop_value,
                                          GError **error)
{
	gboolean processed = TRUE;

	g_return_val_if_fail (prop_name != NULL, FALSE);
	g_return_val_if_fail (prop_value != NULL, FALSE);

	if (g_str_equal (prop_name, CLIENT_BACKEND_PROPERTY_CAPABILITIES)) {
		*prop_value = g_strdup ("local,do-initial-query,bulk-adds,bulk-modifies,bulk-removes,contact-lists");
	} else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_REQUIRED_FIELDS)) {
		*prop_value = g_strdup (e_contact_field_name (E_CONTACT_FILE_AS));
	} else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_FIELDS)) {
		GSList *fields = NULL;
		gint i;

		/* XXX we need a way to say "we support everything", since the
		 * file backend does */
		for (i = 1; i < E_CONTACT_FIELD_LAST; i++)
			fields = g_slist_append (fields, (gpointer) e_contact_field_name (i));

		*prop_value = e_data_book_string_slist_to_comma_string (fields);
		g_slist_free (fields);
	} else if (g_str_equal (prop_name, BOOK_BACKEND_PROPERTY_SUPPORTED_AUTH_METHODS)) {
		*prop_value = NULL;
	} else {
		processed = FALSE;
	}

	return processed;
}

static void
e_book_backend_file_notify_online_cb (EBookBackend *backend,
                                      GParamSpec *pspec)
{
	if (e_book_backend_is_opened (backend))
		e_book_backend_notify_online (backend, TRUE);
}

static void
e_book_backend_file_sync (EBookBackend *backend)
{
	EBookBackendFile *bf = E_BOOK_BACKEND_FILE (backend);
	gint db_error;

	g_return_if_fail (bf != NULL);

	if (bf->priv->file_db) {
		db_error = bf->priv->file_db->sync (bf->priv->file_db, 0);
		if (db_error != 0)
			g_warning (G_STRLOC ": db->sync failed with %s", db_strerror (db_error));
	}
}

typedef struct {
	EContact         *contact;
	EBookBackendFile *bf;
} NotifyData;

static gboolean
view_notify_update (EDataBookView *view,
                    gpointer data)
{
	NotifyData *ndata    = data;
	GHashTable *fields   = e_data_book_view_get_fields_of_interest (view);
	gboolean    notified = FALSE;
	gboolean    with_all_required_fields = FALSE;

	if (e_book_backend_sqlitedb_is_summary_query (e_data_book_view_get_card_query (view)) &&
	    e_book_backend_sqlitedb_is_summary_fields (fields)) {

		const gchar *uid = e_contact_get_const (ndata->contact, E_CONTACT_UID);
		gchar       *vcard;

		vcard = e_book_backend_sqlitedb_get_vcard_string (ndata->bf->priv->sqlitedb,
								  SQLITEDB_FOLDER_ID, uid,
								  fields, &with_all_required_fields, NULL);

		if (vcard) {
			if (with_all_required_fields) {
				e_data_book_view_notify_update_prefiltered_vcard (view, uid, vcard);
				notified = TRUE;
			}
			g_free (vcard);
		}
	}

	if (!notified)
		e_data_book_view_notify_update (view, ndata->contact);

	return TRUE;
}

static void
e_book_backend_file_notify_update (EBookBackend *backend,
                                   const EContact *contact)
{
	NotifyData data = { (EContact *) contact, E_BOOK_BACKEND_FILE (backend) };

	e_book_backend_foreach_view (backend, view_notify_update, &data);
}

static void
e_book_backend_file_dispose (GObject *object)
{
	EBookBackendFile *bf;
	global_env *genv;

	bf = E_BOOK_BACKEND_FILE (object);

	if (bf->priv->file_db) {
		bf->priv->file_db->close (bf->priv->file_db, 0);
		bf->priv->file_db = NULL;
	}

	G_LOCK (db_environments);
	genv = g_hash_table_lookup (db_environments, bf->priv->dirname);
	if (genv) {
		genv->ref_count--;
		if (genv->ref_count == 0) {
			genv->env->close (genv->env, 0);
			g_free (genv);
			g_hash_table_remove (db_environments, bf->priv->dirname);
		}
		if (g_hash_table_size (db_environments) == 0) {
			g_hash_table_destroy (db_environments);
			db_environments = NULL;
		}
	}
	G_UNLOCK (db_environments);

	if (bf->priv->sqlitedb) {
		g_object_unref (bf->priv->sqlitedb);
		bf->priv->sqlitedb = NULL;
	}

	G_OBJECT_CLASS (e_book_backend_file_parent_class)->dispose (object);
}

static void
e_book_backend_file_finalize (GObject *object)
{
	EBookBackendFile *bf;

	bf = E_BOOK_BACKEND_FILE (object);

	g_free (bf->priv->filename);
	g_free (bf->priv->dirname);

	g_free (bf->priv);

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

#ifdef G_OS_WIN32
/* Avoid compiler warning by providing a function with exactly the
 * prototype that db_env_set_func_open() wants for the open method.
 */

static gint
my_open (const gchar *name,
         gint oflag,
         ...)
{
	gint mode = 0;

	if (oflag & O_CREAT) {
		va_list arg;
		va_start (arg, oflag);
		mode = va_arg (arg, gint);
		va_end (arg);
	}

	return g_open (name, oflag, mode);
}

gint
my_rename (const gchar *oldname,
           const gchar *newname)
{
	return g_rename (oldname, newname);
}

gint
my_exists (const gchar *name,
           gint *isdirp)
{
	if (!g_file_test (name, G_FILE_TEST_EXISTS))
		return ENOENT;
	if (isdirp != NULL)
		*isdirp = g_file_test (name, G_FILE_TEST_IS_DIR);
	return 0;
}

gint
my_unlink (const gchar *name)
{
	return g_unlink (name);
}

#endif

static void
e_book_backend_file_class_init (EBookBackendFileClass *klass)
{
	GObjectClass    *object_class = G_OBJECT_CLASS (klass);
	EBookBackendSyncClass *sync_class;
	EBookBackendClass *backend_class;

	sync_class = E_BOOK_BACKEND_SYNC_CLASS (klass);
	backend_class = E_BOOK_BACKEND_CLASS (klass);

	/* Set the virtual methods. */
	backend_class->start_book_view		= e_book_backend_file_start_book_view;
	backend_class->stop_book_view		= e_book_backend_file_stop_book_view;
	backend_class->sync			= e_book_backend_file_sync;
	backend_class->notify_update            = e_book_backend_file_notify_update;

	sync_class->open_sync			= e_book_backend_file_open;
	sync_class->remove_sync			= e_book_backend_file_remove;
	sync_class->get_backend_property_sync	= e_book_backend_file_get_backend_property;
	sync_class->create_contacts_sync	= e_book_backend_file_create_contacts;
	sync_class->remove_contacts_sync	= e_book_backend_file_remove_contacts;
	sync_class->modify_contacts_sync	= e_book_backend_file_modify_contacts;
	sync_class->get_contact_sync		= e_book_backend_file_get_contact;
	sync_class->get_contact_list_sync	= e_book_backend_file_get_contact_list;
	sync_class->get_contact_list_uids_sync	= e_book_backend_file_get_contact_list_uids;
	sync_class->authenticate_user_sync	= e_book_backend_file_authenticate_user;

	object_class->dispose = e_book_backend_file_dispose;
	object_class->finalize = e_book_backend_file_finalize;

#ifdef G_OS_WIN32
	/* Use the gstdio wrappers to open, check, rename and unlink
	 * files from libdb.
	 */
	db_env_set_func_open (my_open);
	db_env_set_func_close (close);
	db_env_set_func_exists (my_exists);
	db_env_set_func_rename (my_rename);
	db_env_set_func_unlink (my_unlink);
#endif
}

static void
e_book_backend_file_init (EBookBackendFile *backend)
{
	EBookBackendFilePrivate *priv;

	priv             = g_new0 (EBookBackendFilePrivate, 1);

	backend->priv = priv;

	g_signal_connect (
		backend, "notify::online",
		G_CALLBACK (e_book_backend_file_notify_online_cb), NULL);
}

