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

/* Copyright (C) 2001-2004 Novell, Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU Lesser General Public
 * License as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "e2k-result.h"
#include "e2k-encoding-utils.h"
#include "e2k-http-utils.h"
#include "e2k-propnames.h"
#include "e2k-xml-utils.h"

#include <stdlib.h>
#include <string.h>

#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xmlmemory.h>

static void
prop_get_binary_array (E2kResult *result, const char *propname, xmlNode *node)
{
	GPtrArray *array;

	array = g_ptr_array_new ();
	for (node = node->xmlChildrenNode; node; node = node->next) {
		if (node->xmlChildrenNode && node->xmlChildrenNode->content)
			g_ptr_array_add (array, e2k_base64_decode (node->xmlChildrenNode->content));
		else
			g_ptr_array_add (array, g_byte_array_new ());
	}

	e2k_properties_set_binary_array (result->props, propname, array);
}

static void
prop_get_string_array (E2kResult *result, const char *propname,
		       E2kPropType real_type, xmlNode *node)
{
	GPtrArray *array;

	array = g_ptr_array_new ();
	for (node = node->xmlChildrenNode; node; node = node->next) {
		if (node->xmlChildrenNode && node->xmlChildrenNode->content)
			g_ptr_array_add (array, g_strdup (node->xmlChildrenNode->content));
		else
			g_ptr_array_add (array, g_strdup (""));
	}

	e2k_properties_set_type_as_string_array (result->props, propname,
						 real_type, array);
}

static void
prop_get_binary (E2kResult *result, const char *propname, xmlNode *node)
{
	GByteArray *data;

	if (node->xmlChildrenNode && node->xmlChildrenNode->content)
		data = e2k_base64_decode (node->xmlChildrenNode->content);
	else
		data = g_byte_array_new ();

	e2k_properties_set_binary (result->props, propname, data);
}

static void
prop_get_string (E2kResult *result, const char *propname,
		 E2kPropType real_type, xmlNode *node)
{
	char *content;

	if (node->xmlChildrenNode && node->xmlChildrenNode->content)
		content = g_strdup (node->xmlChildrenNode->content);
	else
		content = g_strdup ("");

	e2k_properties_set_type_as_string (result->props, propname,
					   real_type, content);
}

static void
prop_get_xml (E2kResult *result, const char *propname, xmlNode *node)
{
	e2k_properties_set_xml (result->props, propname,
				xmlCopyNode (node, TRUE));
}

static void
prop_parse (xmlNode *node, E2kResult *result)
{
	char *name, *type;

	g_return_if_fail (node->ns != NULL);

	if (!result->props)
		result->props = e2k_properties_new ();

	if (!strncmp (node->ns->href, E2K_NS_MAPI_ID, E2K_NS_MAPI_ID_LEN)) {
		/* Reinsert the illegal initial '0' that was stripped out
		 * by sanitize_bad_multistatus. (This also covers us in
		 * the cases where the server returns the property without
		 * the '0'.)
		 */
		name = g_strdup_printf ("%s0%s", node->ns->href, node->name);
	} else
		name = g_strdup_printf ("%s%s", node->ns->href, node->name);

	type = xmlGetNsProp (node, "dt", E2K_NS_TYPE);
	if (type && !strcmp (type, "mv.bin.base64"))
		prop_get_binary_array (result, name, node);
	else if (type && !strcmp (type, "mv.int"))
		prop_get_string_array (result, name, E2K_PROP_TYPE_INT_ARRAY, node);
	else if (type && !strncmp (type, "mv.", 3))
		prop_get_string_array (result, name, E2K_PROP_TYPE_STRING_ARRAY, node);
	else if (type && !strcmp (type, "bin.base64"))
		prop_get_binary (result, name, node);
	else if (type && !strcmp (type, "int"))
		prop_get_string (result, name, E2K_PROP_TYPE_INT, node);
	else if (type && !strcmp (type, "boolean"))
		prop_get_string (result, name, E2K_PROP_TYPE_BOOL, node);
	else if (type && !strcmp (type, "float"))
		prop_get_string (result, name, E2K_PROP_TYPE_FLOAT, node);
	else if (type && !strcmp (type, "dateTime.tz"))
		prop_get_string (result, name, E2K_PROP_TYPE_DATE, node);
	else if (!node->xmlChildrenNode ||
		 !node->xmlChildrenNode->xmlChildrenNode)
		prop_get_string (result, name, E2K_PROP_TYPE_STRING, node);
	else
		prop_get_xml (result, name, node);

	if (type)
		xmlFree (type);
	g_free (name);
}

static void
propstat_parse (xmlNode *node, E2kResult *result)
{
	node = node->xmlChildrenNode;
	if (!E2K_IS_NODE (node, "DAV:", "status"))
		return;
	result->status = e2k_http_parse_status (node->xmlChildrenNode->content);
	if (result->status != E2K_HTTP_OK)
		return;

	node = node->next;
	if (!E2K_IS_NODE (node, "DAV:", "prop"))
		return;

	for (node = node->xmlChildrenNode; node; node = node->next) {
		if (node->type == XML_ELEMENT_NODE)
			prop_parse (node, result);
	}
}

static void
e2k_result_clear (E2kResult *result)
{
	xmlFree (result->href);
	result->href = NULL;
	if (result->props) {
		e2k_properties_free (result->props);
		result->props = NULL;
	}
}

/**
 * e2k_results_array_new:
 *
 * Creates a new results array
 *
 * Return value: the array
 **/
GArray *
e2k_results_array_new (void)
{
	return g_array_new (FALSE, FALSE, sizeof (E2kResult));
}

/* Properties in the /mapi/id/{...} namespaces are usually (though not
 * always) returned with names that start with '0', which is illegal
 * and makes libxml choke. So we preprocess them to fix that.
 */
static char *
sanitize_bad_multistatus (const char *buf, int len)
{
	GString *body;
	const char *p;
	int start, end;
	char ns, badprop[7], *ret;

	/* If there are no "mapi/id/{...}" namespace declarations, then
	 * we don't need any cleanup.
	 */
	if (!memchr (buf, '{', len))
		return NULL;

	body = g_string_new_len (buf, len);

	/* Find the start and end of namespace declarations */
	p = strstr (body->str, " xmlns:");
	g_return_val_if_fail (p != NULL, NULL);
	start = p + 1 - body->str;

	p = strchr (p, '>');
	g_return_val_if_fail (p != NULL, NULL);
	end = p - body->str;

	while (1) {
		if (strncmp (body->str + start, "xmlns:", 6) != 0)
			break;
		if (strncmp (body->str + start + 7, "=\"", 2) != 0)
			break;
		if (strncmp (body->str + start + 9, E2K_NS_MAPI_ID, E2K_NS_MAPI_ID_LEN) != 0)
			goto next;

		ns = body->str[start + 6];

		/* Find properties in this namespace and strip the
		 * initial '0' from their names to make them valid
		 * XML NCNames.
		 */
		snprintf (badprop, 6, "<%c:0x", ns);
		while ((p = strstr (body->str, badprop)))
			g_string_erase (body, p + 3 - body->str, 1);
		snprintf (badprop, 7, "</%c:0x", ns);
		while ((p = strstr (body->str, badprop)))
			g_string_erase (body, p + 4 - body->str, 1);

	next:
		p = strchr (body->str + start, '"');
		if (!p)
			break;
		p = strchr (p + 1, '"');
		if (!p)
			break;
		if (p[1] != ' ')
			break;

		start = p + 2 - body->str;
	}

	ret = body->str;
	g_string_free (body, FALSE);
	return ret;
}

/**
 * e2k_results_array_add_from_multistatus:
 * @results_array: a results array, created by e2k_results_array_new()
 * @msg: a 207 Multi-Status response
 *
 * Constructs an #E2kResult for each response in @msg and appends them
 * to @results_array.
 **/
void
e2k_results_array_add_from_multistatus (GArray *results_array,
					SoupMessage *msg)
{
	xmlDoc *doc;
	xmlNode *node, *rnode;
	E2kResult result;
	char *body;

	g_return_if_fail (msg->status_code == E2K_HTTP_MULTI_STATUS);

	body = sanitize_bad_multistatus (msg->response.body, msg->response.length);
	if (body) {
		doc = e2k_parse_xml (body, -1);
		g_free (body);
	} else
		doc = e2k_parse_xml (msg->response.body, msg->response.length);
	if (!doc)
		return;
	node = doc->xmlRootNode;
	if (!node || !E2K_IS_NODE (node, "DAV:", "multistatus")) {
		xmlFree (doc);
		return;
	}

	for (node = node->xmlChildrenNode; node; node = node->next) {
		if (!E2K_IS_NODE (node, "DAV:", "response") ||
		    !node->xmlChildrenNode)
			continue;

		memset (&result, 0, sizeof (result));
		result.status = E2K_HTTP_OK; /* sometimes omitted if Brief */

		for (rnode = node->xmlChildrenNode; rnode; rnode = rnode->next) {
			if (rnode->type != XML_ELEMENT_NODE)
				continue;

			if (E2K_IS_NODE (rnode, "DAV:", "href"))
				result.href = xmlNodeGetContent (rnode);
			else if (E2K_IS_NODE (rnode, "DAV:", "status")) {
				result.status = e2k_http_parse_status (
					rnode->xmlChildrenNode->content);
			} else if (E2K_IS_NODE (rnode, "DAV:", "propstat"))
				propstat_parse (rnode, &result);
			else
				prop_parse (rnode, &result);
		}

		if (result.href) {
			if (E2K_HTTP_STATUS_IS_SUCCESSFUL (result.status) &&
			    !result.props)
				result.props = e2k_properties_new ();
			g_array_append_val (results_array, result);
		} else
			e2k_result_clear (&result);
	}

	xmlFreeDoc (doc);
}

/**
 * e2k_results_array_free:
 * @results_array: the array
 * @free_results: whether or not to also free the contents of the array
 *
 * Frees @results_array, and optionally its contents
 **/
void
e2k_results_array_free (GArray *results_array, gboolean free_results)
{
	if (free_results) {
		e2k_results_free ((E2kResult *)results_array->data,
				  results_array->len);
	}
	g_array_free (results_array, FALSE);
}


/**
 * e2k_results_from_multistatus:
 * @msg: a 207 Multi-Status response
 * @results: pointer to a variable to store an array of E2kResult in
 * @nresults: pointer to a variable to store the length of *@results in
 *
 * Parses @msg and puts the results in *@results and *@nresults.
 * The caller should free the data with e2k_results_free()
 **/
void
e2k_results_from_multistatus (SoupMessage *msg,
			      E2kResult **results, int *nresults)
{
	GArray *results_array;

	results_array = e2k_results_array_new ();
	e2k_results_array_add_from_multistatus (results_array, msg);

	*results = (E2kResult *)results_array->data;
	*nresults = results_array->len;
	e2k_results_array_free (results_array, FALSE);
}

/**
 * e2k_results_copy:
 * @results: a results array returned from e2k_results_from_multistatus()
 * @nresults: the length of @results
 *
 * Performs a deep copy of @results
 *
 * Return value: a copy of @results.
 **/
E2kResult *
e2k_results_copy (E2kResult *results, int nresults)
{
	GArray *results_array = NULL;
	E2kResult result, *new_results;
	int i;

	results_array = g_array_new (TRUE, FALSE, sizeof (E2kResult));
	for (i = 0; i < nresults; i++) {
		result.href   = xmlMemStrdup (results[i].href);
		result.status = results[i].status;
		result.props  = e2k_properties_copy (results[i].props);

		g_array_append_val (results_array, result);
	}

	new_results = (E2kResult *) (results_array->data);
	g_array_free (results_array, FALSE);
	return new_results;
}

/**
 * e2k_results_free:
 * @results: a results array
 * @nresults: the length of @results
 *
 * Frees the data in @results.
 **/
void
e2k_results_free (E2kResult *results, int nresults)
{
	int i;

	for (i = 0; i < nresults; i++)
		e2k_result_clear (&results[i]);
	g_free (results);
}


/* Iterators */
struct E2kResultIter {
	E2kContext *ctx;
	E2kOperation *op;
	E2kHTTPStatus status;

	E2kResult *results;
	int nresults, next;
	int first, total;
	gboolean ascending;

	E2kResultIterFetchFunc fetch_func;
	E2kResultIterFreeFunc free_func;
	gpointer user_data;
};

static void
iter_fetch (E2kResultIter *iter)
{
	if (iter->nresults) {
		if (iter->ascending)
			iter->first += iter->nresults;
		else
			iter->first -= iter->nresults;
		e2k_results_free (iter->results, iter->nresults);
		iter->nresults = 0;
	}

	iter->status = iter->fetch_func (iter, iter->ctx, iter->op,
					 &iter->results,
					 &iter->nresults,
					 &iter->first,
					 &iter->total,
					 iter->user_data);
	iter->next = 0;
}

/**
 * e2k_result_iter_new:
 * @ctx: an #E2kContext
 * @op: an #E2kOperation, to use for cancellation
 * @ascending: %TRUE if results should be returned in ascending
 * order, %FALSE if they should be returned in descending order
 * @total: the total number of results that will be returned, or -1
 * if not yet known
 * @fetch_func: function to call to fetch more results
 * @free_func: function to call when the iterator is freed
 * @user_data: data to pass to @fetch_func and @free_func
 *
 * Creates a object that can be used to return the results of
 * a Multi-Status query on @ctx.
 *
 * @fetch_func will be called to fetch results, and it may update the
 * #first and #total fields if necessary. If @ascending is %TRUE, then
 * e2k_result_iter_next() will first return the first result, then the
 * second result, etc. If @ascending is %FALSE, it will return the
 * last result, then the second-to-last result, etc.
 *
 * When all of the results returned by the first @fetch_func call have
 * been returned to the caller, @fetch_func will be called again to
 * get more results. This will continue until @fetch_func returns 0
 * results, or returns an error code.
 *
 * Return value: the new iterator
 **/
E2kResultIter *
e2k_result_iter_new (E2kContext *ctx, E2kOperation *op,
		     gboolean ascending, int total,
		     E2kResultIterFetchFunc fetch_func,
		     E2kResultIterFreeFunc free_func,
		     gpointer user_data)
{
	E2kResultIter *iter;

	iter = g_new0 (E2kResultIter, 1);
	iter->ctx = g_object_ref (ctx);
	iter->op = op;
	iter->ascending = ascending;
	iter->total = total;
	iter->fetch_func = fetch_func;
	iter->free_func = free_func;
	iter->user_data = user_data;

	iter_fetch (iter);

	return iter;
}

/**
 * e2k_result_iter_next:
 * @iter: an #E2kResultIter
 *
 * Returns the next result in the operation being iterated by @iter.
 * If there are no more results, or if an error occurs, it will return
 * %NULL. (The return value of e2k_result_iter_free() distinguishes
 * these two cases.)
 *
 * Return value: the result, or %NULL
 **/
E2kResult *
e2k_result_iter_next (E2kResultIter *iter)
{
	g_return_val_if_fail (iter != NULL, NULL);

	if (iter->nresults == 0)
		return NULL;

	if (iter->next >= iter->nresults) {
		iter_fetch (iter);
		if (iter->nresults == 0)
			return NULL;
		if (iter->total <= 0)
			iter->status = E2K_HTTP_MALFORMED;
		if (!E2K_HTTP_STATUS_IS_SUCCESSFUL (iter->status))
			return NULL;
	}

	return iter->ascending ?
		&iter->results[iter->next++] :
		&iter->results[iter->nresults - ++iter->next];
}

/**
 * e2k_result_iter_get_index:
 * @iter: an #E2kResultIter
 *
 * Returns the index of the current result in the complete list of
 * results. Note that for a descending search, %index will start at
 * %total - 1 and count backwards to 0.
 *
 * Return value: the index of the current result
 **/
int
e2k_result_iter_get_index (E2kResultIter *iter)
{
	g_return_val_if_fail (iter != NULL, -1);

	return iter->ascending ?
		iter->first + iter->next - 1 :
		iter->first + (iter->nresults - iter->next);
}

/**
 * e2k_result_iter_get_total:
 * @iter: an #E2kResultIter
 *
 * Returns the total number of results expected for @iter. Note that
 * in some cases, this may change while the results are being iterated
 * (if objects that match the query are added to or removed from the
 * folder).
 *
 * Return value: the total number of results expected
 **/
int
e2k_result_iter_get_total (E2kResultIter *iter)
{
	g_return_val_if_fail (iter != NULL, -1);

	return iter->total;
}

/**
 * e2k_result_iter_free:
 * @iter: an #E2kResultIter
 *
 * Frees @iter and all associated memory, and returns a status code
 * indicating whether it ended successfully or not. (Note that the
 * status may be %E2K_HTTP_OK rather than %E2K_HTTP_MULTI_STATUS.)
 *
 * Return value: the final status
 **/
E2kHTTPStatus
e2k_result_iter_free (E2kResultIter *iter)
{
	E2kHTTPStatus status;

	g_return_val_if_fail (iter != NULL, E2K_HTTP_MALFORMED);

	status = iter->status;
	if (iter->nresults)
		e2k_results_free (iter->results, iter->nresults);
	iter->free_func (iter, iter->user_data);
	g_object_unref (iter->ctx);
	g_free (iter);

	return status;
}
