// NeL - MMORPG Framework <http://dev.ryzom.com/projects/nel/>
// Copyright (C) 2010  Winch Gate Property Limited
//
// 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 "stdmisc.h"

#include "nel/misc/o_xml.h"

#ifndef NL_DONT_USE_EXTERNAL_CODE

// Include from libxml2
#include <libxml/xmlerror.h>

using namespace std;

namespace NLMISC
{

// ***************************************************************************

const char SEPARATOR = ' ';

// ***************************************************************************

#define writenumber(src,format,digits) \
	char number_as_cstring [digits+1]; \
	sprintf( number_as_cstring, format, src ); \
	serialSeparatedBufferOut( number_as_cstring );

// ***************************************************************************
// XML callbacks
// ***************************************************************************

int xmlOutputWriteCallbackForNeL ( void *context, const char *buffer, int len)
{
	// no need to save empty buffer
	if(len == 0) return 0;

	// Get the object
	COXml *object = (COXml*) context;

	// Serialise the buffer
	object->_InternalStream->serialBuffer ((uint8*)buffer, len);

	// Return the value
	return len;
}

// ***************************************************************************

int xmlOutputCloseCallbackForNeL ( void * /* context */ )
{
	// Get the object
	// COXml *object = (COXml*) context;

	// Does nothing
	return 1;
}

// ***************************************************************************

xmlDocPtr COXml::getDocument ()
{
	if (_Document)
		return _Document;

	// Initialise the document
	_Document = xmlNewDoc ((const xmlChar *)_Version.c_str());

	return _Document;
}


// ***************************************************************************

inline void COXml::flushContentString ()
{
	// Current node must exist here
	nlassert (_CurrentNode);

	// String size
	uint size=(uint)_ContentString.length();

	// Some content to write ?
	if (size)
	{
		// Write it in the current node
		xmlNodePtr textNode = xmlNewText ((const xmlChar *)_ContentString.c_str());
		xmlAddChild (_CurrentNode, textNode);

		// Empty the string
		_ContentString.erase ();
	}
}

// ***************************************************************************

COXml::COXml () : IStream (false /* Output mode */)
{
	// Set XML mode
	setXMLMode (true);

	// Set the stream
	_InternalStream = NULL;

	// Set the version
	_Version = "1.0";

	// Initialise the document
	_Document = NULL;

	// Current node
	_CurrentNode = NULL;

	// Content string
	_ContentString = "";

	// Push begin
	_PushBegin = false;
}

// ***************************************************************************

void xmlGenericErrorFuncWrite (void *ctx, const char *msg, ...)
{
	// Get the error string
	string str;
	NLMISC_CONVERT_VARGS (str, msg, NLMISC::MaxCStringSize);
	((COXml*)ctx)->_ErrorString += str;
}

// ***************************************************************************

bool COXml::init (IStream *stream, const char *version)
{
	resetPtrTable();

	// Output stream ?
	if (!stream->isReading())
	{
		// Set error handler
		_ErrorString = "";
		xmlSetGenericErrorFunc	(this, xmlGenericErrorFuncWrite);

		// Set XML mode
		setXMLMode (true);

		// Set the stream
		_InternalStream = stream;

		// Set the version
		_Version = version;

		// Initialise the document
		_Document = NULL;

		// Current node
		_CurrentNode = NULL;

		// Content string
		_ContentString = "";

		// Push begin
		_PushBegin = false;

		// Ok
		return true;
	}
	else
		return false;
}

// ***************************************************************************

COXml::~COXml ()
{
	// Flush document to the internal stream
	flush ();
}

// ***************************************************************************

void COXml::serialSeparatedBufferOut( const char *value )
{
	nlassert( ! isReading() );

	// Output stream has been setuped ?
	if ( _InternalStream )
	{
		// Current node presents ?
		if (_CurrentNode)
		{
			// Write a push attribute ?
			if (_PushBegin)
			{
				// Current attrib is set ?
				if (_AttribPresent)
				{
					// Set the attribute
					xmlSetProp (_CurrentNode, (const xmlChar*)_AttribName.c_str(), (const xmlChar*)value);

					// The attribute has been used
					_AttribPresent = false;
				}
				else
				{
					// * Error, the stream don't use XML streaming properly
					// * You must take care of this in your last serial call:
					// * - Between xmlPushBegin() and xmlPushEnd(), before each serial, you must set the attribute name with xmlSetAttrib.
					// * - Between xmlPushBegin() and xmlPushEnd(), you must serial only basic objects (numbers and strings).
					nlerror ( "Error, the stream don't use XML streaming properly" );
				}
			}
			else
			{
				// Get the content buffer size
				uint size=(uint)_ContentString.length();

				// Add a separator
				if ((size) && (_ContentString[size-1]!='\n'))
					_ContentString += SEPARATOR;

				// Concat the strings
				_ContentString += value;
			}
		}
		else
		{
			// * Error, no current node present.
			// * Check that your serial is initialy made between a xmlPushBegin and xmlPushEnd calls.
			nlerror ( "Error, the stream don't use XML streaming properly" );
		}
	}
	else
	{
		nlerror ( "Output stream has not been setuped" );
	}
}

// ***************************************************************************

void COXml::serial(uint8 &b)
{
	// Write the number
	writenumber( (uint16)b,"%hu", 3 );
}

// ***************************************************************************

void COXml::serial(sint8 &b)
{
	writenumber( (sint16)b, "%hd", 4 );
}

// ***************************************************************************

void COXml::serial(uint16 &b)
{
	writenumber( b, "%hu", 5 );
}

// ***************************************************************************

void COXml::serial(sint16 &b)
{
	writenumber( b, "%hd", 6 );
}

// ***************************************************************************

void COXml::serial(uint32 &b)
{
	writenumber( b, "%u", 10 );
}

// ***************************************************************************

void COXml::serial(sint32 &b)
{
	writenumber( b, "%d", 11 );
}

// ***************************************************************************

void COXml::serial(uint64 &b)
{
	writenumber( b, "%"NL_I64"u", 20 );
}

// ***************************************************************************

void COXml::serial(sint64 &b)
{
	writenumber( b, "%"NL_I64"d", 20 );
}

// ***************************************************************************

void COXml::serial(float &b)
{
	writenumber( (double)b, "%f", 128 );
}

// ***************************************************************************

void COXml::serial(double &b)
{
	writenumber( b, "%f", 128 );
}

// ***************************************************************************

void COXml::serial(bool &b)
{
	serialBit(b);
}

// ***************************************************************************

void COXml::serialBit(bool &bit)
{
	uint8 u = (uint8)bit;
	serial( u );
}

// ***************************************************************************

#ifndef NL_OS_CYGWIN
void COXml::serial(char &b)
{
	char tmp[2] = {b , 0};
	serialSeparatedBufferOut( tmp );
}
#endif // NL_OS_CYGWIN

// ***************************************************************************

void COXml::serial(std::string &b)
{
	nlassert( ! isReading() );

	// Attibute ?
	if (_PushBegin)
	{
		// Only serial the string
		serialSeparatedBufferOut( b.c_str() );
	}
	else
	{
		// Open a string node
		xmlPush ("S");

		// Serial the string
		serialSeparatedBufferOut( b.c_str() );

		// Close the node
		xmlPop ();
	}
}

// ***************************************************************************

void COXml::serial(ucstring &b)
{
	nlassert( ! isReading() );

	// convert ucstring to utf-8 std::string
	std::string output = b.toUtf8();

	// Serial this string
	serial (output);
}

// ***************************************************************************

void COXml::serialBuffer(uint8 *buf, uint len)
{
	// Open a node
	xmlPush ("BUFFER");

	// Serialize the buffer
	for (uint i=0; i<len; i++)
	{
		xmlPush ("ELM");

		serial (buf[i]);

		xmlPop ();
	}

	// Close the node
	xmlPop ();
}

// ***************************************************************************

bool COXml::xmlPushBeginInternal (const char *nodeName)
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Can make a xmlPushBegin ?
		if ( ! _PushBegin )
		{
			// Current node exist ?
			if (_CurrentNode==NULL)
			{
				// No document ?
				if (_Document == NULL)
				{
					// Initialise the document
					_Document = xmlNewDoc ((const xmlChar *)_Version.c_str());

					// Return NULL if error
					nlassert (_Document);
				}

				// Create the first node
				_CurrentNode=xmlNewDocNode (_Document, NULL, (const xmlChar*)nodeName, NULL);
				xmlDocSetRootElement (_Document, _CurrentNode);

				// Return NULL if error
				nlassert (_CurrentNode);
			}
			else
			{
				// Flush current content string ?
				flushContentString ();

				// Create a new node
				_CurrentNode=xmlNewChild (_CurrentNode, NULL, (const xmlChar*)nodeName, NULL);

				// Return NULL if error
				nlassert (_CurrentNode);
			}

			// Push begun
			_PushBegin = true;
		}
		else
		{
			nlwarning ( "XML: You must close your xmlPushBegin - xmlPushEnd before calling a new xmlPushBegin.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}

	// Ok
	return true;
}

// ***************************************************************************

bool COXml::xmlPushEndInternal ()
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Can make a xmlPushEnd ?
		if ( _PushBegin )
		{
			// Push begun
			_PushBegin = false;
		}
		else
		{
			nlwarning ( "XML: You must call xmlPushBegin before calling xmlPushEnd.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}

	// Ok
	return true;
}

// ***************************************************************************

bool COXml::xmlPopInternal ()
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Not in the push mode ?
		if ( ! _PushBegin )
		{
			// Some content to write ?
			flushContentString ();

			// Get parent
			_CurrentNode=_CurrentNode->parent;
		}
		else
		{
			nlwarning ( "XML: You must call xmlPop after xmlPushEnd.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}

	// Ok
	return true;
}

// ***************************************************************************

bool COXml::xmlSetAttribInternal (const char *attribName)
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Can make a xmlPushEnd ?
		if ( _PushBegin )
		{
			// Set attribute name
			_AttribName = attribName;

			// Attribute name is present
			_AttribPresent = true;
		}
		else
		{
			nlwarning ( "XML: You must call xmlSetAttrib between xmlPushBegin and xmlPushEnd calls.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}

	// Ok
	return true;
}

// ***************************************************************************

bool COXml::xmlBreakLineInternal ()
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Not in the push mode ?
		if ( ! _PushBegin )
		{
			// Add a break line
			_ContentString += '\n';
		}
		else
		{
			nlwarning ( "XML: You must call xmlNBreakLine after xmlPushEnd.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}

	// Ok
	return true;
}

// ***************************************************************************

bool COXml::xmlCommentInternal (const char *comment)
{
	nlassert( ! isReading() );

	// Check _InternalStream
	if ( _InternalStream )
	{
		// Not in the push mode ?
		if ( _CurrentNode != NULL)
		{
			// Add a comment node
			xmlNodePtr commentPtr = xmlNewComment ((const xmlChar *)comment);

			// Add the node
			xmlAddChild (_CurrentNode, commentPtr);
		}
		else
		{
			nlwarning ( "XML: You must call xmlCommentInternal between xmlPushBegin and xmlPushEnd.");
			return false;
		}
	}
	else
	{
		nlwarning ( "XML: Output stream has not been setuped.");
		return false;
	}
	// Ok
	return true;
}

// ***************************************************************************

void COXml::flush ()
{
	if (_Document)
	{
		// Generate indentation
		xmlKeepBlanksDefault (0);

		// Create a output context
		xmlOutputBufferPtr outputBuffer = xmlOutputBufferCreateIO  ( xmlOutputWriteCallbackForNeL, xmlOutputCloseCallbackForNeL, this, NULL );

		// Save the file
		int res = xmlSaveFormatFileTo (outputBuffer, _Document, NULL, 1);

		// No error should be returned because, exception should be raised by the internal stream
		nlassert (res!=-1);

		// Free the document
		xmlFreeDoc (_Document);
		_Document = NULL;
	}
}

// ***************************************************************************

bool COXml::isStringValidForProperties (const char *str)
{
	while (*str)
	{
		if (*str == '\n')
			return false;
		str++;
	}
	return true;
}

// ***************************************************************************

const char *COXml::getErrorString () const
{
	return _ErrorString.c_str ();
}

} // NLMISC

#endif // NL_DONT_USE_EXTERNAL_CODE