/**
 * @file
 * Auxiliary class to handle "session" XML files (augmented Folker).
 * It is a GObject that must be freed using \e g_object_unref.
 */
/*
 * Copyright (C) 2012 Otto-von-Guericke-Universität Magdeburg
 *
 * 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 3 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, see .
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "cclosure-marshallers.h"
#include "experiment-reader.h"
static void experiment_reader_class_init(ExperimentReaderClass *klass);
static void experiment_reader_init(ExperimentReader *klass);
static void experiment_reader_finalize(GObject *gobject);
static gint64 get_timepoint_by_ref(xmlDoc *doc, xmlChar *ref);
static xmlNode *get_first_element(xmlNode *children, const gchar *name);
static xmlNode *get_last_element(xmlNode *children, const gchar *name);
static GClosure *experiment_reader_topic_callback_new(ExperimentReaderTopicCallback,
						      gpointer);
static void experiment_reader_topic_callback_invoke(ExperimentReader *reader,
						    GClosure *closure,
						    const gchar *topic_id,
						    gint64 start_time,
						    gint64 end_time);
static gboolean generic_foreach_topic(ExperimentReader *reader, xmlNodeSet *nodes,
				      GClosure *closure);
static gint experiment_reader_contrib_cmp(const ExperimentReaderContrib *a,
					  const ExperimentReaderContrib *b);
static void insert_contribution(gint64 start_time, gchar *text, GList **list);
static inline void process_contribution(xmlDoc *doc, xmlNode *contrib,
					GList **list);
/** @private */
#define XML_CHAR(STR) \
	((const xmlChar *)(STR))
/** @private */
#define EXPERIMENT_READER_GET_PRIVATE(obj) \
	(G_TYPE_INSTANCE_GET_PRIVATE((obj), EXPERIMENT_TYPE_READER, ExperimentReaderPrivate))
/** @private */
struct _ExperimentReaderPrivate {
	xmlDoc *doc;
};
/**
 * @private
 * Will create \e experiment_reader_get_type and set
 * \e experiment_reader_parent_class
 */
G_DEFINE_TYPE(ExperimentReader, experiment_reader, G_TYPE_OBJECT);
static void
experiment_reader_class_init(ExperimentReaderClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
	/* gobject_class->dispose = experiment_reader_dispose; */
	gobject_class->finalize = experiment_reader_finalize;
	g_type_class_add_private(klass, sizeof(ExperimentReaderPrivate));
}
static void
experiment_reader_init(ExperimentReader *klass)
{
	klass->priv = EXPERIMENT_READER_GET_PRIVATE(klass);
	klass->priv->doc = NULL;
}
static void
experiment_reader_finalize(GObject *gobject)
{
	ExperimentReader *reader = EXPERIMENT_READER(gobject);
	if (reader->priv->doc != NULL)
		xmlFreeDoc(reader->priv->doc);
	/* Chain up to the parent class */
	G_OBJECT_CLASS(experiment_reader_parent_class)->finalize(gobject);
}
static gint64
get_timepoint_by_ref(xmlDoc *doc, xmlChar *ref)
{
	xmlChar expr[255];
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	double value;
	xpathCtx = xmlXPathNewContext(doc);
	assert(xpathCtx != NULL);
	/** @todo precompile XPath expression */
	xmlStrPrintf(expr, sizeof(expr),
		     XML_CHAR("/session/timeline/"
			      "timepoint[@timepoint-id = '%s']/"
			      "@absolute-time"), ref);
	xpathObj = xmlXPathEvalExpression(expr, xpathCtx);
	assert(xpathObj != NULL);
	value = xmlXPathCastToNumber(xpathObj);
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
	return (gint64)(value*1000.);
}
static xmlNode *
get_first_element(xmlNode *children, const gchar *name)
{
	for (xmlNode *cur = children; cur != NULL; cur = cur->next)
		if (cur->type == XML_ELEMENT_NODE &&
		    !g_strcmp0((const gchar *)cur->name, name))
			return cur;
	return NULL;
}
static xmlNode *
get_last_element(xmlNode *children, const gchar *name)
{
	xmlNode *ret = NULL;
	for (xmlNode *cur = children; cur != NULL; cur = cur->next)
		if (cur->type == XML_ELEMENT_NODE &&
		    !g_strcmp0((const gchar *)cur->name, name))
			ret = cur;
	return ret;
}
static GClosure *
experiment_reader_topic_callback_new(ExperimentReaderTopicCallback callback,
				     gpointer data)
{
	GClosure *closure = g_cclosure_new(G_CALLBACK(callback), data, NULL);
	g_closure_set_marshal(closure,
			      experiment_reader_marshal_VOID__STRING_INT64_INT64);
	g_closure_ref(closure);
	g_closure_sink(closure);
	return closure;
}
static void
experiment_reader_topic_callback_invoke(ExperimentReader *reader,
					GClosure *closure,
					const gchar *topic_id,
					gint64 start_time, gint64 end_time)
{
	GValue params[4];
	memset(params, 0, sizeof(params));
	g_value_init(params + 0, G_TYPE_OBJECT);
	g_value_set_object(params + 0, reader);
	g_value_init(params + 1, G_TYPE_STRING);
	g_value_set_string(params + 1, topic_id);
	g_value_init(params + 2, G_TYPE_INT64);
	g_value_set_int64(params + 2, start_time);
	g_value_init(params + 3, G_TYPE_INT64);
	g_value_set_int64(params + 3, end_time);
	g_closure_invoke(closure, NULL, G_N_ELEMENTS(params), params, NULL);
	for (gint i = 0; i < G_N_ELEMENTS(params); i++)
		g_value_unset(params + i);
}
static gboolean
generic_foreach_topic(ExperimentReader *reader, xmlNodeSet *nodes,
		      GClosure *closure)
{
	if (nodes == NULL)
		return TRUE;
	for (int i = 0; i < nodes->nodeNr; i++) {
		xmlNode *cur = nodes->nodeTab[i];
		assert(cur != NULL && cur->type == XML_ELEMENT_NODE);
		xmlNode *first_contrib = get_first_element(cur->children,
							   "contribution");
		xmlNode *last_contrib = get_last_element(cur->children,
							 "contribution");
		xmlChar	*topic_id = xmlGetProp(cur, XML_CHAR("id"));
		gint64	start_time = -1;
		gint64	end_time = -1;
		if (first_contrib != NULL) {
			xmlChar *contrib_start_ref;
			contrib_start_ref = xmlGetProp(first_contrib,
						       XML_CHAR("start-reference"));
			start_time = get_timepoint_by_ref(reader->priv->doc,
							  contrib_start_ref);
			xmlFree(contrib_start_ref);
		}
		if (last_contrib != NULL) {
			xmlChar *contrib_end_ref;
			contrib_end_ref = xmlGetProp(last_contrib,
						     XML_CHAR("end-reference"));
			end_time = get_timepoint_by_ref(reader->priv->doc,
							contrib_end_ref);
			xmlFree(contrib_end_ref);
		}
		experiment_reader_topic_callback_invoke(reader, closure,
							(const gchar *)topic_id,
							start_time, end_time);
		xmlFree(topic_id);
	}
	return FALSE;
}
static gint
experiment_reader_contrib_cmp(const ExperimentReaderContrib *a,
			      const ExperimentReaderContrib *b)
{
	if (a->start_time < b->start_time)
		return -1;
	if (a->start_time > b->start_time)
		return 1;
	return 0;
}
static void
insert_contribution(gint64 start_time, gchar *text, GList **list)
{
	ExperimentReaderContrib *contrib;
	if (text == NULL)
		return;
	contrib = g_malloc(sizeof(ExperimentReaderContrib) + strlen(text) + 1);
	contrib->start_time = start_time;
	g_stpcpy(contrib->text, g_strchomp(text));
	*list = g_list_insert_sorted(*list, contrib,
				     (GCompareFunc)experiment_reader_contrib_cmp);
}
static inline void
process_contribution(xmlDoc *doc, xmlNode *contrib, GList **list)
{
	xmlChar *ref;
	gint64 start_time;
	gchar *text = NULL;
	ref = xmlGetProp(contrib, XML_CHAR("start-reference"));
	start_time = get_timepoint_by_ref(doc, ref);
	xmlFree(ref);
	for (xmlNode *cur = contrib->children; cur != NULL; cur = cur->next) {
		xmlChar *content;
		gchar *new;
		switch (cur->type) {
		case XML_TEXT_NODE:
			content = xmlNodeGetContent(cur);
			new = g_strconcat(text != NULL ? text : "",
					  g_strstrip((gchar *)content),
					  " ", NULL);
			g_free(text);
			text = new;
			xmlFree(content);
			break;
		case XML_ELEMENT_NODE:
			if (!xmlStrcmp(cur->name, XML_CHAR("pause"))) {
				xmlChar *duration;
				duration = xmlGetProp(cur, XML_CHAR("duration"));
				if (duration == NULL)
					break;
				if (!xmlStrcmp(duration, XML_CHAR("micro")) ||
				    !xmlStrcmp(duration, XML_CHAR("short")))
					new = g_strconcat(text != NULL ? text : "",
							  "... ", NULL);
				else if (text == NULL)
					new = g_strdup("...\n");
				else
					new = g_strconcat(g_strchomp(text),
							  "\n", NULL);
				g_free(text);
				text = new;
				xmlFree(duration);
			} else if (!xmlStrcmp(cur->name, XML_CHAR("time"))) {
				insert_contribution(start_time, text, list);
				g_free(text);
				text = NULL;
				ref = xmlGetProp(cur,
						 XML_CHAR("timepoint-reference"));
				start_time = get_timepoint_by_ref(doc, ref);
				xmlFree(ref);
			}
			break;
		default:
			break;
		}
	}
	insert_contribution(start_time, text, list);
	g_free(text);
}
/*
 * API
 */
/**
 * @brief Constructs a new ExperimentReader object
 *
 * @param filename Filename of XML file to open
 * @return A new \e ExperimentReader object. Free with \e g_object_unref.
 */
ExperimentReader *
experiment_reader_new(const gchar *filename)
{
	ExperimentReader *reader;
	reader = EXPERIMENT_READER(g_object_new(EXPERIMENT_TYPE_READER, NULL));
	reader->priv->doc = xmlParseFile(filename);
	if (reader->priv->doc == NULL) {
		g_object_unref(G_OBJECT(reader));
		return NULL;
	}
	/** @todo validate against session.dtd */
	return reader;
}
/**
 * @brief Retrieve list of contributions by speaker
 *
 * Returns a newly-allocated doubly-linked list of
 * \ref ExperimentReaderContrib structures representing all contributions
 * by a given speaker. Every text fragment with a \e timepoint reference is
 * considered a contribution.
 * The list is sorted by the contributions' start times, in ascending order.
 *
 * @sa ExperimentReaderContrib
 * @sa experiment_reader_get_contribution_by_time
 * @sa experiment_reader_free_contributions
 *
 * @param reader  \e ExperimentReader instance
 * @param speaker Full name of the speaker (e.g. "Wizard")
 * @return Newly allocated list of contributions (must be freed with
 *         \ref experiment_reader_free_contributions)
 */
GList *
experiment_reader_get_contributions_by_speaker(ExperimentReader *reader,
					       const gchar *speaker)
{
	GList		*list = NULL;
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	xmlChar expr[255];
	xpathCtx = xmlXPathNewContext(reader->priv->doc);
	/* Evaluate xpath expression */
	xmlStrPrintf(expr, sizeof(expr),
		     XML_CHAR("//contribution[@speaker-reference = "
			      "/session/speakers/speaker[name = '%s']/@speaker-id]"),
		     speaker);
	xpathObj = xmlXPathEvalExpression(expr, xpathCtx);
	for (int i = 0; i < xpathObj->nodesetval->nodeNr; i++) {
		xmlNode *contrib = xpathObj->nodesetval->nodeTab[i];
		process_contribution(reader->priv->doc, contrib, &list);
	}
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
	return list;
}
/**
 * @brief Get a contribution by time
 *
 * Gets the closest contribution after the specified time or the last one
 * if there is no contribution after the specified time.
 * The contribution is returned as a pointer into the contribution list
 * so that the list may be traversed by the caller.
 *
 * @param contribs List of \ref ExperimentReaderContrib structures as returned
 *                 by \ref experiment_reader_get_contributions_by_speaker
 * @param timept   Time in milliseconds
 * @return List of contributions beginning with the desired contribution.
 *         It is a pointer into contribs and must not be freed directly.
 */
GList *
experiment_reader_get_contribution_by_time(GList *contribs, gint64 timept)
{
	for (GList *cur = contribs; cur != NULL; cur = cur->next) {
		ExperimentReaderContrib *contrib =
					(ExperimentReaderContrib *)cur->data;
		if (contrib->start_time > timept ||
		    cur->next == NULL)
			return cur;
	}
	return NULL;
}
/**
 * @brief Free list of contributions and associated data
 *
 * @sa experiment_reader_get_contributions_by_speaker
 *
 * @param contribs List of \ref ExperimentReaderContrib structures to free
 */
void
experiment_reader_free_contributions(GList *contribs)
{
	for (GList *cur = contribs; cur != NULL; cur = cur->next)
		g_free(cur->data);
	g_list_free(contribs);
}
/**
 * Calls \e callback with \e userdata for each \b topic in the \b greeting
 * section of the experiment.
 *
 * @param reader   \e ExperimentReader instance
 * @param callback Function to invoke
 * @param userdata User data to pass to \e callback
 */
void
experiment_reader_foreach_greeting_topic(ExperimentReader *reader,
					 ExperimentReaderTopicCallback callback,
					 gpointer userdata)
{
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	GClosure	*closure;
	xpathCtx = xmlXPathNewContext(reader->priv->doc);
	xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/greeting/topic"),
					  xpathCtx);
	closure = experiment_reader_topic_callback_new(callback, userdata);
	generic_foreach_topic(reader, xpathObj->nodesetval, closure);
	g_closure_unref(closure);
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
}
/**
 * Calls \e callback with \e userdata for each \b topic in the
 * \b initial-narrative subsection of the \b experiment section of
 * the experiment.
 *
 * @param reader   \e ExperimentReader instance
 * @param callback Function to invoke
 * @param userdata User data to pass to \e callback
 */
void
experiment_reader_foreach_exp_initial_narrative_topic(reader, callback, userdata)
	ExperimentReader		*reader;
	ExperimentReaderTopicCallback	callback;
	gpointer			userdata;
{
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	GClosure	*closure;
	xpathCtx = xmlXPathNewContext(reader->priv->doc);
	xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/experiment/"
						   "initial-narrative/topic"),
					  xpathCtx);
	closure = experiment_reader_topic_callback_new(callback, userdata);
	generic_foreach_topic(reader, xpathObj->nodesetval, closure);
	g_closure_unref(closure);
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
}
/**
 * Calls \e callback with \e userdata for each \b topic in a \b phase of
 * the \b last-minute subsection of the \b experiment section of
 * the experiment.
 *
 * @param reader   \e ExperimentReader instance
 * @param phase    \b Phase section (integer from 1 to 6)
 * @param callback Function to invoke
 * @param userdata User data to pass to \e callback
 */
void
experiment_reader_foreach_exp_last_minute_phase_topic(reader, phase, callback, userdata)
	ExperimentReader		*reader;
	gint				phase;
	ExperimentReaderTopicCallback	callback;
	gpointer			userdata;
{
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	GClosure	*closure;
	xmlChar expr[255];
	xpathCtx = xmlXPathNewContext(reader->priv->doc);
	/* Evaluate xpath expression */
	xmlStrPrintf(expr, sizeof(expr),
		     XML_CHAR("/session/experiment/last-minute/"
			      "phase[@id = '%d']/topic"),
		     phase);
	xpathObj = xmlXPathEvalExpression(expr, xpathCtx);
	closure = experiment_reader_topic_callback_new(callback, userdata);
	generic_foreach_topic(reader, xpathObj->nodesetval, closure);
	g_closure_unref(closure);
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
}
/**
 * Calls \e callback with \e userdata for each \b topic in the \b farewell
 * section of the experiment.
 *
 * @param reader   \e ExperimentReader instance
 * @param callback Function to invoke
 * @param userdata User data to pass to \e callback
 */
void
experiment_reader_foreach_farewell_topic(ExperimentReader *reader,
					 ExperimentReaderTopicCallback callback,
					 gpointer userdata)
{
	xmlXPathContext	*xpathCtx;
	xmlXPathObject	*xpathObj;
	GClosure	*closure;
	xpathCtx = xmlXPathNewContext(reader->priv->doc);
	xpathObj = xmlXPathEvalExpression(XML_CHAR("/session/farewell/topic"),
					  xpathCtx);
	closure = experiment_reader_topic_callback_new(callback, userdata);
	generic_foreach_topic(reader, xpathObj->nodesetval, closure);
	g_closure_unref(closure);
	xmlXPathFreeObject(xpathObj);
	xmlXPathFreeContext(xpathCtx);
}