/*********************************************************************
 * QofBookMerge.c -- api for QoFBook merge with collision handling   *
 * Copyright (C) 2004,2005,2006                                      *
 *    Neil Williams <linux@codehelp.co.uk>                           *
 *                                                                   *
 * 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 of    *
 * the License, or (at your option) any later version.               *
 *                                                                   *
 * This program is distributed in the hope that it will be useful,   *
 * but WITHOUT ANY WARRANTY; without even the implied warranty of    *
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the     *
 * GNU General Public License for more details.                      *
 *                                                                   *
 * You should have received a copy of the GNU General Public License *
 * along with this program; if not, contact:                         *
 *                                                                   *
 * Free Software Foundation           Voice:  +1-617-542-5942        *
 * 51 Franklin Street, Fifth Floor    Fax:    +1-617-542-2652       *
 * Boston, MA  02110-1301,  USA       gnu@gnu.org                   *
 *                                                                   *
 ********************************************************************/

#include "config.h"
#include <glib.h>
#include "qof.h"

static QofLogModule log_module = QOF_MOD_MERGE;

/* private rule iteration struct */
struct QofBookMergeRuleIterate
{
	QofBookMergeRuleForeachCB fcn;
	QofBookMergeData *data;
	QofBookMergeRule *rule;
	GList *ruleList;
	guint remainder;
};

/* Make string type parameters 3 times more
	important in the match than default types.
	i.e. even if two other parameters differ, 
	a string match will still provide a better target
	than when other types match and the string does not.
*/
#define DEFAULT_MERGE_WEIGHT    1
#define QOF_STRING_WEIGHT       3
#define QOF_DATE_STRING_LENGTH  MAX_DATE_LENGTH

static QofBookMergeRule *
qof_book_merge_update_rule (QofBookMergeRule * currentRule, gboolean match,
	gint weight)
{
	gboolean absolute;

	absolute = currentRule->mergeAbsolute;
	if (absolute && match && currentRule->mergeResult == MERGE_UNDEF)
		currentRule->mergeResult = MERGE_ABSOLUTE;
	if (absolute && !match)
		currentRule->mergeResult = MERGE_UPDATE;
	if (!absolute && match && currentRule->mergeResult == MERGE_UNDEF)
		currentRule->mergeResult = MERGE_DUPLICATE;
	if (!absolute && !match)
	{
		currentRule->difference += weight;
		if (currentRule->mergeResult == MERGE_DUPLICATE)
			currentRule->mergeResult = MERGE_REPORT;
	}
	return currentRule;
}

struct collect_list_s
{
	GSList *linkedEntList;
};

static void
collect_reference_cb (QofEntity * ent, gpointer user_data)
{
	struct collect_list_s *s;

	s = (struct collect_list_s *) user_data;
	if (!ent || !s)
		return;
	s->linkedEntList = g_slist_prepend (s->linkedEntList, ent);
}

static gint
qof_book_merge_compare (QofBookMergeData * mergeData)
{
	QofBookMergeRule *currentRule;
	QofCollection *mergeColl, *targetColl;
	gchar *stringImport, *stringTarget;
	QofEntity *mergeEnt, *targetEnt, *referenceEnt;
	const GUID *guidImport, *guidTarget;
	QofParam *qtparam;
	KvpFrame *kvpImport, *kvpTarget;
	QofIdType mergeParamName;
	QofType mergeType;
	GSList *paramList;
	gboolean absolute, mergeError, knowntype, mergeMatch, booleanImport,
		booleanTarget, (*boolean_getter) (QofEntity *, QofParam *);
	QofNumeric numericImport, numericTarget,
		(*numeric_getter) (QofEntity *, QofParam *);
	gdouble doubleImport, doubleTarget, (*double_getter) (QofEntity *,
		QofParam *);
	gint32 i32Import, i32Target, (*int32_getter) (QofEntity *, QofParam *);
	gint64 i64Import, i64Target, (*int64_getter) (QofEntity *, QofParam *);
	gchar charImport, charTarget, (*char_getter) (QofEntity *, QofParam *);

	g_return_val_if_fail ((mergeData != NULL), -1);
	currentRule = mergeData->currentRule;
	g_return_val_if_fail ((currentRule != NULL), -1);
	absolute = currentRule->mergeAbsolute;
	mergeEnt = currentRule->importEnt;
	targetEnt = currentRule->targetEnt;
	paramList = currentRule->mergeParam;
	currentRule->difference = 0;
	currentRule->mergeResult = MERGE_UNDEF;
	currentRule->linkedEntList = NULL;
	g_return_val_if_fail ((targetEnt) || (mergeEnt) || (paramList), -1);
	kvpImport = kvp_frame_new ();
	kvpTarget = kvp_frame_new ();
	mergeError = FALSE;
	while (paramList != NULL)
	{
		mergeMatch = FALSE;
		knowntype = FALSE;
		qtparam = paramList->data;
		mergeParamName = qtparam->param_name;
		g_return_val_if_fail (mergeParamName != NULL, -1);
		mergeType = qtparam->param_type;
		if (safe_strcmp (mergeType, QOF_TYPE_STRING) == 0)
		{
			stringImport = qtparam->param_getfcn (mergeEnt, qtparam);
			stringTarget = qtparam->param_getfcn (targetEnt, qtparam);
			/* very strict string matches may need to be relaxed. */
			if (stringImport == NULL)
				stringImport = "";
			if (stringTarget == NULL)
				stringTarget = "";
			if (safe_strcmp (stringImport, stringTarget) == 0)
				mergeMatch = TRUE;
			/* Give special weight to a string match */
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, QOF_STRING_WEIGHT);
			stringImport = stringTarget = NULL;
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_TIME) == 0)
		{
			QofTime *qtImport, *qtTarget;

			qtImport = qtparam->param_getfcn (mergeEnt, qtparam);
			qtTarget = qtparam->param_getfcn (targetEnt, qtparam);
			if (qof_time_cmp (qtImport, qtTarget) == 0)
				currentRule = qof_book_merge_update_rule (currentRule,
					mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
#ifndef QOF_DISABLE_DEPRECATED
		if (safe_strcmp (mergeType, QOF_TYPE_DATE) == 0)
		{
			Timespec tsImport, tsTarget, (*date_getter) (QofEntity *, QofParam *);
			date_getter =
				(Timespec (*)(QofEntity *,
					QofParam *)) qtparam->param_getfcn;
			tsImport = date_getter (mergeEnt, qtparam);
			tsTarget = date_getter (targetEnt, qtparam);
			if (timespec_cmp (&tsImport, &tsTarget) == 0)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
#endif
		if ((safe_strcmp (mergeType, QOF_TYPE_NUMERIC) == 0) ||
			(safe_strcmp (mergeType, QOF_TYPE_DEBCRED) == 0))
		{
			numeric_getter =
				(QofNumeric (*)(QofEntity *, QofParam *)) qtparam->
				param_getfcn;
			numericImport = numeric_getter (mergeEnt, qtparam);
			numericTarget = numeric_getter (targetEnt, qtparam);
			if (qof_numeric_compare (numericImport, numericTarget) == 0)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_GUID) == 0)
		{
			guidImport = qtparam->param_getfcn (mergeEnt, qtparam);
			guidTarget = qtparam->param_getfcn (targetEnt, qtparam);
			if (guid_compare (guidImport, guidTarget) == 0)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_INT32) == 0)
		{
			int32_getter =
				(gint32 (*)(QofEntity *,
					QofParam *)) qtparam->param_getfcn;
			i32Import = int32_getter (mergeEnt, qtparam);
			i32Target = int32_getter (targetEnt, qtparam);
			if (i32Target == i32Import)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_INT64) == 0)
		{
			int64_getter =
				(gint64 (*)(QofEntity *,
					QofParam *)) qtparam->param_getfcn;
			i64Import = int64_getter (mergeEnt, qtparam);
			i64Target = int64_getter (targetEnt, qtparam);
			if (i64Target == i64Import)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_DOUBLE) == 0)
		{
			double_getter =
				(double (*)(QofEntity *,
					QofParam *)) qtparam->param_getfcn;
			doubleImport = double_getter (mergeEnt, qtparam);
			doubleTarget = double_getter (mergeEnt, qtparam);
			if (doubleImport == doubleTarget)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_BOOLEAN) == 0)
		{
			boolean_getter =
				(gboolean (*)(QofEntity *,
					QofParam *)) qtparam->param_getfcn;
			booleanImport = boolean_getter (mergeEnt, qtparam);
			booleanTarget = boolean_getter (targetEnt, qtparam);
			if (booleanImport != FALSE && booleanImport != TRUE)
				booleanImport = FALSE;
			if (booleanTarget != FALSE && booleanTarget != TRUE)
				booleanTarget = FALSE;
			if (booleanImport == booleanTarget)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_KVP) == 0)
		{
			kvpImport =
				kvp_frame_copy (qtparam->param_getfcn (mergeEnt, qtparam));
			kvpTarget =
				kvp_frame_copy (qtparam->param_getfcn (targetEnt,
					qtparam));
			if (kvp_frame_compare (kvpImport, kvpTarget) == 0)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_CHAR) == 0)
		{
			char_getter =
				(gchar (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
			charImport = char_getter (mergeEnt, qtparam);
			charTarget = char_getter (targetEnt, qtparam);
			if (charImport == charTarget)
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		/* No object should have QofSetterFunc defined for the book,
		   but just to be safe, do nothing. */
		if (safe_strcmp (mergeType, QOF_ID_BOOK) == 0)
			knowntype = TRUE;
		if (safe_strcmp (mergeType, QOF_TYPE_COLLECT) == 0)
		{
			struct collect_list_s s;
			s.linkedEntList = NULL;
			mergeColl = qtparam->param_getfcn (mergeEnt, qtparam);
			targetColl = qtparam->param_getfcn (targetEnt, qtparam);
			s.linkedEntList = g_slist_copy (currentRule->linkedEntList);
			qof_collection_foreach (mergeColl, collect_reference_cb, &s);
			currentRule->linkedEntList = g_slist_copy (s.linkedEntList);
			if (0 == qof_collection_compare (mergeColl, targetColl))
				mergeMatch = TRUE;
			currentRule = qof_book_merge_update_rule (currentRule,
				mergeMatch, DEFAULT_MERGE_WEIGHT);
			knowntype = TRUE;
		}
		if (safe_strcmp (mergeType, QOF_TYPE_CHOICE) == 0)
		{
			referenceEnt = qtparam->param_getfcn (mergeEnt, qtparam);
			currentRule->linkedEntList =
				g_slist_prepend (currentRule->linkedEntList, referenceEnt);
			if (referenceEnt == qtparam->param_getfcn (targetEnt, qtparam))
				mergeMatch = TRUE;
			knowntype = TRUE;
		}
		if (knowntype == FALSE)
		{
			referenceEnt = qtparam->param_getfcn (mergeEnt, qtparam);
			if ((referenceEnt != NULL)
				&& (safe_strcmp (referenceEnt->e_type, mergeType) == 0))
			{
				currentRule->linkedEntList =
					g_slist_prepend (currentRule->linkedEntList,
					referenceEnt);
				if (referenceEnt ==
					qtparam->param_getfcn (targetEnt, qtparam))
					mergeMatch = TRUE;
				currentRule = qof_book_merge_update_rule (currentRule,
					mergeMatch, DEFAULT_MERGE_WEIGHT);
			}
		}
		paramList = g_slist_next (paramList);
	}
	mergeData->currentRule = currentRule;
	g_free (kvpImport);
	g_free (kvpTarget);
	return 0;
}

static void
qof_book_merge_commit_foreach_cb (gpointer rule, gpointer arg)
{
	struct QofBookMergeRuleIterate *qiter;

	g_return_if_fail (arg != NULL);
	qiter = (struct QofBookMergeRuleIterate *) arg;
	g_return_if_fail (qiter->data != NULL);
	qiter->fcn (qiter->data, (QofBookMergeRule *) rule, 
		qiter->remainder);
	qiter->remainder--;
}

static void
qof_book_merge_commit_foreach (QofBookMergeRuleForeachCB cb,
	QofBookMergeResult mergeResult, QofBookMergeData * mergeData)
{
	struct QofBookMergeRuleIterate qiter;
	QofBookMergeRule *currentRule;
	GList *subList, *node;

	g_return_if_fail (cb != NULL);
	g_return_if_fail (mergeData != NULL);
	currentRule = mergeData->currentRule;
	g_return_if_fail (currentRule != NULL);
	g_return_if_fail (mergeResult > 0);
	g_return_if_fail ((mergeResult != MERGE_INVALID) || 
		(mergeResult != MERGE_UNDEF) || 
		(mergeResult != MERGE_REPORT));

	qiter.fcn = cb;
	subList = NULL;
	qiter.ruleList = NULL;
	for (node = mergeData->mergeList; node != NULL; node = node->next)
	{
		currentRule = node->data;
		if (currentRule->mergeResult == mergeResult)
			subList = g_list_prepend (subList, currentRule);
	}
	qiter.remainder = g_list_length (subList);
	qiter.data = mergeData;
	g_list_foreach (subList, qof_book_merge_commit_foreach_cb, &qiter);
}

/* build the table of target comparisons

This can get confusing, so bear with me. (!)

Whilst iterating through the entities in the mergeBook, qof_book_mergeForeach assigns
a targetEnt to each mergeEnt (until it runs out of targetEnt or mergeEnt). Each match
is made against the one targetEnt that best matches the mergeEnt. Fine so far.

Any mergeEnt is only ever assigned a targetEnt if the calculated difference between
the two is less than the difference between that targetEnt and any previous mergeEnt
match.

The next mergeEnt may be a much better match for that targetEnt and the target_table
is designed to solve the issues that result from this conflict. The previous match
must be re-assigned because if two mergeEnt's are matched with only one targetEnt,
data loss \b WILL follow. Equally, the current mergeEnt must replace the previous
one as it is a better match. qof_entity_rating holds the details required to identify
the correct mergeEnt to be re-assigned and these mergeEnt entities are therefore
orphaned - to be re-matched later.

Meanwhile, the current mergeEnt is entered into target_table with it's difference and
rule data, in case an even better match is found later in the mergeBook.

Finally, each mergeEnt in the orphan_list is now put through the comparison again.

*/
static gboolean
qof_book_merge_rule_cmp (gconstpointer a, gconstpointer b)
{
	QofBookMergeRule *ra = (QofBookMergeRule *) a;
	QofBookMergeRule *rb = (QofBookMergeRule *) b;
	if (ra->difference == rb->difference)
		return TRUE;
	else
		return FALSE;
}

static void
qof_book_merge_orphan_check (double difference,
	QofBookMergeRule * mergeRule, QofBookMergeData * mergeData)
{
	/* Called when difference is lower than previous
	   Lookup target to find previous match
	   and re-assign mergeEnt to orphan_list */
	QofBookMergeRule *rule;

	g_return_if_fail (mergeRule != NULL);
	g_return_if_fail (mergeData != NULL);
	if (g_hash_table_size (mergeData->target_table) == 0)
		return;
	rule =
		(QofBookMergeRule *) g_hash_table_lookup (mergeData->target_table,
		mergeRule->targetEnt);
	/* If NULL, no match was found. */
	if (rule == NULL)
		return;
	/* Only orphan if this is a better match than already exists. */
	if (difference >= rule->difference)
		return;
	rule->targetEnt = NULL;
	rule->mergeResult = MERGE_UNDEF;
	mergeData->orphan_list = g_slist_append (mergeData->orphan_list, rule);
}

static void
qof_book_merge_match_orphans (QofBookMergeData * mergeData)
{
	GSList *orphans, *targets;
	QofBookMergeRule *rule, *currentRule;
	QofEntity *best_matchEnt;
	double difference;

	g_return_if_fail (mergeData != NULL);
	currentRule = mergeData->currentRule;
	g_return_if_fail (currentRule != NULL);
	/* This routine does NOT copy the orphan list, it
	   is used recursively until empty. */
	orphans = mergeData->orphan_list;
	targets = g_slist_copy (mergeData->targetList);
	while (orphans != NULL)
	{
		rule = orphans->data;
		g_return_if_fail (rule != NULL);
		difference = g_slist_length (mergeData->mergeObjectParams);
		if (rule->targetEnt == NULL)
		{
			rule->mergeResult = MERGE_NEW;
			rule->difference = 0;
			mergeData->mergeList =
				g_list_prepend (mergeData->mergeList, rule);
			orphans = g_slist_next (orphans);
			continue;
		}
		mergeData->currentRule = rule;
		g_return_if_fail (qof_book_merge_compare (mergeData) != -1);
		if (difference > mergeData->currentRule->difference)
		{
			best_matchEnt = currentRule->targetEnt;
			difference = currentRule->difference;
			rule = currentRule;
			mergeData->mergeList =
				g_list_prepend (mergeData->mergeList, rule);
			qof_book_merge_orphan_check (difference, rule, mergeData);
		}
		orphans = g_slist_next (orphans);
	}
	g_slist_free (mergeData->orphan_list);
	g_slist_free (targets);
}

static void
qof_book_merge_foreach_target (QofEntity * targetEnt, gpointer user_data)
{
	QofBookMergeData *mergeData;

	g_return_if_fail (user_data != NULL);
	mergeData = (QofBookMergeData *) user_data;
	g_return_if_fail (targetEnt != NULL);
	mergeData->targetList =
		g_slist_prepend (mergeData->targetList, targetEnt);
}

static void
qof_book_merge_foreach_type_target (QofObject * merge_obj,
	gpointer user_data)
{
	QofBookMergeData *mergeData;
	QofBookMergeRule *currentRule;

	g_return_if_fail (user_data != NULL);
	mergeData = (QofBookMergeData *) user_data;
	currentRule = mergeData->currentRule;
	g_return_if_fail (currentRule != NULL);
	g_return_if_fail (merge_obj != NULL);
	if (safe_strcmp (merge_obj->e_type,
			currentRule->importEnt->e_type) == 0)
	{
		qof_object_foreach (currentRule->importEnt->e_type,
			mergeData->targetBook,
			qof_book_merge_foreach_target, user_data);
	}
}

static void
qof_book_merge_foreach (QofEntity * mergeEnt, gpointer user_data)
{
	QofBookMergeRule *mergeRule, *currentRule;
	QofBookMergeData *mergeData;
	QofEntity *targetEnt, *best_matchEnt;
	GUID *g;
	double difference;
	GSList *c;

	g_return_if_fail (user_data != NULL);
	mergeData = (QofBookMergeData *) user_data;
	g_return_if_fail (mergeEnt != NULL);
	currentRule = mergeData->currentRule;
	g_return_if_fail (currentRule != NULL);
	g = guid_malloc ();
	*g = mergeEnt->guid;
	mergeRule = g_new0 (QofBookMergeRule, 1);
	mergeRule->importEnt = mergeEnt;
	mergeRule->difference = difference = 0;
	mergeRule->mergeAbsolute = FALSE;
	mergeRule->mergeResult = MERGE_UNDEF;
	mergeRule->updated = FALSE;
	mergeRule->mergeType = mergeEnt->e_type;
	mergeRule->mergeLabel = qof_object_get_type_label (mergeEnt->e_type);
	mergeRule->mergeParam = g_slist_copy (mergeData->mergeObjectParams);
	mergeRule->linkedEntList = NULL;
	mergeData->currentRule = mergeRule;
	targetEnt = best_matchEnt = NULL;
	targetEnt =
		qof_collection_lookup_entity (qof_book_get_collection
		(mergeData->targetBook, mergeEnt->e_type), g);
	if (targetEnt != NULL)
	{
		mergeRule->mergeAbsolute = TRUE;
		mergeRule->targetEnt = targetEnt;
		g_return_if_fail (qof_book_merge_compare (mergeData) != -1);
		mergeRule->linkedEntList =
			g_slist_copy (currentRule->linkedEntList);
		mergeData->mergeList =
			g_list_prepend (mergeData->mergeList, mergeRule);
		return;
	}
	/* no absolute match exists */
	g_slist_free (mergeData->targetList);
	mergeData->targetList = NULL;
	qof_object_foreach_type (qof_book_merge_foreach_type_target,
		mergeData);
	if (g_slist_length (mergeData->targetList) == 0)
		mergeRule->mergeResult = MERGE_NEW;
	difference = g_slist_length (mergeRule->mergeParam);
	c = g_slist_copy (mergeData->targetList);
	while (c != NULL)
	{
		mergeRule->targetEnt = c->data;
		currentRule = mergeRule;
		/* compare two entities and sum the differences */
		g_return_if_fail (qof_book_merge_compare (mergeData) != -1);
		if (mergeRule->difference == 0)
		{
			/* check if this is a better match than one already assigned */
			best_matchEnt = mergeRule->targetEnt;
			mergeRule->mergeResult = MERGE_DUPLICATE;
			difference = 0;
			mergeRule->linkedEntList =
				g_slist_copy (currentRule->linkedEntList);
			g_slist_free (c);
			guid_free (g);
			/* exact match, return */
			return;
		}
		if (difference > mergeRule->difference)
		{
			/* The chosen targetEnt determines the parenting of any child object */
			/* check if this is a better match than one already assigned */
			best_matchEnt = mergeRule->targetEnt;
			difference = mergeRule->difference;
			/* Use match to lookup the previous entity that matched this targetEnt (if any)
			   and remove targetEnt from the rule for that mergeEnt.
			   Add the previous mergeEnt to orphan_list.
			 */
			qof_book_merge_orphan_check (difference, mergeRule, mergeData);
		}
		c = g_slist_next (c);
	}
	g_slist_free (c);
	if (best_matchEnt != NULL)
	{
		mergeRule->targetEnt = best_matchEnt;
		mergeRule->difference = difference;
		/* Set this entity in the target_table in case a better match can be made
		   with the next mergeEnt. */
		g_hash_table_insert (mergeData->target_table, mergeRule->targetEnt,
			mergeRule);
		/* compare again with the best partial match */
		g_return_if_fail (qof_book_merge_compare (mergeData) != -1);
		mergeRule->linkedEntList =
			g_slist_copy (currentRule->linkedEntList);
	}
	else
	{
		mergeRule->targetEnt = NULL;
		mergeRule->difference = 0;
		mergeRule->mergeResult = MERGE_NEW;
		mergeRule->linkedEntList =
			g_slist_copy (currentRule->linkedEntList);
	}
	mergeData->mergeList =
		g_list_prepend (mergeData->mergeList, mergeRule);
	guid_free (g);
	/* return to qof_book_merge_init */
}

static void
qof_book_merge_foreach_param (QofParam * param, gpointer user_data)
{
	QofBookMergeData *mergeData;

	g_return_if_fail (user_data != NULL);
	mergeData = (QofBookMergeData *) user_data;
	g_return_if_fail (param != NULL);
	if ((param->param_getfcn != NULL) && (param->param_setfcn != NULL))
	{
		mergeData->mergeObjectParams =
			g_slist_append (mergeData->mergeObjectParams, param);
	}
}

static void
qof_book_merge_foreach_type (QofObject * merge_obj, gpointer user_data)
{
	QofBookMergeData *mergeData;

	g_return_if_fail (user_data != NULL);
	mergeData = (QofBookMergeData *) user_data;
	g_return_if_fail ((merge_obj != NULL));
	/* Skip unsupported objects */
	if ((merge_obj->create == NULL) || (merge_obj->foreach == NULL))
	{
		DEBUG (" merge_obj QOF support failed %s", merge_obj->e_type);
		return;
	}
	if (mergeData->mergeObjectParams != NULL)
		g_slist_free (mergeData->mergeObjectParams);
	mergeData->mergeObjectParams = NULL;
	qof_class_param_foreach (merge_obj->e_type,
		qof_book_merge_foreach_param, mergeData);
	qof_object_foreach (merge_obj->e_type, mergeData->mergeBook,
		qof_book_merge_foreach, mergeData);
}

static void
qof_book_merge_rule_cb (gpointer rule, gpointer arg)
{
	struct QofBookMergeRuleIterate *qiter;
	QofBookMergeData *mergeData;

	g_return_if_fail (arg != NULL);
	qiter = (struct QofBookMergeRuleIterate *) arg;
	mergeData = qiter->data;
	g_return_if_fail (mergeData != NULL);
	g_return_if_fail (mergeData->abort == FALSE);
	qiter->fcn (mergeData, (QofBookMergeRule *) rule, qiter->remainder);
	qiter->data = mergeData;
	qiter->remainder--;
}

static void
qof_book_merge_commit_rule_loop (QofBookMergeData * mergeData,
	QofBookMergeRule * rule, guint remainder __attribute__ ((unused)))
{
	QofInstance *inst;
	gboolean registered_type;
	QofEntity *referenceEnt;
	/* cm_ prefix used for variables that hold the data to commit */
	QofCollection *cm_coll;
	QofParam *cm_param;
	gchar *cm_string;
	const GUID *cm_guid;
	KvpFrame *cm_kvp;
	QofTime *cm_qt;
	/* function pointers and variables for parameter getters that don't use pointers normally */
	QofNumeric cm_numeric, (*numeric_getter) (QofEntity *, QofParam *);
	gdouble cm_double, (*double_getter) (QofEntity *, QofParam *);
	gboolean cm_boolean, (*boolean_getter) (QofEntity *, QofParam *);
	gint32 cm_i32, (*int32_getter) (QofEntity *, QofParam *);
	gint64 cm_i64, (*int64_getter) (QofEntity *, QofParam *);
	gchar cm_char, (*char_getter) (QofEntity *, QofParam *);
	/* function pointers to the parameter setters */
	void (*string_setter) (QofEntity *, const gchar *);
	void (*time_setter) (QofEntity *, QofTime *);
	void (*numeric_setter) (QofEntity *, QofNumeric);
	void (*guid_setter) (QofEntity *, const GUID *);
	void (*double_setter) (QofEntity *, double);
	void (*boolean_setter) (QofEntity *, gboolean);
	void (*i32_setter) (QofEntity *, gint32);
	void (*i64_setter) (QofEntity *, gint64);
	void (*char_setter) (QofEntity *, gchar);
	void (*kvp_frame_setter) (QofEntity *, KvpFrame *);
	void (*reference_setter) (QofEntity *, QofEntity *);
	void (*collection_setter) (QofEntity *, QofCollection *);

	g_return_if_fail (rule != NULL);
	g_return_if_fail (mergeData != NULL);
	g_return_if_fail (mergeData->targetBook != NULL);
	g_return_if_fail ((rule->mergeResult != MERGE_NEW)
		|| (rule->mergeResult != MERGE_UPDATE));
	/* create a new object for MERGE_NEW */
	/* The new object takes the GUID from the import to retain an absolute match */
	if (rule->mergeResult == MERGE_NEW)
	{
		inst =
			(QofInstance *) qof_object_new_instance (rule->importEnt->
			e_type, mergeData->targetBook);
		g_return_if_fail (inst != NULL);
		rule->targetEnt = &inst->entity;
		qof_entity_set_guid (rule->targetEnt,
			qof_entity_get_guid (rule->importEnt));
	}
	/* currentRule->targetEnt is now set,
	   1. by an absolute GUID match or
	   2. by best_matchEnt and difference or
	   3. by MERGE_NEW.
	 */
	while (rule->mergeParam != NULL)
	{
		registered_type = FALSE;
		g_return_if_fail (rule->mergeParam->data);
		cm_param = rule->mergeParam->data;
		rule->mergeType = cm_param->param_type;
		if (safe_strcmp (rule->mergeType, QOF_TYPE_STRING) == 0)
		{
			cm_string = cm_param->param_getfcn (rule->importEnt, cm_param);
			string_setter =
				(void (*)(QofEntity *,
					const gchar *)) cm_param->param_setfcn;
			if (string_setter != NULL)
				string_setter (rule->targetEnt, cm_string);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_TIME) == 0)
		{
			QofTime *(*time_getter) (QofEntity *, QofParam *);

			time_getter =
				(QofTime* (*)(QofEntity *, QofParam *))cm_param->param_getfcn;
			cm_qt = qof_time_copy (
				time_getter (rule->importEnt, cm_param));
			time_setter = 
				(void (*)(QofEntity *, QofTime *))
				cm_param->param_setfcn;
			if ((time_setter != NULL) && (qof_time_is_valid (cm_qt)))
				time_setter (rule->targetEnt, cm_qt);
			registered_type = TRUE;
		}
#ifndef QOF_DISABLE_DEPRECATED
		if (safe_strcmp (rule->mergeType, QOF_TYPE_DATE) == 0)
		{
			Timespec cm_date, (*date_getter) (QofEntity *, QofParam *);
			void (*date_setter) (QofEntity *, Timespec);

			date_getter =
				(Timespec (*)(QofEntity *, QofParam *)) cm_param->
				param_getfcn;
			cm_date = date_getter (rule->importEnt, cm_param);
			date_setter =
				(void (*)(QofEntity *, Timespec)) cm_param->param_setfcn;
			if (date_setter != NULL)
				date_setter (rule->targetEnt, cm_date);
			registered_type = TRUE;
		}
#endif
		if ((safe_strcmp (rule->mergeType, QOF_TYPE_NUMERIC) == 0) ||
			(safe_strcmp (rule->mergeType, QOF_TYPE_DEBCRED) == 0))
		{
			numeric_getter =
				(QofNumeric (*)(QofEntity *, QofParam *)) cm_param->
				param_getfcn;
			cm_numeric = numeric_getter (rule->importEnt, cm_param);
			numeric_setter =
				(void (*)(QofEntity *,
					QofNumeric)) cm_param->param_setfcn;
			if (numeric_setter != NULL)
				numeric_setter (rule->targetEnt, cm_numeric);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_GUID) == 0)
		{
			cm_guid = cm_param->param_getfcn (rule->importEnt, cm_param);
			guid_setter =
				(void (*)(QofEntity *,
					const GUID *)) cm_param->param_setfcn;
			if (guid_setter != NULL)
				guid_setter (rule->targetEnt, cm_guid);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_INT32) == 0)
		{
			int32_getter =
				(gint32 (*)(QofEntity *,
					QofParam *)) cm_param->param_getfcn;
			cm_i32 = int32_getter (rule->importEnt, cm_param);
			i32_setter =
				(void (*)(QofEntity *, gint32)) cm_param->param_setfcn;
			if (i32_setter != NULL)
				i32_setter (rule->targetEnt, cm_i32);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_INT64) == 0)
		{
			int64_getter =
				(gint64 (*)(QofEntity *,
					QofParam *)) cm_param->param_getfcn;
			cm_i64 = int64_getter (rule->importEnt, cm_param);
			i64_setter =
				(void (*)(QofEntity *, gint64)) cm_param->param_setfcn;
			if (i64_setter != NULL)
				i64_setter (rule->targetEnt, cm_i64);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_DOUBLE) == 0)
		{
			double_getter =
				(double (*)(QofEntity *,
					QofParam *)) cm_param->param_getfcn;
			cm_double = double_getter (rule->importEnt, cm_param);
			double_setter =
				(void (*)(QofEntity *, double)) cm_param->param_setfcn;
			if (double_setter != NULL)
				double_setter (rule->targetEnt, cm_double);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_BOOLEAN) == 0)
		{
			boolean_getter =
				(gboolean (*)(QofEntity *, QofParam *)) cm_param->
				param_getfcn;
			cm_boolean = boolean_getter (rule->importEnt, cm_param);
			boolean_setter =
				(void (*)(QofEntity *, gboolean)) cm_param->param_setfcn;
			if (boolean_setter != NULL)
				boolean_setter (rule->targetEnt, cm_boolean);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_KVP) == 0)
		{
			cm_kvp =
				kvp_frame_copy (cm_param->
				param_getfcn (rule->importEnt, cm_param));
			kvp_frame_setter =
				(void (*)(QofEntity *, KvpFrame *)) cm_param->param_setfcn;
			if (kvp_frame_setter != NULL)
				kvp_frame_setter (rule->targetEnt, cm_kvp);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_CHAR) == 0)
		{
			char_getter =
				(gchar (*)(QofEntity *,
					QofParam *)) cm_param->param_getfcn;
			cm_char = char_getter (rule->importEnt, cm_param);
			char_setter =
				(void (*)(QofEntity *, gchar)) cm_param->param_setfcn;
			if (char_setter != NULL)
				char_setter (rule->targetEnt, cm_char);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_COLLECT) == 0)
		{
			cm_coll = cm_param->param_getfcn (rule->importEnt, cm_param);
			collection_setter =
				(void (*)(QofEntity *, QofCollection *)) cm_param->
				param_setfcn;
			if (collection_setter != NULL)
				collection_setter (rule->targetEnt, cm_coll);
			registered_type = TRUE;
		}
		if (safe_strcmp (rule->mergeType, QOF_TYPE_CHOICE) == 0)
		{
			referenceEnt =
				cm_param->param_getfcn (rule->importEnt, cm_param);
			reference_setter =
				(void (*)(QofEntity *,
					QofEntity *)) cm_param->param_setfcn;
			if (reference_setter != NULL)
				reference_setter (rule->targetEnt, referenceEnt);
			registered_type = TRUE;
		}
		if (registered_type == FALSE)
		{
			referenceEnt =
				cm_param->param_getfcn (rule->importEnt, cm_param);
			if (referenceEnt)
			{
				reference_setter =
					(void (*)(QofEntity *, QofEntity *)) cm_param->
					param_setfcn;
				if (reference_setter != NULL)
				{
					reference_setter (rule->targetEnt, referenceEnt);
				}
			}
		}
		rule->mergeParam = g_slist_next (rule->mergeParam);
	}
}

/* ================================================================ */
/* API functions. */

QofBookMergeData *
qof_book_merge_init (QofBook * importBook, QofBook * targetBook)
{
	QofBookMergeData *mergeData;
	QofBookMergeRule *currentRule;
	GList *check;

	g_return_val_if_fail ((importBook != NULL)
		&& (targetBook != NULL), NULL);
	mergeData = g_new0 (QofBookMergeData, 1);
	mergeData->abort = FALSE;
	mergeData->mergeList = NULL;
	mergeData->targetList = NULL;
	mergeData->mergeBook = importBook;
	mergeData->targetBook = targetBook;
	mergeData->mergeObjectParams = NULL;
	mergeData->orphan_list = NULL;
	mergeData->target_table =
		g_hash_table_new (g_direct_hash, qof_book_merge_rule_cmp);
	currentRule = g_new0 (QofBookMergeRule, 1);
	mergeData->currentRule = currentRule;
	qof_object_foreach_type (qof_book_merge_foreach_type, mergeData);
	g_return_val_if_fail (mergeData->mergeObjectParams, NULL);
	if (mergeData->orphan_list != NULL)
		qof_book_merge_match_orphans (mergeData);
	for (check = mergeData->mergeList; check != NULL; check = check->next)
	{
		currentRule = check->data;
		if (currentRule->mergeResult == MERGE_INVALID)
		{
			mergeData->abort = TRUE;
			return (NULL);
		}
	}
	return mergeData;
}

void
qof_book_merge_abort (QofBookMergeData * mergeData)
{
	QofBookMergeRule *currentRule;

	g_return_if_fail (mergeData != NULL);
	while (mergeData->mergeList != NULL)
	{
		currentRule = mergeData->mergeList->data;
		g_slist_free (currentRule->linkedEntList);
		g_slist_free (currentRule->mergeParam);
		g_free (mergeData->mergeList->data);
		if (currentRule)
		{
			g_slist_free (currentRule->linkedEntList);
			g_slist_free (currentRule->mergeParam);
			g_free (currentRule);
		}
		mergeData->mergeList = g_list_next (mergeData->mergeList);
	}
	g_list_free (mergeData->mergeList);
	g_slist_free (mergeData->mergeObjectParams);
	g_slist_free (mergeData->targetList);
	if (mergeData->orphan_list != NULL)
		g_slist_free (mergeData->orphan_list);
	g_hash_table_destroy (mergeData->target_table);
	g_free (mergeData);
}

/* The QOF_TYPE_DATE output format from
qof_book_merge_param_as_string has been changed to QSF_XSD_TIME,
a UTC formatted timestring: 2005-01-01T10:55:23Z
If you change QOF_UTC_DATE_FORMAT, change 
backend/file/qsf-xml.c : qsf_entity_foreach to
reformat to QSF_XSD_TIME or the QSF XML will
FAIL the schema validation and QSF exports will become invalid.

The QOF_TYPE_BOOLEAN is lowercase for the same reason.
*/
/** \deprecated replace with qof_util_param_as_string */
gchar *
qof_book_merge_param_as_string (QofParam * qtparam, QofEntity * qtEnt)
{
	gchar *param_string;
	gchar param_sa[GUID_ENCODING_LENGTH + 1];
	QofType paramType;
	const GUID *param_guid;
	QofTime *param_qt;
	QofNumeric param_numeric, (*numeric_getter) (QofEntity *, QofParam *);
	gdouble param_double, (*double_getter) (QofEntity *, QofParam *);
	gboolean param_boolean, (*boolean_getter) (QofEntity *, QofParam *);
	gint32 param_i32, (*int32_getter) (QofEntity *, QofParam *);
	gint64 param_i64, (*int64_getter) (QofEntity *, QofParam *);
	gchar param_char, (*char_getter) (QofEntity *, QofParam *);

	param_string = NULL;
	paramType = qtparam->param_type;
	if (safe_strcmp (paramType, QOF_TYPE_STRING) == 0)
	{
		param_string = qtparam->param_getfcn (qtEnt, qtparam);
		if (param_string == NULL)
			param_string = "";
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_TIME) == 0)
	{
		QofDate *qd;

		param_qt = qof_time_copy (
			qtparam->param_getfcn (qtEnt, qtparam));
		if (!param_qt)
			return NULL;
		qd = qof_date_from_qtime (param_qt);
		param_string = qof_date_print (qd, QOF_DATE_FORMAT_UTC);
		qof_date_free (qd);
		qof_time_free (param_qt);
		return param_string;
	}
#ifndef QOF_DISABLE_DEPRECATED
	if (safe_strcmp (paramType, QOF_TYPE_DATE) == 0)
	{
		Timespec param_ts, (*date_getter) (QofEntity *, QofParam *);
		time_t param_t;
		gchar param_date[QOF_DATE_STRING_LENGTH];

		date_getter =
			(Timespec (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_ts = date_getter (qtEnt, qtparam);
		param_t = timespecToTime_t (param_ts);
		strftime (param_date, QOF_DATE_STRING_LENGTH, QOF_UTC_DATE_FORMAT,
			gmtime (&param_t));
		param_string = g_strdup (param_date);
		return param_string;
	}
#endif
	if ((safe_strcmp (paramType, QOF_TYPE_NUMERIC) == 0) ||
		(safe_strcmp (paramType, QOF_TYPE_DEBCRED) == 0))
	{
		numeric_getter =
			(QofNumeric (*)(QofEntity *,
				QofParam *)) qtparam->param_getfcn;
		param_numeric = numeric_getter (qtEnt, qtparam);
		param_string = g_strdup (qof_numeric_to_string (param_numeric));
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_GUID) == 0)
	{
		param_guid = qtparam->param_getfcn (qtEnt, qtparam);
		guid_to_string_buff (param_guid, param_sa);
		param_string = g_strdup (param_sa);
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_INT32) == 0)
	{
		int32_getter =
			(gint32 (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_i32 = int32_getter (qtEnt, qtparam);
		param_string = g_strdup_printf ("%d", param_i32);
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_INT64) == 0)
	{
		int64_getter =
			(gint64 (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_i64 = int64_getter (qtEnt, qtparam);
		param_string = g_strdup_printf ("%" G_GINT64_FORMAT, param_i64);
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_DOUBLE) == 0)
	{
		double_getter =
			(double (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_double = double_getter (qtEnt, qtparam);
		param_string = g_strdup_printf ("%f", param_double);
		return param_string;
	}
	if (safe_strcmp (paramType, QOF_TYPE_BOOLEAN) == 0)
	{
		boolean_getter =
			(gboolean (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_boolean = boolean_getter (qtEnt, qtparam);
		/* Boolean values need to be lowercase for QSF validation. */
		if (param_boolean == TRUE)
			param_string = g_strdup ("true");
		else
			param_string = g_strdup ("false");
		return param_string;
	}
	/* "kvp" contains repeating values, cannot be a single string for the frame. */
	if (safe_strcmp (paramType, QOF_TYPE_KVP) == 0)
		return param_string;
	if (safe_strcmp (paramType, QOF_TYPE_CHAR) == 0)
	{
		char_getter =
			(gchar (*)(QofEntity *, QofParam *)) qtparam->param_getfcn;
		param_char = char_getter (qtEnt, qtparam);
		param_string = g_strdup_printf ("%c", param_char);
		return param_string;
	}
	return NULL;
}

QofBookMergeData *
qof_book_merge_update_result (QofBookMergeData * mergeData,
	QofBookMergeResult tag)
{
	QofBookMergeRule *resolved;

	g_return_val_if_fail ((mergeData != NULL), NULL);
	g_return_val_if_fail ((tag > 0), NULL);
	g_return_val_if_fail ((tag != MERGE_REPORT), NULL);
	resolved = mergeData->currentRule;
	g_return_val_if_fail ((resolved != NULL), NULL);
	if ((resolved->mergeAbsolute == TRUE) && (tag == MERGE_DUPLICATE))
		tag = MERGE_ABSOLUTE;
	if ((resolved->mergeAbsolute == TRUE) && (tag == MERGE_NEW))
		tag = MERGE_UPDATE;
	if ((resolved->mergeAbsolute == FALSE) && (tag == MERGE_ABSOLUTE))
		tag = MERGE_DUPLICATE;
	if ((resolved->mergeResult == MERGE_NEW) && (tag == MERGE_UPDATE))
		tag = MERGE_NEW;
	if (resolved->updated == FALSE)
		resolved->mergeResult = tag;
	resolved->updated = TRUE;
	if (tag >= MERGE_INVALID)
	{
		mergeData->abort = TRUE;
		mergeData->currentRule = resolved;
		return NULL;
	}
	mergeData->currentRule = resolved;
	return mergeData;
}

gint
qof_book_merge_commit (QofBookMergeData * mergeData)
{
	QofBookMergeRule *currentRule;
	GList *check, *node;

	g_return_val_if_fail (mergeData != NULL, -1);
	g_return_val_if_fail (mergeData->mergeList != NULL, -1);
	g_return_val_if_fail (mergeData->targetBook != NULL, -1);
	if (mergeData->abort == TRUE)
		return -1;
	check = g_list_copy (mergeData->mergeList);
	g_return_val_if_fail (check != NULL, -1);
	for (node = check; node != NULL; node = node->next)
	{
		currentRule = node->data;
		if (currentRule->mergeResult == MERGE_INVALID)
		{
			qof_book_merge_abort (mergeData);
			g_list_free (check);
			return (-2);
		}
		if (currentRule->mergeResult == MERGE_REPORT)
		{
			g_list_free (check);
			return 1;
		}
	}
	g_list_free (check);
	qof_book_merge_commit_foreach (qof_book_merge_commit_rule_loop,
		MERGE_NEW, mergeData);
	qof_book_merge_commit_foreach (qof_book_merge_commit_rule_loop,
		MERGE_UPDATE, mergeData);
	/* Placeholder for QofObject merge_helper_cb - all objects
	   and all parameters set */
	while (mergeData->mergeList != NULL)
	{
		currentRule = mergeData->mergeList->data;
		g_slist_free (currentRule->mergeParam);
		g_slist_free (currentRule->linkedEntList);
		mergeData->mergeList = g_list_next (mergeData->mergeList);
	}
	g_list_free (mergeData->mergeList);
	g_slist_free (mergeData->mergeObjectParams);
	g_slist_free (mergeData->targetList);
	if (mergeData->orphan_list != NULL)
		g_slist_free (mergeData->orphan_list);
	g_hash_table_destroy (mergeData->target_table);
	g_free (mergeData);
	return 0;
}

void
qof_book_merge_rule_foreach (QofBookMergeData * mergeData,
	QofBookMergeRuleForeachCB cb, QofBookMergeResult mergeResult)
{
	struct QofBookMergeRuleIterate qiter;
	QofBookMergeRule *currentRule;
	GList *matching_rules, *node;

	g_return_if_fail (cb != NULL);
	g_return_if_fail (mergeData != NULL);
	currentRule = mergeData->currentRule;
	g_return_if_fail (mergeResult > 0);
	g_return_if_fail (mergeResult != MERGE_INVALID);
	g_return_if_fail (mergeData->abort == FALSE);
	qiter.fcn = cb;
	qiter.data = mergeData;
	matching_rules = NULL;
	for (node = mergeData->mergeList; node != NULL; node = node->next)
	{
		currentRule = node->data;
		if (currentRule->mergeResult == mergeResult)
			matching_rules = g_list_prepend (matching_rules, 
				currentRule);
	}
	qiter.remainder = g_list_length (matching_rules);
	g_list_foreach (matching_rules, qof_book_merge_rule_cb, &qiter);
	g_list_free (matching_rules);
}

/* ============================================================== */
