//-
// ==========================================================================
// Copyright 2009 Autodesk, Inc. All rights reserved.
//
// Use of this software is subject to the terms of the Autodesk
// license agreement provided at the time of installation or download,
// or which otherwise accompanies this software in either electronic
// or hard copy form.
// ==========================================================================
//+

//
// Description:
//	This plug-in provides an example of the use of MPxCacheFormat.
//
//	In this example, the cache files are written in xml format.
//

#include <string>
#include <stack>
#include <fstream>
#include <sstream>

#include <assert.h>
#include <stdlib.h>
#include <maya/MFnPlugin.h>
#include <maya/MGlobal.h>
#include <maya/MString.h>
#include <maya/MPxCacheFormat.h>
#include <maya/MTime.h>
#include <maya/MFloatVectorArray.h>
#include <maya/MDoubleArray.h>
#include <maya/MFloatArray.h>
#include <maya/MIntArray.h>
#include <maya/MVectorArray.h>

using namespace std;

class XmlCacheFormat : public MPxCacheFormat
{
public:
	XmlCacheFormat();
	~XmlCacheFormat();

	static void*	creator();
	static void		setPluginName( const MString& name );
	static MString	translatorName();

	MStatus			isValid();

    MStatus			open( const MString& fileName, FileAccessMode mode );
    void			close();

    MStatus			readHeader();
    MStatus			writeHeader( const MString& version, MTime& startTime, MTime& endTime );

    void			beginWriteChunk();
    void			endWriteChunk();
    MStatus			beginReadChunk();
    void			endReadChunk();

    MStatus			writeTime( MTime& time );
    MStatus			readTime( MTime& time );
    MStatus			findTime( MTime& time, MTime& foundTime );
    MStatus			readNextTime( MTime& foundTime );

    unsigned		readArraySize();

    MStatus			writeDoubleArray( const MDoubleArray& );
    MStatus			readDoubleArray( MDoubleArray&, unsigned size );
    MStatus			writeFloatArray( const MFloatArray& );
    MStatus			readFloatArray( MFloatArray&, unsigned size );
    MStatus			writeDoubleVectorArray( const MVectorArray& array );
    MStatus			readDoubleVectorArray( MVectorArray&, unsigned arraySize );
    MStatus			writeFloatVectorArray( const MFloatVectorArray& array );
    MStatus			readFloatVectorArray( MFloatVectorArray& array, unsigned arraySize );

    MStatus			writeChannelName( const MString& name );
    MStatus			findChannelName( const MString& name );
    MStatus			readChannelName( MString& name );

    MStatus			writeInt32( int );
    int				readInt32();
    MStatus			rewind();
 
	MString			extension();

protected:

	static MString	comment( const MString& text );
	static MString	quote( const MString& text );

	static MString	fExtension;
	static MString	fCacheFormatName;

private:
	void			startXmlBlock( string& t );
	void			endXmlBlock();
	void			writeXmlTagValue( string& tag, string value );
	void			writeXmlTagValue( string& tag, int value );
	bool			readXmlTagValue( string tag, MStringArray& value );
	bool			readXmlTagValueInChunk( string tag, MStringArray& values );
	void			readXmlTag( string& value );
	bool			findXmlStartTag( string& tag );
	bool			findXmlStartTagInChunk( string& tag );
	bool			findXmlEndTag( string& tag );
	void			writeXmlValue( string& value );
	void			writeXmlValue( double value );
	void			writeXmlValue( float value );
	void			writeXmlValue( int value );
		
	MString			fFileName;
	fstream			fFile;
	stack<string>	fXmlStack;
	FileAccessMode	fMode;

};

MString XmlCacheFormat::fExtension = "mc";			// For files on disk
MString XmlCacheFormat::fCacheFormatName = "xml";	// For presentation in GUI

inline MString XmlCacheFormat::translatorName()
{
	return fCacheFormatName;
}

void* XmlCacheFormat::creator()
{
	return new XmlCacheFormat();
}


// ****************************************
//
//  Helper functions

string XMLSTARTTAG( string x ) 
{
	return ( "<" + x + ">" );
}

string XMLENDTAG( string x ) 
{
	return ( "</" + x + ">" );
}

string cacheTag("awGeoCache");
string startTimeTag("startTime");
string endTimeTag("endTime");
string versionTag("version");
string timeTag("time");	
string sizeTag("size");
string intTag("integer32");
string doubleArrayTag("doubleArray");
string floatArrayTag("floatArray");
string doubleVectorArrayTag("doubleVectorArray");
string floatVectorArrayTag("floatVectorArray");
string channelTag("channel");	
string chunkTag("chunk");			

XmlCacheFormat::XmlCacheFormat()
{
}

XmlCacheFormat::~XmlCacheFormat()
{
    close();	
}

MStatus
XmlCacheFormat::open( const MString& fileName, FileAccessMode mode )
{
	bool rtn = true;

	assert((fileName.length() > 0));

	fFileName = fileName;
	
	if( mode == kWrite ) {
		fFile.open(fFileName.asChar(), ios::out);
	}
	else if (mode == kReadWrite) {
		fFile.open(fFileName.asChar(), ios::app);
	} else {
		fFile.open(fFileName.asChar(), ios::in);
	}

	if (!fFile.is_open()) {
		rtn = false;
	} else {
		if (mode == kRead) {
			rtn = readHeader();
		}
	}

	return rtn ? MS::kSuccess : MS::kFailure ;
}


MStatus
XmlCacheFormat::isValid()
{
	bool rtn = fFile.is_open();
	return rtn ? MS::kSuccess : MS::kFailure ;
}

MStatus
XmlCacheFormat::readHeader() 
{
	bool rtn = false;
	if (kWrite != fMode) {
		if( fFile.is_open() ) {
			string tag;
			readXmlTag( tag );

			if( tag == XMLSTARTTAG(cacheTag) ) 
			{
				MStringArray value;
				readXmlTagValue( versionTag, value );

				readXmlTagValue( startTimeTag, value );

				readXmlTagValue( endTimeTag, value );

				readXmlTag( tag ); 	// Should be header close tag, check
				if( tag != XMLENDTAG(cacheTag) )
				{
					// TODO - handle this error case
				}

				rtn = true;
			}
		}
	}

	return rtn ? MS::kSuccess : MS::kFailure ;
}

MStatus
XmlCacheFormat::rewind()
{
   	if( fFile.is_open() ) 
   	{
		close();
        	if(!open(fFileName, kRead ))
		{
			return MS::kFailure;
		}
		return MS::kSuccess;
	}
	else
	{
		return MS::kFailure;
	}
}

void XmlCacheFormat::close() 
{
    if( fFile.is_open() ) {
	fFile.close();
    }
}

MStatus XmlCacheFormat::writeInt32( int i ) 
{
        writeXmlTagValue(intTag, i);
	return MS::kSuccess;
}

int
XmlCacheFormat::readInt32()
{
	MStringArray value;
	readXmlTagValue(intTag, value);
	return atoi( value[0].asChar() );
}


MStatus
XmlCacheFormat::writeHeader( const MString& version, MTime& startTime, MTime& endTime )
{
	stringstream oss;

	startXmlBlock(cacheTag);
	string v(version.asChar());
	writeXmlTagValue(versionTag, v);

	oss << startTime;
	writeXmlTagValue(startTimeTag, oss.str() );

	oss.str("");
	oss.clear();
	oss << endTime;
	writeXmlTagValue(endTimeTag, oss.str() );

	endXmlBlock();
	return MS::kSuccess;
}


MStatus
XmlCacheFormat::readTime( MTime& time )
{
	MStringArray timeValue;
	readXmlTagValue(timeTag, timeValue);
	time.setValue( strtod( timeValue[0].asChar(), NULL ) );

	return MS::kSuccess;
}

MStatus
XmlCacheFormat::writeTime( MTime& time )
{
	stringstream oss;
	oss << time;
	writeXmlTagValue(timeTag, oss.str() );
	return MS::kSuccess;
}


MStatus
XmlCacheFormat::findChannelName( const MString& name )
//	
//  Given that the right time has already been found, find the name
//  of the channel we're trying to read.
//
{
	MStringArray value;
	while (readXmlTagValueInChunk(channelTag, value)) 
	{
		if( value.length() == 1 && value[0] == name )
		{
			return MS::kSuccess;
		}
	}

	return MS::kFailure;
}

MStatus
XmlCacheFormat::readChannelName( MString& name )
//	
//  Given that the right time has already been found, find the name
//  of the channel we're trying to read.
//
//  If no more channels exist, return false. Some callers rely on this false return 
//  value to terminate scanning for channels, thus it's not an error condition.
//
//  TODO: the current implementation assumes there are numChannels of data stored
//  for every time.  This assumption needs to be removed.
//
{
	MStringArray value;
	readXmlTagValueInChunk(channelTag, value);

	name = value[0];

	return name.length() == 0 ? MS::kFailure : MS::kSuccess;
}


MStatus
XmlCacheFormat::readNextTime( MTime& foundTime )
//	
// Read the next time based on the current read position.
//
{	
	MTime readAwTime(0.0, MTime::k6000FPS);
	bool ret = readTime(readAwTime);
	foundTime = readAwTime;

	return ret ? MS::kSuccess : MS::kFailure ;
}

MStatus
XmlCacheFormat::findTime( MTime& time, MTime& foundTime )
//	
// Find the biggest cached time, which is smaller or equal to 
// seekTime and return foundTime
//
{
	MTime timeTolerance(0.0, MTime::k6000FPS);
	MTime seekTime(time);
	MTime preTime( seekTime - timeTolerance );
	MTime postTime( seekTime + timeTolerance );

	bool fileRewound = false;
	while (1)
	{
		bool timeTagFound = beginReadChunk();
		if ( ! timeTagFound && !fileRewound )
		{
			if(!rewind())
			{
				return MS::kFailure;
			}
			fileRewound = true;
			timeTagFound = beginReadChunk();
		}
		if ( timeTagFound ) 
		{
			MTime rTime(0.0, MTime::k6000FPS);
			readTime(rTime);

			if(rTime >= preTime && rTime <= postTime)
			{
				foundTime = rTime;
				return MS::kSuccess;
			}
			if(rTime > postTime ) 
			{
				if (!fileRewound) 
				{
					if(!rewind())
					{
						return MS::kFailure;
					}
					fileRewound = true;
				} 
				else 
				{
					// Time could not be found
					//
					return MS::kFailure;
				}
			} 
			else 
			{
				fileRewound = true;
			}
		    endReadChunk();
		} 
		else
		{
		    // Not a valid disk cache file.
			break;
		}
	}

	return MS::kFailure;
}

MStatus
XmlCacheFormat::writeChannelName( const MString& name )
{
	string chan = name.asChar();
	writeXmlTagValue( channelTag, chan );
	return MS::kSuccess;
}


void XmlCacheFormat::beginWriteChunk()
{
	startXmlBlock(chunkTag);
}


void XmlCacheFormat::endWriteChunk()
{
	endXmlBlock();
}

MStatus XmlCacheFormat::beginReadChunk()
{
	return findXmlStartTag(chunkTag) ? MS::kSuccess : MS::kFailure;
}

void XmlCacheFormat::endReadChunk()
{
	findXmlEndTag(chunkTag);
}

MStatus
XmlCacheFormat::writeDoubleArray( const MDoubleArray& array )
{
	int size = array.length();
	assert(size != 0);

	writeXmlTagValue(sizeTag,size);

	startXmlBlock( doubleArrayTag );
	for(int i = 0; i < size; i++)
	{
	    writeXmlValue(array[i]);
	}
	endXmlBlock();
	return MS::kSuccess;
}

MStatus
XmlCacheFormat::writeFloatArray( const MFloatArray& array )
{
	int size = array.length();
	assert(size != 0);

	writeXmlTagValue(sizeTag,size);

	startXmlBlock( floatArrayTag );
	for(int i = 0; i < size; i++)
	{
	    writeXmlValue(array[i]);
	}
	endXmlBlock();
	return MS::kSuccess;
}

MStatus
XmlCacheFormat::writeDoubleVectorArray( const MVectorArray& array )
{
	int size = array.length();
	assert(size != 0);

	writeXmlTagValue(sizeTag,size);

	startXmlBlock( doubleVectorArrayTag );
	for(int i = 0; i < size; i++)
	{
	    writeXmlValue(array[i][0]);
	    writeXmlValue(array[i][1]);
	    writeXmlValue(array[i][2]);
	    string endl("\n");
	    writeXmlValue(endl);
	}
	endXmlBlock();
	return MS::kSuccess;
}


MStatus 
XmlCacheFormat::writeFloatVectorArray( const MFloatVectorArray& array )
{
	int size = array.length();
	assert(size != 0);

	writeXmlTagValue(sizeTag,size);

	startXmlBlock( floatVectorArrayTag );
	for(int i = 0; i < size; i++)
	{
	    writeXmlValue(array[i][0]);
	    writeXmlValue(array[i][1]);
	    writeXmlValue(array[i][2]);
	    string endl("\n");
	    writeXmlValue(endl);
	}
	endXmlBlock();
	return MS::kSuccess;
}


unsigned
XmlCacheFormat::readArraySize()
{
	MStringArray value;

	if (readXmlTagValue(sizeTag, value))
	{
		unsigned readSize = (unsigned) atoi( value[0].asChar() );
		return readSize;
	}

	return 0;
}

MStatus
XmlCacheFormat::readDoubleArray( MDoubleArray& array, unsigned arraySize )
{
	MStringArray value;
	readXmlTagValue(doubleArrayTag, value);

	assert( value.length() == arraySize );
	array.setLength( arraySize );
	for ( unsigned int i = 0; i < value.length(); i++ )
	{
		array[i] = strtod( value[i].asChar(), NULL );
	}
	
	return MS::kSuccess;
}

MStatus
XmlCacheFormat::readFloatArray( MFloatArray& array, unsigned arraySize )
{
	MStringArray value;
	readXmlTagValue(floatArrayTag, value);

	assert( value.length() == arraySize );
	array.setLength( arraySize );
	for ( unsigned int i = 0; i < value.length(); i++ )
	{
		array[i] = (float)strtod( value[i].asChar(), NULL );
	}
	
	return MS::kSuccess;
}

MStatus
XmlCacheFormat::readDoubleVectorArray( MVectorArray& array, unsigned arraySize )
{
	MStringArray value;
	if( !readXmlTagValue(doubleVectorArrayTag, value) )
	{
		return MS::kFailure;
	}

	assert( value.length() == arraySize * 3 );
	array.setLength( arraySize );
	for (unsigned i = 0; i < arraySize; i++ )
	{
		double v[3];
		v[0] = strtod( value[i*3].asChar(), NULL );
		v[1] = strtod( value[i*3+1].asChar(), NULL );
		v[2] = strtod( value[i*3+2].asChar(), NULL );

		array.set( v, i );
	}
	
	return MS::kSuccess;
}

MStatus 
XmlCacheFormat::readFloatVectorArray( MFloatVectorArray& array, unsigned arraySize )
{
	MStringArray value;
	readXmlTagValue(floatVectorArrayTag, value);

	assert( value.length() == arraySize * 3 );

	array.clear();
	array.setLength( arraySize );
	for (unsigned i = 0; i < arraySize; i++ )
	{
		float v[3];
		v[0] = (float)strtod( value[i*3].asChar(), NULL );
		v[1] = (float)strtod( value[i*3+1].asChar(), NULL );
		v[2] = (float)strtod( value[i*3+2].asChar(), NULL );

		array.set(v,i);
	}

	return MS::kSuccess;
}

MString
XmlCacheFormat::extension()
{
	return fExtension;
}

// ****************************************
//
//  Helper functions
//

void XmlCacheFormat::startXmlBlock( string& t )
{
	fXmlStack.push(t);
	fFile << "<" << t << ">\n";
}


void XmlCacheFormat::endXmlBlock()
{
	string block = fXmlStack.top();
	fFile << "</" << block << ">\n";
	fXmlStack.pop();
}

void XmlCacheFormat::writeXmlTagValue( string& tag, string value )
{
	for (unsigned int i = 0; i < fXmlStack.size(); i++ )
		fFile << "\t";

	fFile << "<" << tag << "> ";	// Extra space is important for reading
	fFile << value;
	fFile << " </" << tag << ">\n";	// Extra space is important for reading
}

void XmlCacheFormat::writeXmlTagValue( string& tag, int value )
{
	for (unsigned int i = 0; i < fXmlStack.size(); i++ )
		fFile << "\t";

	fFile << "<" << tag << "> ";	// Extra space is important for reading
	ostringstream oss;
	oss << value;
	fFile << oss.str();
	fFile << " </" << tag << ">\n";	// Extra space is important for reading
}

bool XmlCacheFormat::readXmlTagValue( string tag, MStringArray& values )
{
	string endTag = XMLENDTAG(tag);
	bool status = true;

	values.clear();

	// Yes this could be much much smarter
	if( findXmlStartTag(tag) )
	{
		string token;
		fFile >> token;
		while ( !fFile.eof() && token != endTag )
		{
			values.append( token.data() );
			fFile >> token;
		} 
	}
	else
	{
		status = false;
	}

	return status;
}


bool XmlCacheFormat::readXmlTagValueInChunk( string tag, MStringArray& values )
{
	string endTag = XMLENDTAG(tag);
	bool status = true;

	values.clear();

    // Find the tag in the currently read chunk.
	if( findXmlStartTagInChunk(tag) )
	{
		string token;
		fFile >> token;
        // Look for the values within the bounds of the tag.
		while ( !fFile.eof() && token != endTag )
		{
			values.append( token.data() );
			fFile >> token;
		} 
	}
	else
	{
		status = false;
	}

	return status;
}

void XmlCacheFormat::readXmlTag( string& value )
{
	value.clear();
        fFile >> value;
}

bool XmlCacheFormat::findXmlStartTag( string& tag )
{
	string tagRead;
	string tagExpected = XMLSTARTTAG(tag);

	fFile >> tagRead;
	// Keep looking all the way to EOF
	while(!fFile.eof() && tagRead != tagExpected) 
	{
		fFile >> tagRead;
	}
	if( tagRead != tagExpected )
	{
	}

	return ( tagRead == tagExpected );
}

bool XmlCacheFormat::findXmlStartTagInChunk( string& tag )
//
// Look for the given tag within the currently read chunk.
//
{
	string tagRead;
	string tagExpected = XMLSTARTTAG(tag);
	string tagEndChunk("</"+chunkTag+">");

    fFile >> tagRead;

    // Keep looking all the way to EOF
    while((!fFile.eof()) && (tagRead != tagExpected) && (tagRead != tagEndChunk)) 
    {
		fFile >> tagRead;
    }
	if( (tagRead != tagExpected) && (tagRead != tagEndChunk) )
	{
	}

	return ( tagRead == tagExpected );
}

bool XmlCacheFormat::findXmlEndTag(string& tag)
{
	string tagRead;
	string tagExpected("</"+tag+">");

	fFile >> tagRead;
	if( tagRead != tagExpected )
	{
	}

	return ( tagRead == tagExpected );
}

void XmlCacheFormat::writeXmlValue( string& value )
{
	fFile << value << " ";
}

void XmlCacheFormat::writeXmlValue( double value )
{
	fFile << value << " ";
}

void XmlCacheFormat::writeXmlValue( float value )
{
	fFile << value << " ";
}

void XmlCacheFormat::writeXmlValue( int value )
{
	fFile << value << " ";
}



// ****************************************

MStatus initializePlugin( MObject obj )
{
	MFnPlugin plugin( obj, PLUGIN_COMPANY, "1.0", "Any" );

	plugin.registerCacheFormat(
		XmlCacheFormat::translatorName(),
		XmlCacheFormat::creator
	);

	return MS::kSuccess;
}


MStatus uninitializePlugin( MObject obj )
{
	MFnPlugin plugin( obj );

	plugin.deregisterCacheFormat( XmlCacheFormat::translatorName() );

	return MS::kSuccess;
}
