#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 );
            } else {
                if ( mxIsLogical ( value ) ) {
                    Setting &child = setting.add ( key, Setting::TypeBoolean );
                    child = ( bool ) mxArray2bin ( value );
                } else {
                    if ( mxIsStruct ( value ) ) {
                        Setting &child = setting.add ( key, Setting::TypeGroup );
                        fillGroup ( child, value );
                    } else {
                        if ( mxIsCell ( value ) ) {
                            Setting &child = setting.add ( key, Setting::TypeList );
                            fillList ( child, value );
                        } else {
                            if ( mxIsNumeric ( value ) ) {
                                storeNumeric ( setting, value, ( string ) key );
                            } else {
                                // it is neither of above - treat it as an address
                                // if (mxIsObject(value)){ <== should be tested but it is broken in matlab...

                                Setting &child = setting.add(key, Setting::TypeInt64);
                                child = (long long)value;
                            }
                        }
                    }
                }
            }
        }
    }

    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
