/*
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of Liblinphone
 * (see https://gitlab.linphone.org/BC/public/liblinphone).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include "xml2lpc.h"
#include <libxml/xmlreader.h>
#include <string.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;
}
