//-----------------------------------------------------------------------------------------------
// SquirrelXML.cpp
//
// Todo/wishlist:
//
// - Validate: exactly one root node
// - Validate: < is followed by non-whitespace, and also not followed by >
// - Validate: after element name has been parsed, / is followed by >
// - Support Unicode content and markup characters
// - Support Unicode file and folder names
//
#include "GypsyPrecompiledHeader.hpp"

//-----------------------------------------------------------------------------------------------
#include "Engine/Core/SquirrelXML.hpp"
#include <string.h>
#include <stdio.h>
#include <vector>


//-----------------------------------------------------------------------------------------------
void AdvanceToEndOfAttributeName( const char*& scan )
{
	for( ;; )
	{
		switch( *scan )
		{
		case ' ':
		case '\t':
		case '\r':
		case '\n':
		case '\0':
		case '=':
			return;

		default:
			++ scan;
		}
	}
}


//-----------------------------------------------------------------------------------------------
void AdvanceToEndOfElementName( const char*& scan )
{
	for( ;; )
	{
		switch( *scan )
		{
		case ' ':
		case '\t':
		case '\r':
		case '\n':
		case '\0':
		case '/':
		case '>':
			return;

		default:
			++ scan;
		}
	}
}


//-----------------------------------------------------------------------------------------------
void AdvanceToEndOfMetaElementName( const char*& scan )
{
	for( ;; )
	{
		switch( *scan )
		{
		case '\r':
		case '\n':
		case '\0':
		case '>':
			return;

		default:
			++ scan;
		}
	}
}


/////////////////////////////////////////////////////////////////////////////////////////////////
// SquirrelXMLSubstring
//

//-----------------------------------------------------------------------------------------------
bool SquirrelXMLSubstring::GetText( char* out_charBuffer, int bufferSize ) const
{
	if( m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	if( m_substringLength > bufferSize-1 )
		return false; // The buffer provided was not large enough

	memcpy( out_charBuffer, m_substringStart, m_substringLength );
	out_charBuffer[ m_substringLength ] = '\0';
	return true;
}


//-----------------------------------------------------------------------------------------------
#ifdef SQUIRRELXML_ALLOW_STL
bool SquirrelXMLSubstring::GetString( std::string& out_string ) const
{
	if( m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	out_string.assign( m_substringStart, m_substringLength );
	return true;
}
#endif



/////////////////////////////////////////////////////////////////////////////////////////////////
// SquirrelXMLAttribute
//

//-----------------------------------------------------------------------------------------------
SquirrelXMLAttribute::~SquirrelXMLAttribute()
{
	if( m_nextSiblingAttribute )
		delete m_nextSiblingAttribute; // chain-delete my next sibling
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLAttribute::GetAttributeDataAsText( char* out_attributeDataCharBuffer, int bufferSize ) const
{
	if( bufferSize >= 1 )
		out_attributeDataCharBuffer[0] = '\0';

	if( m_attributeDataSubstring.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	if( m_attributeDataSubstring.m_substringLength > bufferSize-1 )
		return false; // The buffer provided was not large enough

	const SquirrelXMLSubstring& subString = m_attributeDataSubstring;
	memcpy( out_attributeDataCharBuffer, subString.m_substringStart, subString.m_substringLength );
	out_attributeDataCharBuffer[ subString.m_substringLength ] = '\0';
	return true;
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLAttribute::GetAttributeDataAsCharLiteral( char& out_attributeDataAsCharLiteral ) const
{
	if( m_attributeDataSubstring.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	if( m_attributeDataSubstring.m_substringLength == 0 )
		return false;

	const char* stringStart = m_attributeDataSubstring.m_substringStart;
	int stringLength = m_attributeDataSubstring.m_substringLength;
	if( (*stringStart == '#') && (stringLength > 1) && (stringLength <= 4) )
	{
		char ucsDecimalString[ 5 ];
		memcpy( ucsDecimalString, stringStart, stringLength );
		ucsDecimalString[ stringLength ] = '\0';
		int ucsValue = atoi( &ucsDecimalString[1] );
		out_attributeDataAsCharLiteral = static_cast< char >( ucsValue );
	}
	else
	{
		out_attributeDataAsCharLiteral = *stringStart;
	}

	return true;
}


#ifdef SQUIRRELXML_ALLOW_STL
//-----------------------------------------------------------------------------------------------
bool SquirrelXMLAttribute::GetAttributeDataAsString( std::string& out_attributeDataAsString ) const
{
	if( m_attributeDataSubstring.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	out_attributeDataAsString.assign( m_attributeDataSubstring.m_substringStart, m_attributeDataSubstring.m_substringLength );
	return true;
}
#endif


/////////////////////////////////////////////////////////////////////////////////////////////////
// SquirrelXMLElement
//

//-----------------------------------------------------------------------------------------------
SquirrelXMLElement::~SquirrelXMLElement()
{
	if( m_firstChildElement )
		delete m_firstChildElement; // chain-deletes its own siblings

	if( m_firstAttribute )
		delete m_firstAttribute; // chain-deletes its own siblings

	if( m_nextSiblingElement )
		delete m_nextSiblingElement; // chain-delete my next sibling
}


#ifdef SQUIRRELXML_ALLOW_STL
//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetTextAttribute( const char* attributeName, std::string& out_stringValue ) const
{
	SquirrelXMLAttribute* attribute = GetAttribute( attributeName );
	if( attribute )
	{
		return attribute->GetAttributeDataAsString( out_stringValue );
	}
	else
	{
		return false;
	}
}
#endif


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetElementName( char* out_elementNameCharBuffer, int bufferSize ) const
{
	if( m_elementNameSubstring.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no element name to get

	if( m_elementNameSubstring.m_substringLength > bufferSize-1 )
		return false; // The buffer provided was not large enough

	const SquirrelXMLSubstring& subString = m_elementNameSubstring;
	memcpy( out_elementNameCharBuffer, subString.m_substringStart, subString.m_substringLength );
	out_elementNameCharBuffer[ subString.m_substringLength ] = '\0';
	return true;
}


//-----------------------------------------------------------------------------------------------
SquirrelXMLAttribute* SquirrelXMLElement::GetAttribute( const char* attributeName ) const
{
	SquirrelXMLAttribute* checkAttribute = m_firstAttribute;
	while( checkAttribute != nullptr )
	{
		if( checkAttribute->DoesAttributeNameEqual( attributeName ) )
			return checkAttribute;
		else
			checkAttribute = checkAttribute->m_nextSiblingAttribute;
	}

	return nullptr;
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetCharLiteralAttribute( const char* attributeName, char& out_charLiteralData ) const
{
	SquirrelXMLAttribute* attribute = GetAttribute( attributeName );
	if( attribute )
	{
		return attribute->GetAttributeDataAsCharLiteral( out_charLiteralData );
	}
	else
	{
		return false;
	}
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetTextAttribute( const char* attributeName, char* out_textValueBuffer, int bufferSize ) const
{
	SquirrelXMLAttribute* attribute = GetAttribute( attributeName );
	if( attribute )
	{
		return attribute->GetAttributeDataAsText( out_textValueBuffer, bufferSize );
	}
	else
	{
		return false;
	}
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetCommentBeforeAsText( char* out_commentBuffer, int bufferSize ) const
{
	if( bufferSize >= 1 )
		out_commentBuffer[0] = '\0';

	if( m_commentBefore.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no comment to get

	if( m_commentBefore.m_substringLength > bufferSize-1 )
		return false; // The buffer provided was not large enough

	const SquirrelXMLSubstring& substring = m_commentBefore;
	memcpy( out_commentBuffer, substring.m_substringStart, substring.m_substringLength );
	out_commentBuffer[ substring.m_substringLength ] = '\0';
	return true;
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLElement::GetPCDataAsText( char* out_pcDataCharBuffer, int bufferSize ) const
{
	if( bufferSize >= 1 )
		out_pcDataCharBuffer[0] = '\0';

	if( m_pcData.m_substringStart == nullptr )
		return false; // Internal XML structure error - there was no attribute name to get

	if( m_pcData.m_substringLength > bufferSize-1 )
		return false; // The buffer provided was not large enough

	const SquirrelXMLSubstring& substring = m_pcData;
	memcpy( out_pcDataCharBuffer, substring.m_substringStart, substring.m_substringLength );
	out_pcDataCharBuffer[ substring.m_substringLength ] = '\0';
	return true;
}



/////////////////////////////////////////////////////////////////////////////////////////////////
// SquirrelXMLDocument
//

//-----------------------------------------------------------------------------------------------
SquirrelXMLDocument::SquirrelXMLDocument( const char* xmlFilePath )
	: m_rawText( nullptr )
	, m_rawTextLength( 0 )
	, m_rootElement( nullptr )
{
	strcpy_s( m_xmlFilePath, MAX_XML_FILE_PATH_LENGTH, xmlFilePath );
	LoadXMLFileToBuffer( xmlFilePath );
	BuildXMLDocumentStructureFromBuffer();
}


//-----------------------------------------------------------------------------------------------
SquirrelXMLDocument::~SquirrelXMLDocument()
{
	delete m_rootElement;
	m_rootElement = nullptr;
	delete[] m_rawText;
	m_rawText = nullptr;
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLDocument::LoadXMLFileToBuffer( const char* xmlFilePath )
{
	FILE* fileHandle = nullptr;
	const errno_t errorCode = fopen_s( &fileHandle, xmlFilePath, "rb" );
	if( errorCode )
	{
		return false;
	}
	else
	{
		fseek( fileHandle, 0, SEEK_END );
		int fileSizeInBytes = (int) ftell( fileHandle );
		rewind( fileHandle );

		m_rawText = new char[ fileSizeInBytes + 1 ];
		fread( m_rawText, 1, fileSizeInBytes, fileHandle );
		fclose( fileHandle );
		m_rawText[ fileSizeInBytes ] = '\0';
		m_rawTextLength = fileSizeInBytes;
		return true;
	}
}


//-----------------------------------------------------------------------------------------------
void SquirrelXMLDocument::BuildXMLDocumentStructureFromBuffer()
{
	if( m_rootElement )
	{
		delete m_rootElement;
		m_rootElement = nullptr;
	}

	if( !m_rawText || m_rawTextLength < 3 )
	{
		m_rootElement = nullptr;
		return;
	}

	int lineIndex = 0;
	const char* scan = m_rawText;
	const char* startOfCurrentLine = scan;
	std::vector< SquirrelXMLElement* >	elementContextStack;
	SquirrelXMLElement* previousSiblingElement = nullptr;
	SquirrelXMLSubstring commentBeforeNextElement;

	for( ;; )
	{
		SquirrelXML_SkipWhitespace( scan, lineIndex, startOfCurrentLine );
		if( *scan == '\0' )
		{
			break; // End of buffer
		}
		else if( *scan == '<' )
		{
			// This is one of: open-element tag, close-element tag, empty-element tag
			++ scan; // skip opening '<'
			if( *scan == '!' )
			{
				// This is a special block (most likely a comment block)
				++ scan; // skip preceding '!'
				if( scan[0] == '-' && scan[1] == '-' )
				{
					// This is a comment block; capture (ignore) EVERYTHING until we see "-->"
					scan += 2; // Skip leading "--"
					const char* commentStart = scan;
					while( scan[0] != '-' || scan[1] != '-' || scan[2] != '>' )
					{
						if( *scan == '\0' )
							break; // ERROR: encountered EOF while inside a comment block

						if( *scan == '\n' )
						{
							++ lineIndex;
							startOfCurrentLine = scan+1;
						}

						++ scan;
					}

					if( scan[0] == '-' && scan[1] == '-' && scan[2] == '>' )
					{
						int commentLength = scan - commentStart;
						if( commentBeforeNextElement.IsEmpty() )
						{
							// This is now our "current" comment block, to be tacked onto the next real element
							commentBeforeNextElement.m_substringStart = commentStart;
							commentBeforeNextElement.m_substringLength = commentLength;
						}
						else
						{
							// This comment block follows another comment block; merge them
							commentBeforeNextElement.m_substringLength = scan - commentBeforeNextElement.m_substringStart;
						}
						scan += 3; // Skip trailing "-->"
					}
				}
				else
				{
					// Unknown special block (could be any of <!ELEMENT or <!ATTLIST or <!DOCTYPE or <!ENTITY or...
					// Ignore, and skip to ending 
					while( *scan != '>' && *scan != '\0' )
					{
						if( *scan == '\n' )
						{
							++ lineIndex;
							startOfCurrentLine = scan+1;
						}

						++ scan;
					}

					if( *scan == '>' )
						++ scan;
				}
			}
			else if( *scan == '/' )
			{
				// This is a close-element tag
				SquirrelXMLElement* elementAboutToClose = elementContextStack.back();
				elementAboutToClose->m_pcData.m_substringLength = (scan-1) - elementAboutToClose->m_pcData.m_substringStart;
				++ scan; // skip preceding '/'
				previousSiblingElement = elementAboutToClose;
				AdvanceToEndOfElementName( scan ); // TODO: make sure this close-element tag name matches the one at the top of the current context stack
				elementContextStack.pop_back();
				++ scan; // skip ending '>'
			}
			else if( *scan == '?' )
			{
				// This is a funky (optional) opening meta-element.  Ignore and skip for now (revisit when we come back for Unicode).
				AdvanceToEndOfMetaElementName( scan );
				++ scan; // skip ending '>'
			}
			else
			{
				// This is an open-element (or empty-element) tag
				SquirrelXMLElement* newElement = new SquirrelXMLElement(); // TODO: batch or otherwise pool-manage these for performance
				newElement->m_lineNumber = lineIndex;
				newElement->m_charIndexOnLine = (scan-1) - startOfCurrentLine;
				newElement->m_commentBefore = commentBeforeNextElement;
				commentBeforeNextElement.Clear();
				if( m_rootElement )
				{
					// FIXME: crashes here for XML files with more than one root node; TODO: validate exactly one root node
					newElement->m_parentElement = elementContextStack.back();
					if( !newElement->m_parentElement->m_firstChildElement )
						newElement->m_parentElement->m_firstChildElement = newElement;
				}
				else
				{
					m_rootElement = newElement;
				}

				if( previousSiblingElement )
					previousSiblingElement->m_nextSiblingElement = newElement;

				elementContextStack.push_back( newElement );
				newElement->m_elementNameSubstring.m_substringStart = scan;
				AdvanceToEndOfElementName( scan );
				newElement->m_elementNameSubstring.m_substringLength = (scan - newElement->m_elementNameSubstring.m_substringStart);

				// Parse attributes (if any)
				SquirrelXMLAttribute* previousSiblingAttribute = nullptr;
				bool doneParsingAttributes = false;
				while( !doneParsingAttributes )
				{
					SquirrelXML_SkipWhitespace( scan, lineIndex, startOfCurrentLine );
					switch( *scan )
					{
						case '/': // End of empty-element tag
							++ scan; // skip the '/'
							previousSiblingElement = elementContextStack.back();
							elementContextStack.pop_back();
							++ scan; // skip the '>'
							doneParsingAttributes = true;
							break;

						case '>': // End of open-element tag
							++ scan; // skip the '>'
							previousSiblingElement = nullptr;
							doneParsingAttributes = true;
							newElement->m_pcData.m_substringStart = scan; // This element's PCData starts immediately after its open-tag (and may be 0 characters)
							break;

						case '\0':
							doneParsingAttributes = true;
							break;

						default:
							// Parse attribute
							SquirrelXMLAttribute* newAttribute = new SquirrelXMLAttribute(); // TODO: batch or otherwise pool-manage these for performance
							newAttribute->m_lineNumber = lineIndex;
							newAttribute->m_charIndexOnLine = scan - startOfCurrentLine;
							newAttribute->m_ownerElement = newElement;
							if( !newElement->m_firstAttribute )
							{
								newElement->m_firstAttribute = newAttribute;
							}

							if( previousSiblingAttribute )
							{
								previousSiblingAttribute->m_nextSiblingAttribute = newAttribute;
							}

							newAttribute->m_attributeNameSubstring.m_substringStart = scan;
							AdvanceToEndOfAttributeName( scan );
							newAttribute->m_attributeNameSubstring.m_substringLength = (scan - newAttribute->m_attributeNameSubstring.m_substringStart);
							SquirrelXML_SkipWhitespace( scan, lineIndex, startOfCurrentLine );
							// assert that we're on '='
							++ scan; // skip the '='
							SquirrelXML_SkipWhitespace( scan, lineIndex, startOfCurrentLine );
							// assert that we're on '\"'
							newAttribute->m_attributeDataSubstring.m_substringStart = scan + 1;
							SquirrelXML_SkipQuotedString( scan, lineIndex, startOfCurrentLine );
							newAttribute->m_attributeDataSubstring.m_substringLength = (scan - newAttribute->m_attributeDataSubstring.m_substringStart - 1);
							previousSiblingAttribute = newAttribute;
							break;
					}
				}
			}
		}
		else
		{
			// PCDATA section (Parsed Character Data), for example:
			//	<abc>This is the PCDATA section</abc>
			//	<def>It might even contain <b>nested</b> elements</def>
			// Ignore for now; this will be picked up by the element when we encounter its closing element
			while( *scan != '\0' && *scan != '<' )
			{
				if( *scan == '\n' )
				{
					++ lineIndex;
					startOfCurrentLine = scan+1;
				}

				++ scan;
			}
		}
	}
}


//-----------------------------------------------------------------------------------------------
SquirrelXMLElement* SquirrelXMLDocument::GetRootElement()
{
	return m_rootElement;
}


//-----------------------------------------------------------------------------------------------
bool SquirrelXMLDocument::IsValid() const
{
	return( m_rootElement != nullptr );
}


/////////////////////////////////////////////////////////////////////////////////////////////////
// Misc. utility functions
//

//-----------------------------------------------------------------------------------------------
void SquirrelXML_SkipWhitespace( const char*& scan, int& lineIndex, const char*& startOfLine )
{
	for( ;; )
	{
		switch( *scan )
		{
			case '\n':
				++ lineIndex;
				startOfLine = scan+1;
				// fall through...
			case ' ':
			case '\t':
			case '\r':
				++ scan;
				break;

			default:
				return;
		}
	}
}


//-----------------------------------------------------------------------------------------------
void SquirrelXML_SkipQuotedString( const char*& scan, int& lineIndex, const char*& startOfLine )
{
	++ scan; // skip opening quote
	while( *scan != '\0' && *scan != '\"' )
	{
		if( *scan == '\n' )
		{
			++ lineIndex;
			startOfLine = scan+1;
		}

		++ scan;
	}

	if( *scan == '\"' )
		++ scan;
}



