/*
 * Copyright (C) 2007 - 2014 Vivien Malerba <malerba@gnome-db.org>
 * Copyright (C) 2008 Murray Cumming <murrayc@murrayc.com>
 * Copyright (C) 2010 David King <davidk@openismus.com>
 * Copyright (C) 2018 Daniel Espinosa <esodan@gmail.com>
 *
 * 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 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301, USA.
 */

#include <glib/gi18n-lib.h>
#include <string.h>
#include "gda-report-engine.h"
#include <libgda/gda-data-model.h>
#include <libgda/gda-data-model-iter.h>
#include <libgda/gda-holder.h>
#include <libgda/gda-connection.h>
#include <libgda/gda-data-handler.h>
#include <virtual/libgda-virtual.h>
#include <sql-parser/gda-sql-parser.h>
#include <sql-parser/gda-sql-statement.h>

#include <libgda/handlers/gda-handler-boolean.h>
#include <libgda/handlers/gda-handler-numerical.h>
#include <libgda/handlers/gda-handler-string.h>
#include <libgda/handlers/gda-handler-time.h>
#include <libgda/handlers/gda-handler-type.h>

#include "rt-parser.h"

#include <libgda/gda-debug-macros.h>

/* module error */
GQuark gda_report_engine_error_quark (void)
{
        static GQuark quark;
        if (!quark)
                quark = g_quark_from_static_string ("gda_report_engine_error");
        return quark;
}

typedef struct {
	xmlDocPtr     doc; /* may be %NULL */
	xmlNodePtr    spec;
	xmlNodePtr    result;
	GHashTable   *objects;
	gchar        *output_dir;
} GdaReportEnginePrivate;

G_DEFINE_TYPE_WITH_PRIVATE (GdaReportEngine, gda_report_engine, G_TYPE_OBJECT)

/* properties */
enum
{
        PROP_0,
	PROP_SPEC_NODE,
	PROP_SPEC_STRING,
	PROP_SPEC_FILE,
	PROP_OUTPUT_DIR
};

static void gda_report_engine_dispose   (GObject *object);
static void gda_report_engine_set_property (GObject *object,
					    guint param_id,
					    const GValue *value,
					    GParamSpec *pspec);
static void gda_report_engine_get_property (GObject *object,
					    guint param_id,
					    GValue *value,
					    GParamSpec *pspec);

static GHashTable *data_handlers = NULL; /* key = GType, value = GdaDataHandler obj */

/*
 * GdaReportEngine class implementation
 */
static void
gda_report_engine_class_init (GdaReportEngineClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	/* report methods */
	object_class->dispose = gda_report_engine_dispose;

	/* Properties */
        object_class->set_property = gda_report_engine_set_property;
        object_class->get_property = gda_report_engine_get_property;

	g_object_class_install_property (object_class, PROP_SPEC_NODE,
                                         g_param_spec_pointer ("spec", NULL, NULL, G_PARAM_WRITABLE | G_PARAM_READABLE));
	g_object_class_install_property (object_class, PROP_SPEC_STRING,
					 g_param_spec_string ("spec-string", NULL, NULL, NULL, G_PARAM_WRITABLE));
	g_object_class_install_property (object_class, PROP_SPEC_FILE,
					 g_param_spec_string ("spec-filename", NULL, NULL, NULL, G_PARAM_WRITABLE));
	/**
	 * GdaReportEngine::output-directory:
	 *
	 * Defines the directory in which any file generated by the report engine will be stored;
	 * if not specified the current working directory is used.
	 *
	 * Since: 5.0.3
	 */
	g_object_class_install_property (object_class, PROP_OUTPUT_DIR,
					 g_param_spec_string ("output-directory", NULL, NULL,
							      NULL, G_PARAM_WRITABLE | G_PARAM_READABLE));
}

static void
gda_report_engine_init (GdaReportEngine *eng)
{
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (eng);
	priv->objects = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
	priv->output_dir = NULL;
}

static void
gda_report_engine_dispose (GObject *object)
{
	GdaReportEngine *eng = (GdaReportEngine *) object;

	g_return_if_fail (GDA_IS_REPORT_ENGINE (eng));
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (eng);

	/* free memory */
	if (priv->objects) {
		g_hash_table_destroy (priv->objects);
		priv->objects = NULL;
	}

	if (priv->doc) {
		xmlFreeDoc (priv->doc);
		priv->doc = NULL;
	}

	if (priv->spec) {
		xmlFreeNode (priv->spec);
		priv->spec = NULL;
	}
	if (priv->output_dir) {
		g_free (priv->output_dir);
		priv->output_dir = NULL;
	}

	/* chain to parent class */
	G_OBJECT_CLASS (gda_report_engine_parent_class)->dispose (object);
}


static void
gda_report_engine_set_property (GObject *object,
				guint param_id,
				const GValue *value,
				GParamSpec *pspec)
{
	GdaReportEngine *eng;

	eng = GDA_REPORT_ENGINE (object);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (eng);

	switch (param_id) {
		case PROP_SPEC_NODE: {
			if (priv->spec) {
				xmlFreeNode (priv->spec);
				priv->spec = NULL;
			}
			priv->spec = g_value_get_pointer (value);
			break;
		}
		case PROP_SPEC_STRING: {
			xmlDocPtr doc;
			
			doc = xmlParseDoc (BAD_CAST g_value_get_string (value));
			if (doc) {
				if (priv->spec)
					xmlFreeNode (priv->spec);
				priv->spec = xmlDocGetRootElement (doc);
				xmlUnlinkNode (priv->spec);
				if (priv->doc)
					xmlFreeDoc (priv->doc);
				priv->doc = doc;
			}
			break;
		}
		case PROP_SPEC_FILE: {
			xmlDocPtr doc;
			
			doc = xmlParseFile (g_value_get_string (value));
			if (doc) {
				if (priv->spec)
					xmlFreeNode (priv->spec);
				priv->spec = xmlDocGetRootElement (doc);
				xmlUnlinkNode (priv->spec);
				if (priv->doc)
					xmlFreeDoc (priv->doc);
				priv->doc = doc;
			}
			break;
		}
		case PROP_OUTPUT_DIR:
			g_free (priv->output_dir);
			priv->output_dir = g_value_dup_string (value);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
			break;
	}
}

static void
gda_report_engine_get_property (GObject *object,
				guint param_id,
				GValue *value,
				GParamSpec *pspec)
{
	GdaReportEngine *eng;

	eng = GDA_REPORT_ENGINE (object);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (eng);
	switch (param_id) {
		case PROP_SPEC_NODE: 
			g_value_set_pointer (value, priv->spec);
			break;
		case PROP_OUTPUT_DIR:
			g_value_set_string (value, priv->output_dir);
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
			break;
	}
}

/**
 * gda_report_engine_new
 * @spec_node: a #xmlNodePtr node
 *
 * Creates a new #GdaReportEngine using @spec_node as the specifications. Note that @spec_node is
 * not copied, but "stolen" by the new report engine object.
 *
 * Returns: a new #GdaReportEngine object
 */
GdaReportEngine *
gda_report_engine_new (xmlNodePtr spec_node)
{
        return (GdaReportEngine *) g_object_new (GDA_TYPE_REPORT_ENGINE, "spec", spec_node, NULL);
}

/**
 * gda_report_engine_new_from_string
 * @spec_string: an XML string
 *
 * Creates a new #GdaReportEngine using @spec_string as the specifications.
 *
 * Returns: a new #GdaReportEngine object
 */
GdaReportEngine *
gda_report_engine_new_from_string (const gchar *spec_string)
{	
	return (GdaReportEngine *) g_object_new (GDA_TYPE_REPORT_ENGINE, "spec_string", spec_string, NULL);
}

/**
 * gda_report_engine_new_from_file
 * @spec_file_name: a file name
 *
 * Creates a new #GdaReportEngine using the contents of @spec_file_name as the specifications.
 *
 * Returns: a new #GdaReportEngine object
 */
GdaReportEngine *
gda_report_engine_new_from_file (const gchar *spec_file_name)
{
	return (GdaReportEngine *) g_object_new (GDA_TYPE_REPORT_ENGINE, "spec-filename", spec_file_name, NULL);
}

/**
 * gda_report_engine_declare_object
 * @engine: a #GdaReportEngine object
 * @object: a #GObject to declare
 * @obj_name: the name to give to @object within @engine
 *
 * Declares an object which will be used in @engine, referenced by the @obj_name name.
 *
 * @object must be of a supported types, that it must be a #GdaConnection, #GdaStatement or #GdaHolder object.
 */
void
gda_report_engine_declare_object (GdaReportEngine *engine, GObject *object, const gchar *obj_name)
{
	gchar prefix, *real_name;
	GObject *current_obj;
	g_return_if_fail (GDA_IS_REPORT_ENGINE (engine));
	g_return_if_fail (G_IS_OBJECT (object));
	g_return_if_fail (obj_name);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (engine);

	if (GDA_IS_STATEMENT (object))
		prefix = 'S';
	else if (GDA_IS_CONNECTION (object))
		prefix = 'C';
	else if (GDA_IS_HOLDER (object))
		prefix = 'H';
	else {
		g_warning (_("Object type '%s' cannot be declared in this context"), G_OBJECT_TYPE_NAME (object));
		return;
	}
	
	real_name = g_strdup_printf ("%c%s", prefix, obj_name);
	current_obj = g_hash_table_lookup (priv->objects, real_name);
	if (current_obj) {
		if (current_obj != object) 
			g_warning (_("An object with the '%s' name has already been declared"), obj_name);
	}
	else {
		/*g_print ("%s(): declared %p as %s\n", __FUNCTION__, object, real_name);*/
		g_hash_table_insert (priv->objects, real_name, object);
		g_object_ref (object);
	}
}

/**
 * gda_report_engine_find_declared_object
 * @engine: a #GdaReportEngine object
 * @obj_type: the type of requested object
 * @obj_name: the name (in @engine) of the object to find
 *
 * Finds an object which has previously been declared using gda_report_engine_declare_object().
 *
 * Returns: a pointer to the requested object, or %NULL if not found
 */
GObject *
gda_report_engine_find_declared_object (GdaReportEngine *engine, GType obj_type, const gchar *obj_name)
{
	gchar prefix, *real_name;
	GObject *current_obj;
	g_return_val_if_fail (GDA_IS_REPORT_ENGINE (engine), NULL);
	g_return_val_if_fail (obj_name, NULL);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (engine);

	if (obj_type == GDA_TYPE_STATEMENT)
		prefix = 'S';
	else if (obj_type == GDA_TYPE_CONNECTION)
		prefix = 'C';
	else if (obj_type == GDA_TYPE_HOLDER)
		prefix = 'H';
	else {
		g_warning (_("Object type '%s' cannot be requested in this context"), g_type_name (obj_type));
		return NULL;
	}
	real_name = g_strdup_printf ("%c%s", prefix, obj_name);
	current_obj = g_hash_table_lookup (priv->objects, real_name);
	/*g_print ("%s(): requested %s => %p\n", __FUNCTION__, real_name, current_obj);*/
	g_free (real_name);
	return current_obj;
}

typedef struct _RunContext RunContext;
static gboolean real_run_at_node (GdaReportEngine *engine, xmlNodePtr topnode, RunContext *context, GError **error);

/**
 * gda_report_engine_run_as_node
 * @engine: a #GdaReportEngine object
 * @error: a place to store errors, or %NULL
 *
 * Execute the @engine engine and creates a new xmlNodePtr XML node
 *
 * Returns: a new xmlNodePtr or %NULL if an error occurred
 */
xmlNodePtr 
gda_report_engine_run_as_node (GdaReportEngine *engine, GError **error)
{
	xmlNodePtr retnode;
	g_return_val_if_fail (GDA_IS_REPORT_ENGINE (engine), NULL);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (engine);
	g_return_val_if_fail (priv->spec, NULL);

	retnode = xmlCopyNode (priv->spec, 1);
	if (!real_run_at_node (engine, retnode, NULL, error)) {
		xmlFreeNode (retnode);
		retnode = NULL;
	}
	return retnode;
}

/**
 * gda_report_engine_run_as_doc
 * @engine: a #GdaReportEngine object
 * @error: a place to store errors, or %NULL
 *
 * Execute the @engine engine and creates a new XML document
 *
 * Returns: a new xmlDocPtr or %NULL if an error occurred
 */
xmlDocPtr
gda_report_engine_run_as_doc (GdaReportEngine *engine, GError **error)
{
	xmlNodePtr retnode;
	xmlDocPtr doc;
	g_return_val_if_fail (GDA_IS_REPORT_ENGINE (engine), NULL);
	GdaReportEnginePrivate *priv = gda_report_engine_get_instance_private (engine);
	g_return_val_if_fail (priv->spec, NULL);

	retnode = gda_report_engine_run_as_node (engine, error);
	if (!retnode)
		return NULL;
	if (priv->doc)
		doc = xmlCopyDoc (priv->doc, 1);
	else 
		doc = xmlNewDoc (BAD_CAST "1.0");
	xmlDocSetRootElement (doc, retnode);
	return doc;
}

struct _RunContext {
	RunContext         *parent;

	GdaConnection      *cnc;
	GdaStatement       *stmt;
	GdaDataModel       *model;
	GdaDataModelIter   *iter;
};
static GdaHolder *run_context_find_param (GdaReportEngine *engine, RunContext *context, const xmlChar *name);
static GdaStatement *run_context_find_stmt (GdaReportEngine *engine, RunContext *context, const xmlChar *name);
static GdaConnection *run_context_find_connection (GdaReportEngine *engine, RunContext *context, const xmlChar *name);

static gboolean command_gda_report_section_run (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
						RunContext *context, GError **error);
static gboolean command_gda_report_iter_run (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
					     RunContext *context, GError **error);
static gboolean command_gda_report_param_value (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
						RunContext *context, GError **error);
static gboolean command_gda_report_eval_expr (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
					      RunContext *context, GError **error);
static gboolean command_gda_report_if (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
				       RunContext *context, GError **error);

static GdaStatement *rewrite_statement (GdaReportEngine *engine, RunContext *context, GdaStatement *stmt, GError **error);
static gboolean assign_parameters_values (GdaReportEngine *engine, RunContext *context, GdaSet *plist, GError **error);
static GValue *evaluate_expression (GdaReportEngine *engine, RunContext *context, const gchar *expr, GError **error);
static xmlNodePtr value_to_node (GdaReportEngine *engine, RunContext *context, const GValue *value, GdaSet *options);

/*
 * Function to be called when a "gda_report_..." node is found
 * NOTE:
 *   - the XML tree CAN be modified _at and below_ the xmlNodePtr as 2nd arg.
 *   - the XML tree CANNOT be modified _above_ it
 *   - any new child added to the list as 3rd argument will be added _before_ the node as the 2dn arg,
 *     those children _must not_ be inserted in any tree.
 */
typedef gboolean (*EngineCommandFunc) (GdaReportEngine *, xmlNodePtr, GSList **nodes, RunContext *, GError **);
typedef struct _EngineCommand {
	gchar             *tag_name;
	EngineCommandFunc  func; /* can be %NULL */
	gboolean           has_prefix; /* TRUE if @tag_name starts with "gda_report_" */
} EngineCommand;

EngineCommand commands[] = {
	{"gda_report_section", command_gda_report_section_run, TRUE},
	{"gda_report_iter", command_gda_report_iter_run, TRUE},
	{"gda_report_param_value", command_gda_report_param_value, TRUE},
	{"gda_report_query", NULL, TRUE},
	{"gda_report_expr", command_gda_report_eval_expr, TRUE},
	{"gda_report_if", command_gda_report_if, TRUE},
};

/*
 * Interprets nodes from @topnode and for all of @topnode's next siblings 
 */
static gboolean
real_run_at_node (GdaReportEngine *engine, xmlNodePtr topnode, RunContext *context, GError **error)
{
	xmlNodePtr node;
	xmlNodePtr next_node = NULL;
	for (node = topnode; node; node = next_node) {
		next_node = node->next;

		if (!strncmp ((gchar *) node->name, "gda_report", 10)) {
			GSList *created_nodes = NULL;
			gboolean cmd_res = TRUE;
			gsize i;
			gboolean command_found = FALSE;
			
			for (i = 0; i < sizeof (commands) / sizeof (EngineCommand); i++) {
				EngineCommand *ec = (EngineCommand *) &(commands [i]);
				if (ec->has_prefix) {
					if (!strcmp (((gchar *) node->name) + 10, ec->tag_name + 10)) {
						command_found = TRUE;
						if (ec->func)
							cmd_res = (ec->func) (engine, node, &created_nodes, context, error);
						break;
					}
				}
			}
			if (!command_found) {
				/* gda_ node not implemented */
				TO_IMPLEMENT;
				g_warning ("Engine command '%s' is not yet implemented", (gchar *) node->name);
			}

			if (created_nodes) {
				/* put @node's contents before @node */
				GSList *list;
				for (list = created_nodes; list; list = list->next) {
					next_node = xmlAddPrevSibling (node, (xmlNodePtr) list->data);
					g_assert (next_node);
				}
				g_slist_free (created_nodes);

				/* destroy @node */
				xmlUnlinkNode (node);
				xmlFreeNode (node);
				next_node = next_node->next;
			} 
			else {
				/* destroy @node */
				xmlUnlinkNode (node);
				xmlFreeNode (node);
			}
			if (!cmd_res)
				return FALSE;
		}
		else if (node->children) {
			if (!real_run_at_node (engine, node->children, context, error))
				return FALSE;
		}
	}
	return TRUE;
}

/*
 * Commands implementation
 */
static RunContext *run_context_push_with_stmt (GdaReportEngine *engine, RunContext *context, 
					       GdaConnection *cnc, GdaStatement *stmt, const gchar *stmt_name,  
					       GError **error);
static void        run_context_pop (GdaReportEngine *engine, RunContext *context);

/*
 * COMMAND: <gda_report_section>
 *
 * creates a new RunContext, then executes its children, and destroys the RunContext
 *
 * uses node's contents: yes
 * requested attributes: "query_name"
 *
 * REM: either "query_name" or a <gda_report_query> sub node must be provided to create a data model.
 */
static gboolean
command_gda_report_section_run (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
				RunContext *context, GError **error)
{	
	xmlChar *qname;
	RunContext *ctx = NULL;
	
	/* compute stmt to push a RunContext */
	qname = xmlGetProp (node, BAD_CAST "query_name");
	if (qname) {
		GdaStatement *stmt;
		GdaConnection *cnc = NULL;
		xmlChar *prop;

		/* find statement */
		stmt = run_context_find_stmt (engine, context, qname);
		if (!stmt) {
			g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
				     _("Unknown query '%s'"), qname);
			xmlFree (qname);
			return FALSE;
		}

		/* find which connection to use */
		prop = xmlGetProp (node, BAD_CAST "cnc_name");
		cnc = run_context_find_connection (engine, context, prop);
		if (!cnc) {
			if (prop) {
				g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
					     _("No connection named '%s' found"), prop);
				xmlFree (prop);
			}
			else
				g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR, "%s",
					     _("No connection specified"));
			return FALSE;
		}

		ctx = run_context_push_with_stmt (engine, context, cnc, stmt, (gchar *) qname, error);
		xmlFree (qname);
	}
	else {
		xmlNodePtr child;
		for (child = node->children; child; child = child->next) {
			if (!strcmp ((gchar *) child->name, "gda_report_query")) {
				GdaStatement *stmt;
				xmlChar *prop;
				GdaConnection *cnc = NULL;
				GdaSqlParser *parser = NULL;

				/* find which connection to use */
				prop = xmlGetProp (child, BAD_CAST "cnc_name");
				cnc = run_context_find_connection (engine, context, prop);
				if (!cnc) {
					if (prop) {
						g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
							     _("No connection named '%s' found"), prop);
						xmlFree (prop);
					}
					else
						g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR, "%s",
							     _("No connection specified"));
					return FALSE;
				}
				
				/* parser object */
				parser = g_object_get_data (G_OBJECT (cnc), "__gda_parser");
				if (!parser) {
					parser = gda_connection_create_parser (cnc);
					g_object_set_data_full (G_OBJECT (cnc), "__gda_parser", 
								parser, g_object_unref);
				}
				
				/* statement */
				stmt = gda_sql_parser_parse_string (parser, (gchar *) xmlNodeGetContent (child), 
								    NULL, error);
				if (!stmt)
					return FALSE;

				qname = xmlGetProp (child, BAD_CAST "query_name");
				if (qname) {
					ctx = run_context_push_with_stmt (engine, context, cnc,
									  stmt, (gchar *) qname, error);
					xmlFree (qname);
				}
				else
					ctx = run_context_push_with_stmt (engine, context, cnc,
									  stmt, "context", error);
				if (ctx)
					ctx->stmt = stmt;
				else {
					g_object_unref (stmt);
					return FALSE;
				}
				break;
			}
		}
	}

	if (!ctx) {
		g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR, "%s",
			     _("Query is not specified (not named and not defined)"));
		return FALSE;
	}
	
	/* go for the children while iterating */
	if (!real_run_at_node (engine, node->children, ctx, error)) {
		run_context_pop (engine, ctx);
		return FALSE;
	}
	else {
		xmlNodePtr child;
		for (child = node->children; child; child = node->children) {
			xmlUnlinkNode (child);
			*created_nodes = g_slist_prepend (*created_nodes, child);
		}
	}

	*created_nodes = g_slist_reverse (*created_nodes);
	run_context_pop (engine, ctx);

	return TRUE;
}

/*
 * COMMAND: <gda_report_section>
 *
 * Creates copies of its contents, one copy per row in the new run context's
 * data model.
 *
 * uses node's contents: yes
 * requested attributes: none
 *
 * REM: either "query_name" or a <gda_report_query> sub node must be provided to create a data model.
 */
static gboolean
command_gda_report_iter_run (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
			     RunContext *context, GError **error)
{
	if (!context || !context->iter)
		return TRUE;

	gda_data_model_iter_move_next (context->iter);
	while (gda_data_model_iter_is_valid (context->iter)) {
		xmlNodePtr dup, child;
		dup = xmlCopyNode (node, 1);
				
		if (!real_run_at_node (engine, dup->children, context, error)) {
			xmlFreeNode (dup);
			return FALSE;
		}
		else {
			for (child = dup->children; child; child = dup->children) {
				xmlUnlinkNode (child);
				*created_nodes = g_slist_prepend (*created_nodes, child);
			}
		}
		xmlFreeNode (dup);
		gda_data_model_iter_move_next (context->iter);
	}

	*created_nodes = g_slist_reverse (*created_nodes);

	return TRUE;
}

/*
 * run_context_push
 *
 * Add a new run context level below @context, running @stmt to create a data model
 */
static RunContext *
run_context_push_with_stmt (GdaReportEngine *engine, RunContext *context, GdaConnection *cnc, 
			    GdaStatement *stmt, const gchar *stmt_name, GError **error)
{
	GdaDataModel *model;
	GdaSet *plist;
	GdaStatement *lstmt;
	lstmt = rewrite_statement (engine, context, stmt, error);
	if (!lstmt)
		return NULL;

	g_assert (cnc);
	if (!gda_statement_get_parameters (lstmt, &plist, error)) {
		g_object_unref (lstmt);
		return NULL;
	}

	if (plist && !assign_parameters_values (engine, context, plist, error)) {
		g_object_unref (plist);
		g_object_unref (lstmt);
		return NULL;
	}
	model = gda_connection_statement_execute_select (cnc, lstmt, plist, error);
	if (plist)
		g_object_unref (plist);
	g_object_unref (lstmt);
	if (!model) 
		return NULL;
	g_object_set_data_full (G_OBJECT (model), "name", g_strdup (stmt_name), g_free);
		
	/* add a parameter for the number of rows, attached to model */
	GdaHolder *param;
	GValue *value;
	gchar *name;
	param = gda_holder_new (G_TYPE_INT, "id");
	value = gda_value_new (G_TYPE_INT);
	g_value_set_int (value, gda_data_model_get_n_rows  (model));
	if (! gda_holder_set_value (param, value, error)) {
		g_object_unref (param);
		gda_value_free (value);
		return NULL;
	}
	gda_value_free (value);
	name = g_strdup_printf ("%s|?nrows", stmt_name);
	g_object_set_data_full (G_OBJECT (model), name, param, g_object_unref);
	g_free (name);

	/* create a new RunContext */
	RunContext *ctx;
	ctx = g_new0 (RunContext, 1);
	ctx->stmt = NULL;
	ctx->cnc = cnc;
	ctx->parent = context;
	ctx->model = model;
	ctx->iter = gda_data_model_create_iter (model);

	/*g_print (">>>> PUSH CONTEXT %p\n", ctx);*/
	return ctx;
}

/*
 * Frees any memory associated to @context
 */
static void
run_context_pop (G_GNUC_UNUSED GdaReportEngine *engine, RunContext *context)
{
	/*g_print ("<<<< POP CONTEXT %p\n", context);*/
	if (context->stmt)
		g_object_unref (context->stmt);
	g_object_unref (context->iter);
	g_object_unref (context->model);
	g_free (context);
}

/*
 * COMMAND: <gda_report_param_value>
 *
 * Creates a new text node containing the value of the parameter named from "param_name"
 *
 * uses node's contents: no
 * requested attributes: param_name
 * optional attributes: converter => use "richtext::docbook"
 */
static gboolean
command_gda_report_param_value (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
				RunContext *context, GError **error)
{
	xmlChar *prop;
	prop = xmlGetProp (node, BAD_CAST "param_name");
	if (prop) {
		GdaHolder *param;
		
		param = run_context_find_param (engine, context, prop);
		if (!param) {
			g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
				     _("Unknown parameter '%s'"), prop);
			return FALSE;
		}
		else {
			/* Add a text node */
			const GValue *value;
			xmlNodePtr child;
			GdaSet *options = NULL;
			xmlChar *cprop;

			cprop = xmlGetProp (node, BAD_CAST "converter");
			if (cprop) {
				options = gda_set_new_inline (1, "converter", G_TYPE_STRING, (gchar*) cprop);
				xmlFree (cprop);
			}
			value = gda_holder_get_value (param);
			child = value_to_node (engine, context, value, options);
			if (options)
				g_object_unref (options);
			*created_nodes = g_slist_prepend (NULL, child);
		}
		xmlFree (prop);
		return TRUE;
	}
	else {
		g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR, "%s",
			     _("Parameter name not specified"));
		return FALSE;
	}
}

/*
 * COMMAND: <gda_report_expr>
 *
 * Creates a text node containing the evaluation of the textual contents of the node
 *
 * uses node's contents: yes
 * requested attributes: none
 */
static gboolean 
command_gda_report_eval_expr (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
			      RunContext *context, GError **error)
{
	GValue *value;
	xmlNodePtr child;

	value = evaluate_expression (engine, context, 
				     (const gchar *) xmlNodeGetContent (node), error);
	if (!value)
		return FALSE;
	child = value_to_node (engine, context, value, NULL);
	*created_nodes = g_slist_prepend (NULL, child);

	return TRUE;
}

/*
 * COMMAND: <gda_report_if>
 *
 * Creates a copy of the children of the <gda_report_if_true> or <gda_report_if_false> nodes depending on the
 * result of the execution of the expression
 *
 * uses node's contents: yes
 * requested attributes: none
 */
static gboolean 
command_gda_report_if (GdaReportEngine *engine, xmlNodePtr node, GSList **created_nodes,
		       RunContext *context, GError **error)
{
	GValue *value;
	gboolean expr_is_true = FALSE;
	xmlChar *prop;

	prop = xmlGetProp (node, BAD_CAST "expr");
	if (!prop) {
		g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR, "%s",
			     _("No expression specified")); 
		return FALSE;
	}

	/* evaluate expression as boolean */
	value = evaluate_expression (engine, context, 
				     (const gchar *) prop, error);
	if (!value)
		return FALSE;
	xmlFree (prop);
	if (value && !gda_value_is_null (value)) {
		if (G_VALUE_TYPE (value) == G_TYPE_BOOLEAN)
			expr_is_true = g_value_get_boolean (value);
		else {
			GValue *trans;
			trans = gda_value_new (G_TYPE_BOOLEAN);
			if (g_value_transform (value, trans)) {
				expr_is_true = g_value_get_boolean (trans);
				gda_value_free (trans);
			}
			else {
				gda_value_free (trans);
				g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
					     _("Cannot cast value from type '%s' to type '%s'"), 
					     g_type_name (G_VALUE_TYPE (value)), g_type_name (G_TYPE_BOOLEAN));
				return FALSE;
			}
		}
		gda_value_free (value);
	}
	/*g_print ("IF Expression evaluates to %d\n", expr_is_true);*/

	/* find the correct sub node: <gda_report_if_true> or <gda_report_if_false> */
	gchar *sub_node_name;
	xmlNodePtr sub_node;
	sub_node_name = expr_is_true ? "gda_report_if_true" : "gda_report_if_false";
	for (sub_node = node->children; sub_node; sub_node = sub_node->next) {
		if (!strcmp ((gchar *) sub_node->name, sub_node_name)) {
			if (!real_run_at_node (engine, sub_node->children, context, error)) 
				return FALSE;
			else {
				xmlNodePtr child;
				for (child = sub_node->children; child; child = sub_node->children) {
					xmlUnlinkNode (child);
					*created_nodes = g_slist_prepend (*created_nodes, child);
				}
			}
		}
	}

	*created_nodes = g_slist_reverse (*created_nodes);

	return TRUE;
}

typedef struct {
	GdaReportEngine *engine;
	RunContext *context;
} ForeachData;

static gboolean
rewrite_statement_foreach_func (GdaSqlAnyPart *node, ForeachData *fdata, GError **error)
{
	GdaSqlParamSpec *pspec;
	if (!node) return TRUE;

	if ((node->type == GDA_SQL_ANY_EXPR) &&
	    (pspec = ((GdaSqlExpr*) node)->param_spec)) {
		if (pspec->g_type != GDA_TYPE_NULL)
			return TRUE;

		GdaHolder *source_param;
		source_param = run_context_find_param (fdata->engine, fdata->context, BAD_CAST pspec->name);
		if (!source_param) {
			g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
				     _("Unknown parameter '%s'"), pspec->name);
			return FALSE;
		}
		pspec->g_type = gda_holder_get_g_type (source_param);
	}
	return TRUE;
}

/*
 * rewrite_statement
 *
 * If @stmt contains some variables for which the g_type is undefined, try to adjust it
 *
 * Returns: a new #GdaStatement if no error occurred
 */
static GdaStatement *
rewrite_statement (GdaReportEngine *engine, RunContext *context,
		   GdaStatement *stmt, GError **error)
{
	ForeachData fdata;
	fdata.engine = engine;
	fdata.context = context;

	GdaSqlStatement *sql_st;
	g_object_get (G_OBJECT (stmt), "structure", &sql_st, NULL);
	
	if (!gda_sql_any_part_foreach (GDA_SQL_ANY_PART (sql_st->contents),
				       (GdaSqlForeachFunc) rewrite_statement_foreach_func, &fdata, error)) {
		gda_sql_statement_free (sql_st);
		return NULL;
	}

	GdaStatement *out_stmt;
	out_stmt = (GdaStatement*) g_object_new (GDA_TYPE_STATEMENT, "structure", sql_st, NULL);
	gda_sql_statement_free (sql_st);

	return out_stmt;
}

/*
 * assign_parameters_values
 *
 * Tries to assign a value for each parameter in @plist, from the context
 *
 * Returns: TRUE on success
 */
static gboolean
assign_parameters_values (GdaReportEngine *engine, RunContext *context, GdaSet *plist, GError **error)
{
	if (plist) {
		GSList *list;
		for (list = gda_set_get_holders (plist); list; list = list->next) {
			GdaHolder *source_param;
			source_param = run_context_find_param (engine, context, 
							       BAD_CAST gda_holder_get_id (GDA_HOLDER (list->data)));
			if (!source_param) {
				g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
					     _("Unknown parameter '%s'"), 
					     gda_holder_get_id (GDA_HOLDER (list->data)));
				return FALSE;
			}
			gda_holder_set_not_null (GDA_HOLDER (list->data), FALSE);

			GType ptype, source_ptype;
			ptype = gda_holder_get_g_type (GDA_HOLDER (list->data));
			source_ptype = gda_holder_get_g_type (source_param);
			if (ptype == source_ptype) {
				if (! gda_holder_set_bind (GDA_HOLDER (list->data), source_param, error))
					return FALSE;
			}
			else {
				const GValue *source_value;
				source_value = gda_holder_get_value (source_param);
				if (source_value && !gda_value_is_null (source_value)) {
					GValue *trans;
					trans = gda_value_new (ptype);
					if (g_value_transform (source_value, trans)) {
						if (! gda_holder_set_value (GDA_HOLDER (list->data), trans, error)) {
							gda_value_free (trans);
							return FALSE;
						}
						gda_value_free (trans);
					}
					else {
						gda_value_free (trans);
						g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
							     _("Cannot cast parameter from type '%s' to type '%s'"), 
							     g_type_name (source_ptype), g_type_name (ptype));
						return FALSE;
					}
				}
				else
					if (! gda_holder_set_value (GDA_HOLDER (list->data), NULL, error))
						return FALSE;
			}
		}
	}
	return TRUE;
}

/*
 * evaluate_expression
 *
 * Evaluates the @expr expression, which must be a valid SQLite expression
 *
 * Returns: a new GValue if no error occurred
 */
static GValue *
evaluate_expression (GdaReportEngine *engine, RunContext *context, const gchar *expr, GError **error)
{
	GdaStatement *stmt;
	GdaSet *plist;
	GdaDataModel *model;
	GValue *retval;
	gchar *sql;
	static GMutex init_mutex;
	GdaConnection *vcnc = NULL;
	GdaSqlParser *parser;

	/* create a virtual connection to execute the expression, if it's the first time */
	g_mutex_lock (&init_mutex);
	if (!vcnc) {
		static GdaVirtualProvider *provider = NULL;

		if (!provider)
			provider = gda_vprovider_data_model_new ();
		vcnc = gda_virtual_connection_open (provider, GDA_CONNECTION_OPTIONS_NONE, error);
		if (! vcnc) {
			g_mutex_unlock (&init_mutex);
			return NULL;
		}
	}
	g_mutex_unlock (&init_mutex);

	/* parser */
	parser = g_object_get_data (G_OBJECT (context->cnc), "__gda_parser");
	if (!parser) {
		parser = gda_connection_create_parser (context->cnc);
		g_object_set_data_full (G_OBJECT (context->cnc), "__gda_parser", 
					parser, g_object_unref);
	}
	
	/* create the stmt 
	 * REM: SQL injection is prevented because only the first statement is kept by GdaStatement 
	 */
	sql = g_strdup_printf ("SELECT %s", expr);
	stmt = gda_sql_parser_parse_string (parser, sql, NULL, error);
	if (!stmt)
		return NULL;

	GdaStatement *lstmt;
	lstmt = rewrite_statement (engine, context, stmt, error);
	g_object_unref (stmt);
	if (!lstmt)
		return NULL;
	
	stmt = lstmt;
	if (!gda_statement_get_parameters (stmt, &plist, error)) {
		g_object_unref (stmt);
		return NULL;
	}

	if (plist) {
		if (!assign_parameters_values (engine, context, plist, error)) {
			g_object_unref (stmt);
			g_object_unref (plist);
			return NULL;
		}
	}

	model = gda_connection_statement_execute_select (vcnc, stmt, plist, error);
	if (plist)
		g_object_unref (plist);
	g_object_unref (stmt);
	if (!model) 
		return NULL;

	if (gda_data_model_get_n_rows (model) != 1) {
		g_set_error (error, GDA_REPORT_ENGINE_ERROR, GDA_REPORT_ENGINE_GENERAL_ERROR,
			     _("Expression '%s' should return exactly one value"), expr);
		return NULL;
	}
	retval = (GValue *) gda_data_model_get_value_at (model, 0, 0, error);
	if (retval)
		retval = gda_value_copy (retval);
	g_object_unref (model);
	return retval;
}

static guint
gtype_hash (gconstpointer key)
{
        return GPOINTER_TO_UINT (key);
}

static gboolean
gtype_equal (gconstpointer a, gconstpointer b)
{
        return (GType) a == (GType) b ? TRUE : FALSE;
}

/*
 * value_to_node
 *
 * Converts @value to a string
 */
static xmlNodePtr
value_to_node (GdaReportEngine *engine, G_GNUC_UNUSED RunContext *context, const GValue *value, GdaSet *options)
{
	xmlNodePtr retnode = NULL;
	GdaHolder *converter = NULL;
	if (options)
		converter = gda_set_get_holder (options, "converter");

	if (!value || gda_value_is_null (value))
		retnode = xmlNewText (BAD_CAST "");
	else if ((G_VALUE_TYPE (value) == GDA_TYPE_BINARY) ||
		 (G_VALUE_TYPE (value) == GDA_TYPE_BLOB)) {
		TO_IMPLEMENT;
		retnode = xmlNewText (BAD_CAST _("Binary data"));
	}
	else {
		GdaDataHandler *dh;
		gchar *str;

		if (!data_handlers) {
			/* initialize the internal data handlers */
			data_handlers = g_hash_table_new_full (gtype_hash, gtype_equal,
                                                               NULL, (GDestroyNotify) g_object_unref);
			
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_INT64, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_UINT64, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_BOOLEAN, gda_handler_boolean_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_DATE, gda_handler_time_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_DOUBLE, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_INT, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) GDA_TYPE_NUMERIC, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_FLOAT, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) GDA_TYPE_SHORT, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) GDA_TYPE_USHORT, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_STRING, gda_handler_string_new ());
			g_hash_table_insert (data_handlers, (gpointer) GDA_TYPE_TIME, gda_handler_time_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_DATE_TIME, gda_handler_time_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_CHAR, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_UCHAR, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_ULONG, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_LONG, gda_handler_numerical_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_GTYPE, gda_handler_type_new ());
			g_hash_table_insert (data_handlers, (gpointer) G_TYPE_UINT, gda_handler_numerical_new ());
		}

		gboolean converted = FALSE;

		dh = g_hash_table_lookup (data_handlers, (gpointer) G_VALUE_TYPE (value));
		if (dh) 
			str = gda_data_handler_get_str_from_value (dh, value);
		else
			str = gda_value_stringify (value);
		if (converter) {
			const GValue *cvalue;
			cvalue = gda_holder_get_value (converter);
			if ((G_VALUE_TYPE (cvalue) == G_TYPE_STRING) && g_value_get_string (cvalue)) {
				gchar **array;
				array = g_strsplit (g_value_get_string (cvalue), "::", 0);
				if (array[0] && !strcmp (array[0], "richtext")) {
					if (array[1] && !strcmp (array[1], "docbook")) {
						retnode = xmlNewNode (NULL, BAD_CAST "para");
						parse_rich_text_to_docbook (engine, retnode, str);
						converted = TRUE;
					}
					else if (array[1] && !strcmp (array[1], "html")) {
						retnode = xmlNewNode (NULL, BAD_CAST "p");
						parse_rich_text_to_html (engine, retnode, str);
						converted = TRUE;
					}
				}
			}
		}
		if (!converted)
			retnode = xmlNewText (BAD_CAST (str ? str : ""));

		g_free (str);
	}

	return retnode;
}

/*
 * run_context_find_param
 *
 * Find a parameter in the context
 */
static GdaHolder *
run_context_find_param (GdaReportEngine *engine, RunContext *context, const xmlChar *name)
{
	GdaHolder *retval = NULL;
	RunContext *ctx;
	gchar **array;

	/*g_print ("%s (context=%p, name=%s) ", __FUNCTION__, context, name);*/
	array = g_strsplit ((gchar *) name, "|", 0);
	if (g_strv_length (array) == 2) {
		gchar *model_name = array [0];
		gchar *col_name = array [1];
		/* search through the contexts for a [data model]/[column name] parameter */
		for (ctx = context; ctx; ctx = ctx->parent) {
			if (!ctx->model || !ctx->iter)
				continue;
			const gchar *obj_name;
			obj_name = g_object_get_data (G_OBJECT (ctx->model), "name");
			if (obj_name && !strcmp (obj_name, model_name)) {
				if (*col_name == '@')
					retval = gda_set_get_holder (GDA_SET (ctx->iter), col_name + 1);
				else if (*col_name == '#') {
					gint nth = atoi (col_name + 1);
					retval = g_slist_nth_data (gda_set_get_holders (GDA_SET (ctx->iter)), nth);
				}
				else 
					retval = g_object_get_data (G_OBJECT (ctx->model), (gchar*) name);
				break;
			}
		}
	}
	g_strfreev (array);

	/* try among the engine's declared parameters */
	if (!retval) 
		retval = (GdaHolder *) gda_report_engine_find_declared_object (engine, GDA_TYPE_HOLDER, 
									       (gchar *) name);

	/*g_print ("=> %p\n", retval);*/
	return retval;
}

/*
 * run_context_find_stmt
 *
 * Find a stmt in the context
 */
static GdaStatement *
run_context_find_stmt (GdaReportEngine *engine, RunContext *context, const xmlChar *name)
{
	/* search through the contexts */
	RunContext *ctx;
	for (ctx = context; ctx; ctx = ctx->parent) {
		if (ctx->stmt) {
			const gchar *qname = g_object_get_data (G_OBJECT (ctx->stmt), "name");
			if (qname && !strcmp (qname, (gchar *)name))
				return ctx->stmt;
		}
	}

	/* search through the list of declared queries */
	return (GdaStatement *) gda_report_engine_find_declared_object (engine, GDA_TYPE_STATEMENT, 
									(gchar *) name);
}

/*
 * run_context_find_connection
 *
 * Find a connection in the context, if connection is not named, use the last one available in a Context 
 */
static GdaConnection *
run_context_find_connection (GdaReportEngine *engine, RunContext *context, const xmlChar *name)
{
	/* use connection's name if provided */
	if (name) 
		return (GdaConnection *) gda_report_engine_find_declared_object (engine, GDA_TYPE_CONNECTION, 
										 (gchar *) name);
	else if (context)
		return context->cnc;
	else
		return NULL;
}
