/*
 * Copyright (c) 2010-2019 Belledonne Communications SARL.
 *
 * This file is part of Liblinphone.
 *
 * 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 <http://www.gnu.org/licenses/>.
 */

#include "xml2lpc.h"
#include <string.h>
#include <libxml/xmlreader.h>


#define XML2LPC_BZ 2048

struct _xml2lpc_context {
	LpConfig *lpc;
	xml2lpc_function cbf;
	void *ctx;

	xmlDoc *doc;
	xmlDoc *xsd;
	char errorBuffer[XML2LPC_BZ];
	char warningBuffer[XML2LPC_BZ];
};


xml2lpc_context* xml2lpc_context_new(xml2lpc_function cbf, void *ctx) {
	xml2lpc_context *xmlCtx = (xml2lpc_context*)malloc(sizeof(xml2lpc_context));
	if(xmlCtx != NULL) {
		xmlCtx->lpc = NULL;
		xmlCtx->cbf = cbf;
		xmlCtx->ctx = ctx;

		xmlCtx->doc = NULL;
		xmlCtx->xsd = NULL;
		xmlCtx->errorBuffer[0]='\0';
		xmlCtx->warningBuffer[0]='\0';
	}
	return xmlCtx;
}

void xml2lpc_context_destroy(xml2lpc_context *ctx) {
	if(ctx->doc != NULL) {
		xmlFreeDoc(ctx->doc);
		ctx->doc = NULL;
	}
	if(ctx->xsd != NULL) {
		xmlFreeDoc(ctx->xsd);
		ctx->xsd = NULL;
	}

	// TODO Fix me, xml2 isn't robust for use with multiple context
	// Unset callback to not disturb other user of the xml lib
	xmlSetGenericErrorFunc(NULL, NULL);

	free(ctx);
}

static void xml2lpc_context_clear_logs(xml2lpc_context *ctx) {
	ctx->errorBuffer[0]='\0';
	ctx->warningBuffer[0]='\0';
}

static void xml2lpc_log(xml2lpc_context *xmlCtx, xml2lpc_log_level level, const char *fmt, ...) {
	va_list args;
	va_start(args, fmt);
	if(xmlCtx->cbf != NULL) {
		xmlCtx->cbf((xmlCtx)->ctx, level, fmt, args);
	}
	va_end(args);
}

static void xml2lpc_genericxml_error(void *ctx, const char *fmt, ...) {
	xml2lpc_context *xmlCtx = (xml2lpc_context *)ctx;
	size_t sl = strlen(xmlCtx->errorBuffer);
	va_list args;
	va_start(args, fmt);
	vsnprintf(xmlCtx->errorBuffer + sl, XML2LPC_BZ-sl, fmt, args);
	va_end(args);
}

static void xml2lpc_genericxml_warning(void *ctx, const char *fmt, ...) {
	xml2lpc_context *xmlCtx = (xml2lpc_context *)ctx;
	size_t sl = strlen(xmlCtx->warningBuffer);
	va_list args;
	va_start(args, fmt);
	vsnprintf(xmlCtx->warningBuffer + sl, XML2LPC_BZ-sl, fmt, args);
	va_end(args);
}

#if 0
static void dumpNodes(int level, xmlNode * a_node, xml2lpc_context *ctx) {
	xmlNode *cur_node = NULL;

	for (cur_node = a_node; cur_node; cur_node = cur_node->next) {
		if (cur_node->type == XML_ELEMENT_NODE) {
			xml2lpc_log(ctx, XML2LPC_DEBUG, "node level: %d type: Element, name: %s", level, cur_node->name);
		} else {
			xml2lpc_log(ctx, XML2LPC_DEBUG, "node level: %d type: %d, name: %s", level, cur_node->type, cur_node->name);
		}

		dumpNodes(level + 1, cur_node->children, ctx);
	}
}
#endif


static void dumpNode(xmlNode *node, xml2lpc_context *ctx) {
	xml2lpc_log(ctx, XML2LPC_DEBUG, "node type: %d, name: %s", node->type, node->name);
}

static void dumpAttr(xmlNode *node, xml2lpc_context *ctx) {
	xml2lpc_log(ctx, XML2LPC_DEBUG, "attr name: %s value:%s", node->name, node->children->content);
}

static void dumpContent(xmlNode *node, xml2lpc_context *ctx) {
	if (node->children)
		xml2lpc_log(ctx, XML2LPC_DEBUG, "content: %s", node->children->content);
	else
		xml2lpc_log(ctx, XML2LPC_DEBUG, "content: ");
}

static int processEntry(xmlElement *element, const char *sectionName, xml2lpc_context *ctx) {
	xmlNode *cur_attr = NULL;
	const char *name = NULL;
	const char *value = NULL;
	bool_t overwrite = FALSE;

	for (cur_attr = (xmlNode *)element->attributes; cur_attr; cur_attr = cur_attr->next) {
		dumpAttr(cur_attr, ctx);
		if(strcmp((const char*)cur_attr->name, "name") == 0) {
			name = (const char*)cur_attr->children->content;
		} else if(strcmp((const char*)cur_attr->name, "overwrite") == 0) {
			if(strcmp((const char*)cur_attr->children->content, "true") == 0) {
				overwrite = TRUE;
			}
		}
	}

	dumpContent((xmlNode *)element, ctx);
	if (element->children)
		value = (const char *)element->children->content;
	else
		value = "";

	if(name != NULL) {
		const char *str = linphone_config_get_string(ctx->lpc, sectionName, name, NULL);
		if(str == NULL || overwrite) {
			xml2lpc_log(ctx, XML2LPC_MESSAGE, "Set %s|%s = %s", sectionName, name, value);
			linphone_config_set_string(ctx->lpc, sectionName, name, value);
		} else {
			xml2lpc_log(ctx, XML2LPC_MESSAGE, "Don't touch %s|%s = %s",sectionName, name, str);
		}
	} else {
		xml2lpc_log(ctx, XML2LPC_WARNING, "ignored entry with no \"name\" attribute line:%d",xmlGetLineNo((xmlNode*)element));
	}
		return 0;
}

static int processSection(xmlElement *element, xml2lpc_context *ctx) {
	xmlNode *cur_node = NULL;
	xmlNode *cur_attr = NULL;
	const char *name = NULL;

	for (cur_attr = (xmlNode *)element->attributes; cur_attr; cur_attr = cur_attr->next) {
		dumpAttr(cur_attr, ctx);
		if(strcmp((const char*)cur_attr->name, "name") == 0) {
			name = (const char*)cur_attr->children->content;
		}
	}

	if(name != NULL) {
		for (cur_node = element->children; cur_node; cur_node = cur_node->next) {
			dumpNode(cur_node, ctx);
			if (cur_node->type == XML_ELEMENT_NODE) {
				if(strcmp((const char*)cur_node->name, "entry") == 0 ) {
					processEntry((xmlElement*)cur_node, name, ctx);
				}
			}

		}
		} else {
			xml2lpc_log(ctx, XML2LPC_WARNING, "ignored section with no \"name\" attribute, line:%d", xmlGetLineNo((xmlNode*)element));
		}

		return 0;
}

static int processConfig(xmlElement *element, xml2lpc_context *ctx) {
	xmlNode *cur_node = NULL;

	for (cur_node = element->children; cur_node; cur_node = cur_node->next) {
		dumpNode(cur_node, ctx);
		if (cur_node->type == XML_ELEMENT_NODE &&
			strcmp((const char*)cur_node->name, "section") == 0 ) {
			processSection((xmlElement*)cur_node, ctx);
			}

		}
	return 0;
}

static int processDoc(xmlNode *node, xml2lpc_context *ctx) {
	dumpNode(node, ctx);

	if (node->type == XML_ELEMENT_NODE &&
		strcmp((const char*)node->name, "config") == 0 ) {
		processConfig((xmlElement*)node, ctx);
	} else {
		xml2lpc_log(ctx, XML2LPC_WARNING, "root element is not \"config\", line:%d", xmlGetLineNo(node));
	}
	return 0;
}

static int internal_convert_xml2lpc(xml2lpc_context *ctx) {
	xmlNode *rootNode;
	int ret;

	xml2lpc_log(ctx, XML2LPC_DEBUG, "Parse started");
	rootNode = xmlDocGetRootElement(ctx->doc);
	//dumpNodes(0, rootNode, cbf, ctx);
	ret = processDoc(rootNode, ctx);
	xml2lpc_log(ctx, XML2LPC_DEBUG, "Parse ended ret:%d", ret);
	return ret;
}

int xml2lpc_validate(xml2lpc_context *xmlCtx) {
	xmlSchemaValidCtxtPtr validCtx;
	xmlSchemaParserCtxtPtr parserCtx;
	int ret;

	xml2lpc_context_clear_logs(xmlCtx);
	parserCtx = xmlSchemaNewDocParserCtxt(xmlCtx->xsd);
	validCtx = xmlSchemaNewValidCtxt(xmlSchemaParse(parserCtx));
	xmlSchemaSetValidErrors(validCtx, xml2lpc_genericxml_error, xml2lpc_genericxml_warning, xmlCtx);
	ret = xmlSchemaValidateDoc(validCtx, xmlCtx->doc);
	if(ret > 0) {
		if(strlen(xmlCtx->warningBuffer) > 0)
			xml2lpc_log(xmlCtx, XML2LPC_WARNING, "%s", xmlCtx->warningBuffer);
		if(strlen(xmlCtx->errorBuffer) > 0)
			xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
	} else if(ret < 0) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Internal error");
	}
	xmlSchemaFreeValidCtxt(validCtx);
	return ret;
}

int xml2lpc_convert(xml2lpc_context *xmlCtx, LpConfig *lpc) {
	xml2lpc_context_clear_logs(xmlCtx);
	if(xmlCtx->doc == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "No doc set");
		return -1;
	}
	if(lpc == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Invalid lpc");
	}
	xmlCtx->lpc = lpc;
	return internal_convert_xml2lpc(xmlCtx);
}

int xml2lpc_set_xml_file(xml2lpc_context* xmlCtx, const char *filename) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->doc != NULL) {
		xmlFreeDoc(xmlCtx->doc);
		xmlCtx->doc = NULL;
	}
	xmlCtx->doc = xmlReadFile(filename, NULL, 0);
	if(xmlCtx->doc == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse file \"%s\"", filename);
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}

int xml2lpc_set_xml_fd(xml2lpc_context* xmlCtx, int fd) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->doc != NULL) {
		xmlFreeDoc(xmlCtx->doc);
		xmlCtx->doc = NULL;
	}
	xmlCtx->doc = xmlReadFd(fd, 0, NULL, 0);
	if(xmlCtx->doc == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse fd \"%d\"", fd);
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}

int xml2lpc_set_xml_string(xml2lpc_context* xmlCtx, const char *content) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->doc != NULL) {
		xmlFreeDoc(xmlCtx->doc);
		xmlCtx->doc = NULL;
	}
	xmlCtx->doc = xmlReadDoc((const unsigned char*)content, 0, NULL, 0);
	if(xmlCtx->doc == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't parse string");
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}

int xml2lpc_set_xsd_file(xml2lpc_context* xmlCtx, const char *filename) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->xsd != NULL) {
		xmlFreeDoc(xmlCtx->xsd);
		xmlCtx->xsd = NULL;
	}
	xmlCtx->xsd = xmlReadFile(filename, NULL, 0);
	if(xmlCtx->xsd == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse file \"%s\"", filename);
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}

int xml2lpc_set_xsd_fd(xml2lpc_context* xmlCtx, int fd) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->xsd != NULL) {
		xmlFreeDoc(xmlCtx->xsd);
		xmlCtx->xsd = NULL;
	}
	xmlCtx->xsd = xmlReadFd(fd, 0, NULL, 0);
	if(xmlCtx->xsd == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't open/parse fd \"%d\"", fd);
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}

int xml2lpc_set_xsd_string(xml2lpc_context* xmlCtx, const char *content) {
	xml2lpc_context_clear_logs(xmlCtx);
	xmlSetGenericErrorFunc(xmlCtx, xml2lpc_genericxml_error);
	if(xmlCtx->xsd != NULL) {
		xmlFreeDoc(xmlCtx->xsd);
		xmlCtx->xsd = NULL;
	}
	xmlCtx->xsd = xmlReadDoc((const unsigned char*)content, 0, NULL, 0);
	if(xmlCtx->xsd == NULL) {
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "Can't parse string");
		xml2lpc_log(xmlCtx, XML2LPC_ERROR, "%s", xmlCtx->errorBuffer);
		return -1;
	}
	return 0;
}
