/*
 * libopensync - A synchronization framework
 * Copyright (C) 2004-2005  Armin Bauer <armin.bauer@opensync.org>
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 * 
 */
 
#include "opensync.h"
#include "opensync_internals.h"
#include "opensync_hashtable_internals.h"

#include "opensync-data.h"
#include "opensync-helper.h"
#include "opensync-db.h"
#include <db/opensync_db_internals.h>


/**
 * @defgroup OSyncHashtableAPI OpenSync Hashtables
 * @ingroup OSyncPublic
 * @brief A Hashtable can be used to detect changes
 * 
 * Hashtables can be used to detect changes since the last invocation. They do this
 * by keeping track of all reported uids and the hashes of the objects.
 * 
 * A hash is a string that changes when an object is updated or when the content of
 * the object changes. So hashes can either be a real hash like an MD5, or something 
 * like a timestamp. The only important thing is that the hash changes when the item
 * gets updated.
 * 
 * The hashtable is created or loaded from a .db file using the osync_hashtable_new()
 * function.
 * 
 * Now you can query and alter the table. You can ask if a item has changed by doing:
 * - osync_hashtable_get_changetype() to get the changetype of a certain uid and hash
 * - or the convience function osync_hashtable_detect_change() which calls 
 * osync_hashtable_get_changetype() and sets this changetype on the change object and then
 * automatically calls osync_hashtable_report()
 * After you reported all objects you can query the table for the deleted objects using
 * osync_hashtable_get_deleted() or osync_hashtable_report_deleted()
 * 
 * After you are finished using the hashtable, call:
 * - osync_hashtable_free()
 * 
 * The hashtable works like this:
 * 
 * First the items are reported with a certain uid or hash. If the uid does not yet
 * exist in the database it is reported as ADDED. If the uid exists and the hash is different
 * it is reported as MODIFIED. If the uid exists but the hash is the same it means that the
 * object is UNMODIFIED.
 * 
 * To be able to report deleted objects the hashtables keeps track of the uids you reported.
 * After you are done with asking the hashtable for changes you can ask it for deleted objects.
 * All items that are in the hashtable but where not reported by you have to be DELETED.
 * 
 */
/*@{*/

osync_bool osync_hashtable_create(OSyncHashTable *table, const char *objtype, OSyncError **error)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %s, %p)", __func__, table, objtype, error);

	char *query = g_strdup_printf("CREATE TABLE tbl_hash_%s (id INTEGER PRIMARY KEY, uid VARCHAR UNIQUE, hash VARCHAR)", objtype);
	if (!osync_db_query(table->dbhandle, query, error)) {
		osync_trace(TRACE_EXIT_ERROR, "%s: %s", __func__, osync_error_print(error));
		g_free(query);
		return FALSE;
	}

	g_free(query);
	osync_trace(TRACE_EXIT, "%s", __func__);
	return TRUE;
}

/*! @brief Loads or creates a hashtable
 * 
 * Hashtables can be used to detect what has been changed since
 * the last sync
 * 
 * @param path the full path and file name of the hashtable .db file to load from or create
 * @param objtype the object type of the hashtable
 * @param error An error struct
 * @returns A new hashtable, or NULL if an error occurred.
 * 
 */
OSyncHashTable *osync_hashtable_new(const char *path, const char *objtype, OSyncError **error)
{
	osync_trace(TRACE_ENTRY, "%s(%s, %s, %p)", __func__, path, objtype, error);
	
	OSyncHashTable *table = osync_try_malloc0(sizeof(OSyncHashTable), error);
	if (!table) {
		osync_trace(TRACE_EXIT_ERROR, "%s: %s", __func__, osync_error_print(error));
		return NULL;
	}
	
	table->used_entries = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);

	table->dbhandle = osync_db_new(error);
	if (!table->dbhandle)
		goto error;

	if (!osync_db_open(table->dbhandle, path, error))
		goto error_and_free;

	table->tablename = g_strdup_printf("tbl_hash_%s", objtype);

	int ret = osync_db_exists(table->dbhandle, table->tablename, error);
	if (ret > 0) {
		goto end;
	} else if (ret < 0) {
		goto error_and_free;
	}
	/* if ret == 0 then table does not exist yet. contiune and create one. */

	if (!osync_hashtable_create(table, objtype, error))
		goto error_and_free;

end:
	osync_trace(TRACE_EXIT, "%s: %p", __func__, table);
	return table;

error_and_free:	
	g_free(table->dbhandle);
	g_free(table);
error:	
	osync_trace(TRACE_EXIT_ERROR, "%s: %s", __func__, osync_error_print(error));
	return FALSE;

}

/*! @brief Frees a hashtable
 * 
 * @param table The hashtable to free
 * 
 */
void osync_hashtable_free(OSyncHashTable *table)
{
	osync_trace(TRACE_ENTRY, "%s(%p)", __func__, table);
	osync_assert(table);
	
	if (!osync_db_close(table->dbhandle, NULL))
		osync_trace(TRACE_INTERNAL, "Can't close database");

		
	g_hash_table_destroy(table->used_entries);

	g_free(table->tablename);
	g_free(table->dbhandle);
	g_free(table);
	
	osync_trace(TRACE_EXIT, "%s", __func__);
}

/*! @brief Makes a hashtable forget
 * 
 * You can ask the hashtable to detect the changes. In the end you can
 * ask the hashtable for all items that have been deleted since the last sync.
 * For this the hashtable maintains a internal table of items you already reported and
 * reports the items it didnt see yet as deleted.
 * This function resets the internal table so it start to report deleted items again
 * 
 * @param table The hashtable
 * 
 */
void osync_hashtable_reset(OSyncHashTable *table)
{
	osync_trace(TRACE_ENTRY, "%s(%p)", __func__, table);
	osync_assert(table);
	osync_assert(table->dbhandle);

	osync_db_reset(table->dbhandle, table->tablename, NULL);
	
	osync_trace(TRACE_EXIT, "%s", __func__);
}

/*! @brief Returns the number of entries in this hashtable
 * 
 * @param table The hashtable
 * @returns The number of entries, or -1 if an error occurred
 * 
 */
int osync_hashtable_num_entries(OSyncHashTable *table)
{
	osync_trace(TRACE_ENTRY, "%s(%p)", __func__, table);
	osync_assert(table);
	osync_assert(table->dbhandle);
	
	char *query = g_strdup_printf("SELECT * FROM %s", table->tablename);
	int ret = osync_db_count(table->dbhandle, query, NULL);
	g_free(query);
	
	if (ret < 0) {
		osync_trace(TRACE_EXIT_ERROR, "%s: Cannot count number of hashtable entries!", __func__);
		return -1;
	}

	osync_trace(TRACE_EXIT, "%s: %i", __func__, ret);
	return ret;
}

/*! @brief Gets the nth entry from the table
 * 
 * This is mainly useful for debugging or special purposes
 * 
 * @param table The hashtable
 * @param nth The number of the entry to return
 * @param uid A pointer to a char * that will hold the uid. The caller is responsible for freeing this.
 * @param hash A pointer to a char * that will hold the hash. The caller is responsible for freeing this.
 * @returns TRUE if successful, FALSE otherwise
 * 
 */
osync_bool osync_hashtable_nth_entry(OSyncHashTable *table, int nth, char **uid, char **hash)
{
	osync_assert(table);
	osync_assert(table->dbhandle);
	
	GList *list = NULL;
	OSyncError *error = NULL;
	

	char *query = g_strdup_printf("SELECT uid, hash FROM %s LIMIT 1 OFFSET %i", table->tablename, nth);
	list = osync_db_query_table(table->dbhandle, query, &error);
	g_free(query);

	if (osync_error_is_set(&error)) {
		osync_trace(TRACE_EXIT_ERROR, "%s: Cannot get #%i entry from hashtable: %s", __func__, nth, osync_error_print(&error));
		osync_error_unref(&error);
		return FALSE;
	}
		
	GList *column = list->data; 

	*uid = g_strdup((char*)g_list_nth_data(column, 0));
	*hash = g_strdup((char*)g_list_nth_data(column, 1));
	
	osync_db_free_list(list);

	return TRUE;
}

/*! @brief Gets the hash value for given uid
 *
 * @param table The hashtable
 * @param uid The uid to lookup
 * @returns The hash. Has to be freed by the caller.
 */
char *osync_hashtable_get_hash(OSyncHashTable *table, const char *uid)
{
       osync_assert(uid);
       osync_assert(table);
       osync_assert(table->dbhandle);

       char *hash = NULL;
       GList *list = NULL;
       OSyncError *error = NULL;
       char *escaped_uid = _osync_db_sql_escape(uid);

       char *query = g_strdup_printf("SELECT hash FROM %s WHERE uid= '%s' LIMIT 1",
                                     table->tablename, escaped_uid);
       list = osync_db_query_table(table->dbhandle, query, &error);
       g_free(query);
       g_free(escaped_uid);

       if (osync_error_is_set(&error)) {
               osync_trace(TRACE_EXIT_ERROR, "%s: Cannot get hash for '%s': %s",
                           __func__, uid, osync_error_print(&error));
               osync_error_unref(&error);
               return NULL;
       }

       if (list && list->data) {
               GList *column = list->data;
               hash = g_strdup((char *)g_list_nth_data(column, 0));
       }

       osync_db_free_list(list);

       return hash;
}

void osync_hashtable_write(OSyncHashTable *table, const char *uid, const char *hash)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %s, %s)", __func__, table, uid, hash);
	osync_assert(table);
	osync_assert(table->dbhandle);

	char *escaped_uid = _osync_db_sql_escape(uid);
	char *escaped_hash = _osync_db_sql_escape(hash);
	char *query = g_strdup_printf("REPLACE INTO %s ('uid', 'hash') VALUES('%s', '%s')", table->tablename, escaped_uid, escaped_hash);
	g_free(escaped_uid);
	g_free(escaped_hash);

	if (!osync_db_query(table->dbhandle, query, NULL)) {
		g_free(query);
		osync_trace(TRACE_EXIT, "%s: Cannot write hashtable entry.", __func__);
		return;
	}
	g_free(query);
	
	osync_trace(TRACE_EXIT, "%s", __func__);
}

void osync_hashtable_delete(OSyncHashTable *table, const char *uid)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %s)", __func__, table, uid);
	osync_assert(table);
	osync_assert(table->dbhandle);

	char *escaped_uid = _osync_db_sql_escape(uid);
	char *query = g_strdup_printf("DELETE FROM %s WHERE uid='%s'", table->tablename, escaped_uid);
	g_free(escaped_uid);

	if (!osync_db_query(table->dbhandle, query, NULL)) {
		g_free(query);
		osync_trace(TRACE_EXIT_ERROR, "%s: Cannot delete hashtable entry.", __func__);
		return;
	}
	g_free(query);

	osync_trace(TRACE_EXIT, "%s", __func__);
}

/*! @brief Update the hash for a entry
 * 
 * Updates the hash for a entry in the hashtable. Do this after you see that a hash
 * has changed, for example after reading it during get_changes or after you
 * wrote it
 * 
 * @param table The hashtable
 * @param type The type of change (added, modified, etc.)
 * @param uid the uid of the changed entry
 * @param hash the new hash of the changed entry
 */
void osync_hashtable_update_hash(OSyncHashTable *table, OSyncChangeType type, const char *uid, const char *hash)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %i, %s, %s)", __func__, table, type, uid, hash);
	osync_assert(table);
	osync_assert(table->dbhandle);

	switch (type) {
		case OSYNC_CHANGE_TYPE_DELETED:
			osync_hashtable_delete(table, uid);
			break;
		case OSYNC_CHANGE_TYPE_UNMODIFIED:
		case OSYNC_CHANGE_TYPE_UNKNOWN:
		case OSYNC_CHANGE_TYPE_MODIFIED:
		case OSYNC_CHANGE_TYPE_ADDED:
			osync_hashtable_write(table, uid, hash);
			break;
	}
	
	osync_trace(TRACE_EXIT, "%s", __func__);
}

/*! @brief Report a item
 * 
 * When you use this function the item is marked as reported, so it will not get
 * listed as deleted. Use this function if there are problems accessing an object for
 * example so that the object does not get reported as deleted accidentally.
 * 
 * @param table The hashtable
 * @param uid The uid to report
 * 
 */
void osync_hashtable_report(OSyncHashTable *table, const char *uid)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %s)", __func__, table, uid);
	osync_assert(table);
	osync_assert(table->dbhandle);
	
	g_hash_table_insert(table->used_entries, g_strdup(uid), GINT_TO_POINTER(1));
	
	osync_trace(TRACE_EXIT, "%s", __func__);
}

/*! @brief Get the uid of all deleted items
 * 
 * @param table The hashtable
 * @returns A null-terminated array of uids. The uids and this array have to be freed by the caller.
 * 
 */
char **osync_hashtable_get_deleted(OSyncHashTable *table)
{
	osync_trace(TRACE_ENTRY, "%s(%p)", __func__, table);

	osync_assert(table);
	osync_assert(table->dbhandle);

	GList *row = NULL, *result = NULL;

	char *query = g_strdup_printf("SELECT uid FROM %s", table->tablename);
	result = osync_db_query_table(table->dbhandle, query, NULL); 
	g_free(query);

	int numrows = g_list_length(result);
	char **ret = g_malloc0((numrows + 1) * sizeof(char *));
	
	int num = 0;
	for (row = result; row; row = row->next) {
		GList *column = row->data;

		const char *uid = (const char *) g_list_nth_data(column, 0);

		if (!g_hash_table_lookup(table->used_entries, uid))
			ret[num++] = g_strdup(uid);
	}

	osync_db_free_list(result);

	osync_trace(TRACE_EXIT, "%s: %p", __func__, ret);
	return ret;
}

/*! @brief Gets the changetype for a given uid and hash
 * 
 * This function does not report the object so if you only use this function then
 * the object will get reported as deleted! Please use osync_hashtable_report() for reporting
 * an object.
 * 
 * @param table The hashtable
 * @param uid The uid to lookup
 * @param hash The hash to compare
 * @returns The changetype
 * 
 */
OSyncChangeType osync_hashtable_get_changetype(OSyncHashTable *table, const char *uid, const char *hash)
{
	osync_trace(TRACE_ENTRY, "%s(%p, %s, %s)", __func__, table, uid, hash);
	osync_assert(table);
	osync_assert(table->dbhandle);

	char *orighash = NULL;
	
	OSyncChangeType retval = OSYNC_CHANGE_TYPE_UNMODIFIED;

	char *escaped_uid = _osync_db_sql_escape(uid);
	char *query = g_strdup_printf("SELECT hash FROM %s WHERE uid='%s'", table->tablename, escaped_uid);
	orighash = osync_db_query_single_string(table->dbhandle, query, NULL); 
	g_free(query);
	g_free(escaped_uid);
	
	osync_trace(TRACE_INTERNAL, "Comparing %s with %s", hash, orighash);
	
	if (orighash) {
		if (!strcmp(hash, orighash))
			retval = OSYNC_CHANGE_TYPE_UNMODIFIED;
		else
			retval = OSYNC_CHANGE_TYPE_MODIFIED;
	} else
		retval = OSYNC_CHANGE_TYPE_ADDED;

	g_free(orighash);
	
	osync_trace(TRACE_EXIT, "%s: %i", __func__, retval);
	return retval;
}

/*@}*/
