/*
 * plist.c
 * XML plist implementation
 *
 * Copyright (c) 2008 Jonathan Beck All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA 
 */


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


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

#include "plist.h"

#define XPLIST_TEXT	BAD_CAST("text")
#define XPLIST_KEY	BAD_CAST("key")
#define XPLIST_FALSE	BAD_CAST("false")
#define XPLIST_TRUE	BAD_CAST("true")
#define XPLIST_INT	BAD_CAST("integer")
#define XPLIST_REAL	BAD_CAST("real")
#define XPLIST_DATE	BAD_CAST("date")
#define XPLIST_DATA	BAD_CAST("data")
#define XPLIST_STRING	BAD_CAST("string")
#define XPLIST_ARRAY	BAD_CAST("array")
#define XPLIST_DICT	BAD_CAST("dict")

static const char *plist_base = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
</plist>\0";


/** Formats a block of text to be a given indentation and width.
 * 
 * The total width of the return string will be depth + cols.
 *
 * @param buf The string to format.
 * @param cols The number of text columns for returned block of text.
 * @param depth The number of tabs to indent the returned block of text.
 *
 * @return The formatted string.
 */
static gchar *format_string(const char *buf, int cols, int depth)
{
	int colw = depth + cols + 1;
	int len = strlen(buf);
	int nlines = len / cols + 1;
	gchar *new_buf = (gchar *) g_malloc0(nlines * colw + depth + 1);
	int i = 0;
	int j = 0;

	assert(cols >= 0);
	assert(depth >= 0);

	// Inserts new lines and tabs at appropriate locations
	for (i = 0; i < nlines; i++) {
		new_buf[i * colw] = '\n';
		for (j = 0; j < depth; j++)
			new_buf[i * colw + 1 + j] = '\t';
		memcpy(new_buf + i * colw + 1 + depth, buf + i * cols, (i + 1) * cols <= len ? cols : len - i * cols);
	}
	new_buf[len + (1 + depth) * nlines] = '\n';

	// Inserts final row of indentation and termination character
	for (j = 0; j < depth; j++)
		new_buf[len + (1 + depth) * nlines + 1 + j] = '\t';
	new_buf[len + (1 + depth) * nlines + depth + 1] = '\0';

	return new_buf;
}



struct xml_node {
	xmlNodePtr xml;
	uint32_t depth;
};

/** Creates a new plist XML document.
 * 
 * @return The plist XML document.
 */
static xmlDocPtr new_xml_plist(void)
{
	char *plist = strdup(plist_base);
	xmlDocPtr plist_xml = xmlParseMemory(plist, strlen(plist));

	if (!plist_xml)
		return NULL;

	free(plist);

	return plist_xml;
}

static void node_to_xml(GNode * node, gpointer xml_struct)
{
	struct xml_node *xstruct = NULL;
	plist_data_t node_data = NULL;

	xmlNodePtr child_node = NULL;
	char isStruct = FALSE;

	const xmlChar *tag = NULL;
	gchar *val = NULL;

	//for base64
	gchar *valtmp = NULL;

	//for unicode
	glong len = 0;
	glong items_read = 0;
	glong items_written = 0;
	GError *error = NULL;

	uint32_t i = 0;

	if (!node)
		return;

	xstruct = (struct xml_node *) xml_struct;
	node_data = plist_get_data(node);

	switch (node_data->type) {
	case PLIST_BOOLEAN:
		{
			if (node_data->boolval)
				tag = XPLIST_TRUE;
			else
				tag = XPLIST_FALSE;
		}
		break;

	case PLIST_UINT:
		tag = XPLIST_INT;
		val = g_strdup_printf("%llu", node_data->intval);
		break;

	case PLIST_REAL:
		tag = XPLIST_REAL;
		val = g_strdup_printf("%f", node_data->realval);
		break;

	case PLIST_STRING:
		tag = XPLIST_STRING;
		val = g_strdup(node_data->strval);
		break;

	case PLIST_UNICODE:
		tag = XPLIST_STRING;
		len = node_data->length;
		val = g_utf16_to_utf8(node_data->unicodeval, len, &items_read, &items_written, &error);
		break;

	case PLIST_KEY:
		tag = XPLIST_KEY;
		val = g_strdup((gchar *) node_data->strval);
		break;

	case PLIST_DATA:
		tag = XPLIST_DATA;
		if (node_data->length) {
			valtmp = g_base64_encode(node_data->buff, node_data->length);
			val = format_string(valtmp, 60, xstruct->depth);
			g_free(valtmp);
		}
		break;
	case PLIST_ARRAY:
		tag = XPLIST_ARRAY;
		isStruct = TRUE;
		break;
	case PLIST_DICT:
		tag = XPLIST_DICT;
		isStruct = TRUE;
		break;
	case PLIST_DATE:
		tag = XPLIST_DATE;
		val = g_time_val_to_iso8601(&node_data->timeval);
		break;
	default:
		break;
	}

	for (i = 0; i < xstruct->depth; i++) {
		xmlNodeAddContent(xstruct->xml, BAD_CAST("\t"));
	}
	child_node = xmlNewChild(xstruct->xml, NULL, tag, BAD_CAST(val));
	xmlNodeAddContent(xstruct->xml, BAD_CAST("\n"));
	g_free(val);

	//add return for structured types
	if (node_data->type == PLIST_ARRAY || node_data->type == PLIST_DICT)
		xmlNodeAddContent(child_node, BAD_CAST("\n"));

	if (isStruct) {
		struct xml_node child = { child_node, xstruct->depth + 1 };
		g_node_children_foreach(node, G_TRAVERSE_ALL, node_to_xml, &child);
	}
	//fix indent for structured types
	if (node_data->type == PLIST_ARRAY || node_data->type == PLIST_DICT) {

		for (i = 0; i < xstruct->depth; i++) {
			xmlNodeAddContent(child_node, BAD_CAST("\t"));
		}
	}

	return;
}

static void xml_to_node(xmlNodePtr xml_node, plist_t * plist_node)
{
	xmlNodePtr node = NULL;
	plist_data_t data = NULL;
	plist_t subnode = NULL;

	//for string
	unsigned char *tmp = NULL;
	glong len = 0;
	glong items_read = 0;
	glong items_written = 0;
	GError *error = NULL;
	int type = 0;

	if (!xml_node)
		return;

	for (node = xml_node->children; node; node = node->next) {

		while (node && !xmlStrcmp(node->name, XPLIST_TEXT))
			node = node->next;
		if (!node)
			break;

		data = plist_new_plist_data();
		subnode = plist_new_node(data);
		if (*plist_node)
			g_node_append(*plist_node, subnode);
		else
			*plist_node = subnode;

		if (!xmlStrcmp(node->name, XPLIST_TRUE)) {
			data->boolval = TRUE;
			data->type = PLIST_BOOLEAN;
			data->length = 1;
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_FALSE)) {
			data->boolval = FALSE;
			data->type = PLIST_BOOLEAN;
			data->length = 1;
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_INT)) {
			xmlChar *strval = xmlNodeGetContent(node);
			data->intval = g_ascii_strtoull((char *)strval, NULL, 0);
			data->type = PLIST_UINT;
			data->length = 8;
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_REAL)) {
			xmlChar *strval = xmlNodeGetContent(node);
			data->realval = atof((char *)strval);
			data->type = PLIST_REAL;
			data->length = 8;
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_DATE)) {
			xmlChar *strval = xmlNodeGetContent(node);
			g_time_val_from_iso8601((char *) strval, &data->timeval);
			data->type = PLIST_DATE;
			data->length = sizeof(GTimeVal);
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_STRING)) {
			xmlChar *strval = xmlNodeGetContent(node);
			len = strlen((char *) strval);
			items_read = 0;
			items_written = 0;
			error = NULL;
			type = xmlDetectCharEncoding(strval, len);

			if (XML_CHAR_ENCODING_UTF8 == type) {
				data->unicodeval = g_utf8_to_utf16((char *) strval, len, &items_read, &items_written, &error);
				data->type = PLIST_UNICODE;
				data->length = items_written;
			} else if (XML_CHAR_ENCODING_ASCII == type || XML_CHAR_ENCODING_NONE == type) {
				data->strval = strdup((char *) strval);
				data->type = PLIST_STRING;
				data->length = strlen(data->strval);
			}
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_KEY)) {
			xmlChar *strval = xmlNodeGetContent(node);
			data->strval = strdup((char *) strval);
			data->type = PLIST_KEY;
			data->length = strlen(data->strval);
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_DATA)) {
			xmlChar *strval = xmlNodeGetContent(node);
			gsize size = 0;
			guchar *dec = g_base64_decode((char *) strval, &size);
			data->buff = (uint8_t*) malloc( size * sizeof(uint8_t));
			memcpy(data->buff, dec, size * sizeof(uint8_t));
			g_free(dec);
			data->length = size;
			data->type = PLIST_DATA;
			xmlFree(strval);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_ARRAY)) {
			data->type = PLIST_ARRAY;
			xml_to_node(node, &subnode);
			continue;
		}

		if (!xmlStrcmp(node->name, XPLIST_DICT)) {
			data->type = PLIST_DICT;
			xml_to_node(node, &subnode);
			continue;
		}
	}
}

void plist_to_xml(plist_t plist, char **plist_xml, uint32_t * length)
{
	xmlDocPtr plist_doc = NULL;
	xmlNodePtr root_node = NULL;
	struct xml_node root = { NULL, 0 };
	int size = 0;

	if (!plist || !plist_xml || *plist_xml)
		return;
	plist_doc = new_xml_plist();
	root_node = xmlDocGetRootElement(plist_doc);
	root.xml = root_node;

	node_to_xml(plist, &root);

	xmlDocDumpMemory(plist_doc, (xmlChar **) plist_xml, &size);
	if (size >= 0)
		*length = size;
	xmlFreeDoc(plist_doc);
}

void plist_from_xml(const char *plist_xml, uint32_t length, plist_t * plist)
{
	xmlDocPtr plist_doc = xmlParseMemory(plist_xml, length);
	xmlNodePtr root_node = xmlDocGetRootElement(plist_doc);

	xml_to_node(root_node, plist);
	xmlFreeDoc(plist_doc);
}