#ifndef MXPARSE_H
#define MXPARSE_H


#include "base/libconfig/lib/libconfig.h++"
#include <itpp/itbase.h>
#include <itpp/itmex.h>
#include <stdlib.h>


using namespace itpp;
using namespace std;
using namespace libconfig;

//! Class for writing Settings into Matlab's mxArray
class UImxArray : public Config {
public:
	//! Build an instance of Config with fields filled from the given \a mxarray
	UImxArray ( const mxArray *mxarray ) : Config() {
		Setting & setting = this->getRoot(); //setting is a group
		if ( !mxIsStruct ( mxarray ) ) {
			mexErrMsgTxt ( "Given mxArray is not a struct." );
		};

		//mexCallMATLAB(0, NULL, 1, (mxArray **) &mxarray, "dump");
		fillGroup ( setting, mxarray );
		setAutoConvert ( true );
	}
	UImxArray() : Config() {
		setAutoConvert ( true );
	}
	//! Add libconfig's \c list to the structure
	void addList ( const mxArray *mxarray, const char* name ) {
		Setting & setting = this->getRoot(); //setting is a group
		Setting & child = setting.add ( name, Setting::TypeList );
		fillList ( child, mxarray );
	}
	//! Add libconfig's \c group to the structure
	void addGroup ( const mxArray *mxarray, const char* name ) {
		Setting & setting = this->getRoot(); //setting is a group
		Setting & child = setting.add ( name, Setting::TypeGroup );
		fillGroup ( child, mxarray );
	}
	//! Operator for more convenient access to this Config
	operator Setting&() {
		return getRoot();
	}

private:
	void storeNumeric ( Setting &setting, const mxArray *value, string key = "" ) {
		//TODO: integer matrices
		if ( !mxIsNumeric ( value ) ) {
			mexErrMsgTxt ( "Given mxArray is not numeric." );
		};
		//treat empty matrices independently
		mat val;
		if ( mxGetM ( value ) > 0 && mxGetN(value)>0) {
			val = mxArray2mat ( value );
		}
		if ( ( val.rows() == 1 ) && ( val.cols() == 1 ) ) {
			Setting &child = ( key == "" ) ? setting.add ( Setting::TypeFloat )
			                 : setting.add ( key, Setting::TypeFloat );
			child = val ( 0, 0 );
		} else {
			Setting &child = ( key == "" ) ? setting.add ( Setting::TypeList )
			                 : setting.add ( key, Setting::TypeList );
			Setting &label = child.add ( Setting::TypeString );
			label = "matrix";
			Setting &rows = child.add ( Setting::TypeInt );
			Setting &cols = child.add ( Setting::TypeInt );
			Setting &elements = child.add ( Setting::TypeArray );
			cols = val.cols();
			rows = val.rows();
			for ( int i = 0; i < val.rows(); i++ ) {
				for ( int j = 0; j < val.cols(); j++ ) {
					Setting &el = elements.add ( Setting::TypeFloat );
					el = val ( i, j );
				}
			}
		}
	}

	void fillGroup ( Setting &setting, const mxArray *mxarray ) {
		if ( !mxIsStruct ( mxarray ) ) {
			mexErrMsgTxt ( "Given mxArray is not a struct." );
		};
		for ( int i = 0; i < mxGetNumberOfFields ( mxarray ); i++ ) {
			const char *key = mxGetFieldNameByNumber ( mxarray, i );
			mxArray *value = mxGetField ( mxarray, 0, key );
			if ( mxIsChar ( value ) ) {
				Setting &child = setting.add ( key, Setting::TypeString );
				child = mxArray2string ( value );
			}
			if ( mxIsLogical ( value ) ) {
				Setting &child = setting.add ( key, Setting::TypeBoolean );
				child = ( bool ) mxArray2bin ( value );
			}
			if ( mxIsStruct ( value ) ) {
				Setting &child = setting.add ( key, Setting::TypeGroup );
				fillGroup ( child, value );
			}
			if ( mxIsCell ( value ) ) {
				Setting &child = setting.add ( key, Setting::TypeList );
				fillList ( child, value );
			}
			if ( mxIsNumeric ( value ) ) {
				storeNumeric ( setting, value, ( string ) key );
			}
		}
	}

	void fillList ( Setting &setting, const mxArray *mxarray ) {
		if ( !mxIsCell ( mxarray ) ) {
			mexErrMsgTxt ( "Given mxArray is not a cell." );
		};
		for ( unsigned int i = 0; i < ( unsigned int ) mxGetNumberOfElements ( mxarray ); i++ ) {
			mxArray *value = mxGetCell ( mxarray, i );
			if ( mxIsChar ( value ) ) {
				Setting &child = setting.add ( Setting::TypeString );
				child = mxArray2string ( value );
			}
			if ( mxIsLogical ( value ) ) {
				Setting &child = setting.add ( Setting::TypeBoolean );
				child = ( bool ) mxArray2bin ( value );
			}
			if ( mxIsStruct ( value ) ) {
				Setting &child = setting.add ( Setting::TypeGroup );
				fillGroup ( child, value );
			}
			if ( mxIsCell ( value ) ) {
				Setting &child = setting.add ( Setting::TypeList );
				fillList ( child, value );
			}
			if ( mxIsNumeric ( value ) ) {
				storeNumeric ( setting, value );
			}
		}
	}

public:
	//! Convert existing Setting to Matlab arrays
	static mxArray * create_mxArray( const Setting &setting )
	{
		return group2mxstruct ( setting );
	}

	//! Convert existing Setting to Matlab arrays
	mxArray * create_mxArray(  )
	{
		return group2mxstruct ( *this );
	}

private:
	//! Convert libconfig's array to Matlab vector
	static mxArray* array2mxvector ( const Setting &setting )  {
		if ( !setting.isArray() ) mexErrMsgTxt ( "Given setting is not an array" );
		mxArray *result = mxCreateDoubleMatrix ( 1, setting.getLength(), mxREAL );
		double *elements = mxGetPr ( result );
		for ( int i = 0; i < setting.getLength(); i++ ) {
			if ( setting.getType() == Setting::TypeInt ) { //TODO: tady je chyba -- zaporna cisla nejsou TypeInt
				elements[i] = ( int ) setting[i];
			} else {
				elements[i] =  setting[i];
			}
		}
		return result;
	}

	//! Convert libconfig's array to Matlab matrix
	static mxArray* list2mxmatrix ( const Setting &setting )  {
		if ( !setting.isList() || ( strcmp ( "matrix", setting[0] ) != 0 ) )
			mexErrMsgTxt ( "Given setting is not a matrix" );
		int rows = setting[1];
		int cols = setting[2];
		if ( setting[3].getLength() != rows*cols )
			mexErrMsgTxt ( "Matrix elements do not fit to rows*cols" );
		double *elements = new double[rows*cols];
		for ( int i = 0; i < rows*cols; i++ ) {
			elements[i] = setting[3][i];
		}
		mat &m = * ( new mat ( elements, rows, cols ) );
		mxArray *result = mxCreateDoubleMatrix ( rows, cols, mxREAL );
		mat2mxArray ( m, result );
		delete &m;
		delete [] elements;
		return result;
	}

	//! Convert libconfig's gourp to Matlab structure
	static mxArray* group2mxstruct ( const Setting &setting ) {
		if ( !setting.isGroup() ) mexErrMsgTxt ( "Given setting is not a group." );
		const char ** keys = new const char*[setting.getLength() ];
		for ( int i = 0; i < setting.getLength(); i++ ) {
			keys[i] = setting[i].getName();
		}
		mxArray *result = mxCreateStructMatrix ( 1, 1, setting.getLength(), keys );
		delete keys;
		for ( int i = 0; i < setting.getLength(); i++ ) {
			Setting &value = setting[i];
			mxArray *old = mxGetFieldByNumber ( result, 0, i );
			if ( old ) mxDestroyArray ( old );
			switch ( value.getType() ) {
			case Setting::TypeString:
				mxSetFieldByNumber ( result, 0, i, mxCreateString ( value ) );
				break;
			case Setting::TypeBoolean:
				mxSetFieldByNumber ( result, 0, i, mxCreateLogicalScalar ( value ) );
				break;
			case Setting::TypeGroup:
				mxSetFieldByNumber ( result, 0, i, group2mxstruct ( value ) );
				break;
			case Setting::TypeList:
				mxSetFieldByNumber ( result, 0, i, list2mxcell ( value ) );
				break;
			case Setting::TypeArray:
				mxSetFieldByNumber ( result, 0, i, array2mxvector ( value ) );
				break;
			case Setting::TypeInt:
			case Setting::TypeInt64:
				mxSetFieldByNumber ( result, 0, i, mxCreateDoubleScalar ( ( int ) value ) );
				break;
			case Setting::TypeFloat:
				mxSetFieldByNumber ( result, 0, i, mxCreateDoubleScalar ( value ) );
				break;
			default:
				//this should never happen
				mexErrMsgTxt ( "Unknown type of a setting." );
			}
		}
		return result;

	}
	//! Convert libconfig's list  to Matlab cell
	static mxArray* list2mxcell ( const Setting &setting )  {
		if ( !setting.isList() ) mexErrMsgTxt ( "Given setting is not a list." );
		if ( setting.getLength() == 0 ) {
			mxArray *result = mxCreateCellMatrix ( 1, 0 );
			return result;
		}

		if ( ( setting[0].getType() == Setting::TypeString ) ) {
			string s = ( setting[0] );
			if ( s == "matrix" ) {
				return list2mxmatrix ( setting );
			}
		}
		mxArray *result = mxCreateCellMatrix ( 1, setting.getLength() );
		for ( int i = 0; i < setting.getLength(); i++ ) {
			Setting &value = setting[i];
			mxArray *old = mxGetCell ( result, i );
			if ( old ) mxDestroyArray ( old );
			switch ( value.getType() ) {
			case Setting::TypeString:
				mxSetCell ( result, i, mxCreateString ( value ) );
				break;
			case Setting::TypeBoolean:
				mxSetCell ( result, i, mxCreateLogicalScalar ( value ) );
				break;
			case Setting::TypeGroup:
				mxSetCell ( result, i, group2mxstruct ( value ) );
				break;
			case Setting::TypeList:
				mxSetCell ( result, i, list2mxcell ( value ) );
				break;
			case Setting::TypeArray:
				mxSetCell ( result, i, array2mxvector ( value ) );
				break;
			case Setting::TypeInt:
			case Setting::TypeInt64:
				mxSetCell ( result, i, mxCreateDoubleScalar ( ( int ) value ) );
				break;
			case Setting::TypeFloat:
				mxSetCell ( result, i, mxCreateDoubleScalar ( value ) );
				break;
			default:
				//this should never happen
				mexErrMsgTxt ( "Unknown type of a setting." );
			}
		}
		return result;
	}
};

#endif //MXPARSE_H
