/*
* Copyright 2006 Sony Computer Entertainment Inc.
*
* Licensed under the MIT Open Source License, for details please see license.txt or the website
* http://www.opensource.org/licenses/mit-license.php
*
*/ 

// The user can choose whether or not to include libxml support in the DOM. Supporting libxml will
// require linking against it. By default libxml support is included.
#if defined(DOM_INCLUDE_LIBXML)

// This is a rework of the XML plugin that contains a complete interface to libxml2 "readXML"
// This is intended to be a seperate plugin but I'm starting out by rewriting it in daeLIBXMLPlugin
// because I'm not sure if all the plugin handling stuff has been tested.  Once I get a working
// plugin I'll look into renaming it so the old daeLIBXMLPlugin can coexist with it.
//
#include <string>
#include <sstream>
#include <modules/daeLIBXMLPlugin.h>
#include <dae.h>
#include <dom.h>
#include <dae/daeDatabase.h>
#include <dae/daeMetaElement.h>
#include <libxml/xmlreader.h>
#include <libxml/xmlwriter.h>
#include <libxml/xmlmemory.h>
#include <dae/daeErrorHandler.h>
#include <dae/daeMetaElementAttribute.h>

using namespace std;


// Some helper functions for working with libxml
namespace {
	daeInt getCurrentLineNumber(xmlTextReaderPtr reader) {
#if LIBXML_VERSION >= 20620
	return xmlTextReaderGetParserLineNumber(reader);
#else
	return -1;
#endif
	}

	// Return value should be freed by caller with delete[]. Passed in value should not
	// be null.
	xmlChar* utf8ToLatin1(const xmlChar* utf8) {
		int inLen = xmlStrlen(utf8);
		int outLen = (inLen+1) * 2;
		xmlChar* latin1 = new xmlChar[outLen];
		int numBytes = UTF8Toisolat1(latin1, &outLen, utf8, &inLen);
		if (numBytes < 0)
			// Failed. Return an empty string instead.
			numBytes = 0;

		latin1[numBytes] = '\0';
		return latin1;
	}

	// Return value should be freed by caller with delete[].
	xmlChar* latin1ToUtf8(const string& latin1) {
		int inLen = (int)latin1.length();
		int outLen = (inLen+1) * 2;
		xmlChar* utf8 = new xmlChar[outLen];
		int numBytes = isolat1ToUTF8(utf8, &outLen, (xmlChar*)latin1.c_str(), &inLen);
		if (numBytes < 0)
			// Failed. Return an empty string instead.
			numBytes = 0;

		utf8[numBytes] = '\0';
		return utf8;
	}

	typedef pair<daeString, daeString> stringPair;
	
	// The attributes vector passed in should be empty. If 'encoding' is anything
	// other than utf8 the caller should free the returned attribute value
	// strings. The 'freeAttrValues' function is provided for that purpose.
	void packageCurrentAttributes(xmlTextReaderPtr reader,
	                              DAE::charEncoding encoding,
	                              /* out */ vector<stringPair>& attributes) {
		int numAttributes = xmlTextReaderAttributeCount(reader);
		if (numAttributes == -1 || numAttributes == 0)
			return;
		attributes.reserve(numAttributes);
		
		while (xmlTextReaderMoveToNextAttribute(reader) == 1) {
			const xmlChar* xmlName = xmlTextReaderConstName(reader);
			const xmlChar* xmlValue = xmlTextReaderConstValue(reader);
			if (encoding == DAE::Latin1)
				attributes.push_back(stringPair((daeString)xmlName, (daeString)utf8ToLatin1(xmlValue)));
			else
				attributes.push_back(stringPair((daeString)xmlName, (daeString)xmlValue));
		}
	}

	void freeAttrValues(vector<stringPair>& pairs) {
		for(size_t i=0, size=pairs.size(); i<size; ++i) {
			delete[] pairs[i].second;
			pairs[i].second = 0;
		}
	}
}

daeLIBXMLPlugin::daeLIBXMLPlugin(DAE& dae) : dae(dae), rawRelPath(dae)
{
	supportedProtocols.push_back("*");
	xmlInitParser();
	rawFile = NULL;
	rawByteCount = 0;
	saveRawFile = false;
}

daeLIBXMLPlugin::~daeLIBXMLPlugin()
{
	 xmlCleanupParser();
}

daeInt daeLIBXMLPlugin::setOption( daeString option, daeString value )
{
	if ( strcmp( option, "saveRawBinary" ) == 0 )
	{
		if ( strcmp( value, "true" ) == 0 || strcmp( value, "TRUE" ) == 0 )
		{
			saveRawFile = true;
		}
		else
		{
			saveRawFile = false;
		}
		return DAE_OK;
	}
	return DAE_ERR_INVALID_CALL;
}

daeString daeLIBXMLPlugin::getOption( daeString option )
{
	if ( strcmp( option, "saveRawBinary" ) == 0 )
	{
		if ( saveRawFile )
		{
			return "true";
		}
		return "false";
	}
	return NULL;
}

namespace {
	void libxmlErrorHandler(void* arg,
	                        const char* msg,
	                        xmlParserSeverities severity,
	                        xmlTextReaderLocatorPtr locator) {
		if(severity == XML_PARSER_SEVERITY_VALIDITY_WARNING  ||
		   severity == XML_PARSER_SEVERITY_WARNING) {
			daeErrorHandler::get()->handleWarning(msg);
		}
		else
			daeErrorHandler::get()->handleError(msg);
	}
}

// A simple structure to help alloc/free xmlTextReader objects
struct xmlTextReaderHelper {
	xmlTextReaderHelper(const daeURI& uri) {
		if((reader = xmlReaderForFile(cdom::fixUriForLibxml(uri.str()).c_str(), NULL, 0)))
		   xmlTextReaderSetErrorHandler(reader, libxmlErrorHandler, NULL);
	}

	xmlTextReaderHelper(daeString buffer, const daeURI& baseUri) {
		if((reader = xmlReaderForDoc((xmlChar*)buffer, cdom::fixUriForLibxml(baseUri.str()).c_str(), NULL, 0)))
			xmlTextReaderSetErrorHandler(reader, libxmlErrorHandler, NULL);
	};

	~xmlTextReaderHelper() {
		if (reader)
			xmlFreeTextReader(reader);
	}

	xmlTextReaderPtr reader;
};

daeElementRef daeLIBXMLPlugin::readFromFile(const daeURI& uri) {
	xmlTextReaderHelper readerHelper(uri);
	if (!readerHelper.reader) {
		daeErrorHandler::get()->handleError((string("Failed to open ") + uri.str() +
		                                    " in daeLIBXMLPlugin::readFromFile\n").c_str());
		return NULL;
	}
	return read(readerHelper.reader);
}

daeElementRef daeLIBXMLPlugin::readFromMemory(daeString buffer, const daeURI& baseUri) {
	xmlTextReaderHelper readerHelper(buffer, baseUri);
	if (!readerHelper.reader) {
		daeErrorHandler::get()->handleError("Failed to open XML document from memory buffer in "
		                                    "daeLIBXMLPlugin::readFromMemory\n");
		return NULL;
	}
	return read(readerHelper.reader);
}

daeElementRef daeLIBXMLPlugin::read(_xmlTextReader* reader) {
	// Drop everything up to the first element. In the future, we should try to store header comments somewhere.
	while(xmlTextReaderNodeType(reader) != XML_READER_TYPE_ELEMENT)
	{
		if (xmlTextReaderRead(reader) != 1) {
			daeErrorHandler::get()->handleError("Error parsing XML in daeLIBXMLPlugin::read\n");
			return NULL;
		}
	}

	int readRetVal = 0;
	return readElement(reader, NULL, readRetVal);
}

daeElementRef daeLIBXMLPlugin::readElement(_xmlTextReader* reader,
                                           daeElement* parentElement,
                                           /* out */ int& readRetVal) {
	assert(xmlTextReaderNodeType(reader) == XML_READER_TYPE_ELEMENT);
	daeString elementName = (daeString)xmlTextReaderConstName(reader);
	bool empty = xmlTextReaderIsEmptyElement(reader) != 0;

	vector<attrPair> attributes;
	packageCurrentAttributes(reader, dae.getCharEncoding(), /* out */ attributes);
	
	daeElementRef element = beginReadElement(parentElement, elementName, attributes, getCurrentLineNumber(reader));
	if (dae.getCharEncoding() != DAE::Utf8)
		freeAttrValues(attributes);

	if (!element) {
		// We couldn't create the element. beginReadElement already printed an error message. Just make sure
		// to skip ahead past the bad element.
		xmlTextReaderNext(reader);
		return NULL;
	}

	if ((readRetVal = xmlTextReaderRead(reader)) == -1)
		return NULL;
	if (empty)
		return element;

	int nodeType = xmlTextReaderNodeType(reader);
	while (readRetVal == 1  &&  nodeType != XML_READER_TYPE_END_ELEMENT) {
		if (nodeType == XML_READER_TYPE_ELEMENT) {
			element->placeElement(readElement(reader, element, readRetVal));
		}
		else if (nodeType == XML_READER_TYPE_TEXT) {
			const xmlChar* xmlText = xmlTextReaderConstValue(reader);
			if (dae.getCharEncoding() == DAE::Latin1)
				xmlText = utf8ToLatin1(xmlText);
			readElementText(element, (daeString)xmlText, getCurrentLineNumber(reader));
			if (dae.getCharEncoding() == DAE::Latin1)
				delete[] xmlText;

			readRetVal = xmlTextReaderRead(reader);
		}
		else
			readRetVal = xmlTextReaderRead(reader);

		nodeType = xmlTextReaderNodeType(reader);
	}

	if (nodeType == XML_READER_TYPE_END_ELEMENT)
		readRetVal = xmlTextReaderRead(reader);

	if (readRetVal == -1) // Something went wrong (bad xml probably)
		return NULL;

	return element;
}

daeInt daeLIBXMLPlugin::write(const daeURI& name, daeDocument *document, daeBool replace)
{
	// Make sure database and document are both set
	if (!database)
		return DAE_ERR_INVALID_CALL;
	if(!document)
		return DAE_ERR_COLLECTION_DOES_NOT_EXIST;

	// Convert the URI to a file path, to see if we're about to overwrite a file
	string file = cdom::uriToNativePath(name.str());
	if (file.empty()  &&  saveRawFile)
	{
		daeErrorHandler::get()->handleError( "can't get path in write\n" );
		return DAE_ERR_BACKEND_IO;
	}
	
	// If replace=false, don't replace existing files
	if(!replace)
	{
		// Using "stat" would be better, but it's not available on all platforms
		FILE *tempfd = fopen(file.c_str(), "r");
		if(tempfd != NULL)
		{
			// File exists, return error
			fclose(tempfd);
			return DAE_ERR_BACKEND_FILE_EXISTS;
		}
		fclose(tempfd);
	}
	if ( saveRawFile )
	{
		string rawFilePath = file + ".raw";
		if ( !replace )
		{
			rawFile = fopen(rawFilePath.c_str(), "rb" );
			if ( rawFile != NULL )
			{
				fclose(rawFile);
				return DAE_ERR_BACKEND_FILE_EXISTS;
			}
			fclose(rawFile);
		}
		rawFile = fopen(rawFilePath.c_str(), "wb");
		if ( rawFile == NULL )
		{
			return DAE_ERR_BACKEND_IO;
		}
		rawRelPath.set(cdom::nativePathToUri(rawFilePath));
		rawRelPath.makeRelativeTo( &name );
	}

	// Open the file we will write to
	writer = xmlNewTextWriterFilename(cdom::fixUriForLibxml(name.str()).c_str(), 0);
	if ( !writer ) {
		ostringstream msg;
		msg << "daeLIBXMLPlugin::write(" << name.str() << ") failed\n";
		daeErrorHandler::get()->handleError(msg.str().c_str());
		return DAE_ERR_BACKEND_IO;
	}
	xmlTextWriterSetIndentString( writer, (const xmlChar*)"\t" ); // Don't change this to spaces
	xmlTextWriterSetIndent( writer, 1 ); // Turns indentation on
    xmlTextWriterStartDocument( writer, "1.0", "UTF-8", NULL );
	
	writeElement( document->getDomRoot() );
	
	xmlTextWriterEndDocument( writer );
	xmlTextWriterFlush( writer );
	xmlFreeTextWriter( writer );

	if ( saveRawFile && rawFile != NULL )
	{
		fclose( rawFile );
	}

	return DAE_OK;
}

void daeLIBXMLPlugin::writeElement( daeElement* element )
{
	daeMetaElement* _meta = element->getMeta();

	//intercept <source> elements for special handling
	if ( saveRawFile )
	{
		if ( strcmp( element->getTypeName(), "source" ) == 0 )
		{
			daeElementRefArray children;
			element->getChildren( children );
			bool validArray = false, teqCommon = false;
			for ( unsigned int i = 0; i < children.getCount(); i++ )
			{
				if ( strcmp( children[i]->getTypeName(), "float_array" ) == 0 || 
					 strcmp( children[i]->getTypeName(), "int_array" ) == 0 )
				{
					validArray = true;
				}
				else if ( strcmp( children[i]->getTypeName(), "technique_common" ) == 0 )
				{
					teqCommon = true;
				}
			}
			if ( validArray && teqCommon )
			{
				writeRawSource( element );
				return;
			}
		}
	}

	if (!_meta->getIsTransparent() ) {
		xmlTextWriterStartElement(writer, (xmlChar*)element->getElementName());
		daeMetaAttributeRefArray& attrs = _meta->getMetaAttributes();
		
		int acnt = (int)attrs.getCount();
		
		for(int i=0;i<acnt;i++) {
			writeAttribute(attrs[i], element);
		}
	}
	writeValue(element);
	
	daeElementRefArray children;
	element->getChildren( children );
	for ( size_t x = 0; x < children.getCount(); x++ ) {
		writeElement( children.get(x) );
	}

	/*if (_meta->getContents() != NULL) {
		daeElementRefArray* era = (daeElementRefArray*)_meta->getContents()->getWritableMemory(element);
		int elemCnt = (int)era->getCount();
		for(int i = 0; i < elemCnt; i++) {
			daeElementRef elem = (daeElementRef)era->get(i);
			if (elem != NULL) {
				writeElement( elem );
			}
		}
	}
	else
	{
		daeMetaElementAttributeArray& children = _meta->getMetaElements();
		int cnt = (int)children.getCount();
		for(int i=0;i<cnt;i++) {
			daeMetaElement *type = children[i]->getElementType();
			if ( !type->getIsAbstract() ) {
				for (int c = 0; c < children[i]->getCount(element); c++ ) {
					writeElement( *(daeElementRef*)children[i]->get(element,c) );
				}
			}
		}
	}*/
	if (!_meta->getIsTransparent() ) {
		xmlTextWriterEndElement(writer);
	}
}

void daeLIBXMLPlugin::writeAttribute( daeMetaAttribute* attr, daeElement* element)
{
	ostringstream buffer;
	attr->memoryToString(element, buffer);
	string str = buffer.str();

	// Don't write the attribute if
	//  - The attribute isn't required AND
	//     - The attribute has no default value and the current value is ""
	//     - The attribute has a default value and the current value matches the default
	if (!attr->getIsRequired()) {
		if(!attr->getDefaultValue()  &&  str.empty())
			return;
		if(attr->getDefaultValue()  &&  attr->compareToDefault(element) == 0)
			return;
	}

	xmlTextWriterStartAttribute(writer, (xmlChar*)(daeString)attr->getName());
	xmlChar* utf8 = (xmlChar*)str.c_str();
	if (dae.getCharEncoding() == DAE::Latin1)
		utf8 = latin1ToUtf8(str);
	xmlTextWriterWriteString(writer, utf8);
	if (dae.getCharEncoding() == DAE::Latin1)
		delete[] utf8;
	
	xmlTextWriterEndAttribute(writer);
}

void daeLIBXMLPlugin::writeValue(daeElement* element) {
	if (daeMetaAttribute* attr = element->getMeta()->getValueAttribute()) {
		ostringstream buffer;
		attr->memoryToString(element, buffer);
		string s = buffer.str();
		if (!s.empty()) {
			xmlChar* str = (xmlChar*)s.c_str();
			if (dae.getCharEncoding() == DAE::Latin1)
				str = latin1ToUtf8(s);
			xmlTextWriterWriteString(writer, (xmlChar*)s.c_str());
			if (dae.getCharEncoding() == DAE::Latin1)
				delete[] str;
		}
	}
}

void daeLIBXMLPlugin::writeRawSource( daeElement *src )
{
	daeElementRef newSrc = src->clone();
	daeElementRef array = NULL;
	daeElement *accessor = NULL;
	daeElementRefArray children;
	newSrc->getChildren( children );
	bool isInt = false;
	for ( int i = 0; i < (int)children.getCount(); i++ )
	{
		if ( strcmp( children[i]->getTypeName(), "float_array" ) == 0 )
		{
			array = children[i];
			newSrc->removeChildElement( array );
		}
		else if ( strcmp( children[i]->getTypeName(), "int_array" ) == 0 )
		{
			array = children[i];
			isInt = true;
			newSrc->removeChildElement( array );
		}
		else if ( strcmp( children[i]->getTypeName(), "technique_common" ) == 0 )
		{
			children[i]->getChildren( children );
		}
		else if ( strcmp( children[i]->getTypeName(), "accessor" ) == 0 )
		{
			accessor = children[i];
		}
	}

	daeULong *countPtr = (daeULong*)array->getAttributeValue( "count" );
	daeULong count = countPtr != NULL ? *countPtr : 0;

	daeULong *stridePtr = (daeULong*)accessor->getAttributeValue( "stride" );
	daeULong stride = stridePtr != NULL ? *stridePtr : 1;

	children.clear();
	accessor->getChildren( children );
	if ( children.getCount() > stride ) {
		*stridePtr = children.getCount();
	}

	daeFixedName newURI;
	sprintf( newURI, "%s#%ld", rawRelPath.getOriginalURI(), rawByteCount );
	accessor->setAttribute( "source", newURI );

	daeArray *valArray = (daeArray*)array->getValuePointer();

	//TODO: pay attention to precision for the array.
	if ( isInt )
	{
		for( size_t i = 0; i < count; i++ )
		{
			daeInt tmp = (daeInt)*(daeLong*)(valArray->getRaw(i));
			rawByteCount += (unsigned long)(fwrite( &tmp, sizeof(daeInt), 1, rawFile ) * sizeof(daeInt));
		}
	}
	else
	{
		for( size_t i = 0; i < count; i++ )
		{
			daeFloat tmp = (daeFloat)*(daeDouble*)(valArray->getRaw(i));
			rawByteCount += (unsigned long)(fwrite( &tmp, sizeof(daeFloat), 1, rawFile ) * sizeof(daeFloat));
		}
	}

	writeElement( newSrc );
}

#endif // DOM_INCLUDE_LIBXML
