/*
   Interface for connecting python based user-defined meta-models with
   LS-OPT.
*/


/*

   Livermore Software Technology Corporation (LSTC) holds the copyright
   for this code. LSTC hereby grants anyone the right to create and
   distribute derivative work based on this code under their own terms.
   
   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
   CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES
   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
   MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
   CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
   GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
   BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
   LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
   OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   POSSIBILITY OF SUCH DAMAGE.

*/
#ifdef __cplusplus
extern "C" {
#endif 

#include <Python.h>
#include "UserMetaModel.h"

//#include <stdio.h>

double RunPython(UserMetaModel* mm, PyObject *pArgs, PyObject *pFunc);
void InitPython(UserMetaModel* mm);
PyObject *pName, *pModule, *pFunc;
PyObject *pFuncBuild, *pFuncVal, *pFuncGrad;
int built = 0, userdefinedfuncgrad = 0;

/* Our initialization routine, called by LS-OPT. */
void UserMetaModel_Init(UserMetaModel* mm)
{
	/* This is a requirement of the protocol! */
	mm->size_user = sizeof(UserMetaModel);
	
	/* Present ourself to LS-OPT */
	mm->description = "Interface for Python-metamodels";
	/* Here, we can allocate memory that isn't really needed for persistant
     state.
     
	This memory must be freed in  UserMetaModel_CleanUp.
	*/
}

void UserMetaModel_CleanUp(UserMetaModel* mm)
{
  /* If we would have allocated memory in UserMetaModel_Init, here is where
     we would free it */
	if (mm->userstate) free(mm->userstate);
		printf("Cleaned");
}

int UserMetaModel_GetConstantCount(UserMetaModel* mm, int numpoints_good)
{
	/* As constants, we actually just use our points! */
	return (mm->numvar + 1) * numpoints_good;
}

void UserMetaModel_Build(UserMetaModel* mm, double ** x_case_good,
			 double * funcvals_good,int numpoints_good)
{
	int i,j;
	built=1;
	/*Conversion of the datatypes and arrangement as arguments for Python*/
	InitPython(mm);
	PyObject *coo,*coos, *vals, *pArgs;

	pArgs = PyTuple_New(2);
	coos = PyList_New(numpoints_good);
	vals = PyList_New(numpoints_good);
	
	//Arguments are saved into lists
	for (i=0; i<numpoints_good;i++) {
		coo = PyList_New(mm->numvar);
		for (j=0; j<mm->numvar;j++) {
			PyList_SET_ITEM(coo,j,PyFloat_FromDouble(x_case_good[i][j]));
		}
		PyList_SET_ITEM(coos,i,coo);
		PyList_SET_ITEM(vals,i,PyFloat_FromDouble(funcvals_good[i]));
	}
	PyTuple_SetItem(pArgs, 0, coos);
	PyTuple_SetItem(pArgs, 1, vals);

	double result = RunPython(mm,pArgs,pFuncBuild);
	//Saving the data
	int c=0;
	for (i=0; i<numpoints_good; i++) {
		for (j=0; j<mm->numvar; j++)
			mm->constants[c++] = x_case_good[i][j];
		mm->constants[c++] = funcvals_good[i];
	}
}

double UserMetaModel_FuncVal(UserMetaModel* mm, double *coords)
{
	if (built==0) {
		mm->error = UMM_ERROR;
		mm->error_message="Metamodel has not been built - no calculation of points possible.";
	}

	int i;
	int numvar = mm->numvar;
	int numpoints_good = mm->num_constants / (mm->numvar+1);
	PyObject *coo, *pArgs;

	//Collecting and converting the given coordinates as arguments in lists for Python
	pArgs = PyTuple_New(1);
	coo = PyList_New(numvar);
	for (i=0; i<numvar; i++) 
		PyList_SET_ITEM(coo,i,PyFloat_FromDouble(coords[i]));
	PyTuple_SetItem(pArgs, 0, coo);

	return RunPython(mm,pArgs,pFuncVal);
}

double UserMetaModel_FuncGrad(UserMetaModel* mm, int var, double *coords)
{
	if (userdefinedfuncgrad == 1) {
		//If funcgrad found it will be called with converted parameters.
		int i;
		int numvar = mm->numvar;
		int numpoints_good = mm->num_constants / (mm->numvar+1);
		PyObject *coo, *pArgs;
	
		//Collecting and converting the given coordinates and the direction of the derivative as arguments in lists for Python.
		pArgs = PyTuple_New(2);
		PyTuple_SetItem(pArgs,0,PyInt_FromLong(var));
		coo = PyList_New(numvar);
		for (i=0; i<numvar; i++) 
			PyList_SET_ITEM(coo,i,PyFloat_FromDouble(coords[i]));
		PyTuple_SetItem(pArgs, 0, coo);

		return RunPython(mm,pArgs,pFuncGrad);
	} else {
		//If no funcgrad was found a very simple derivative will be calculated. Function was given by the example of user-defined metamodels.
		double *mycoords;
		if (!mm->userstate)
			mm->userstate = malloc(sizeof(double) * mm->numvar);
		mycoords = (double*)mm->userstate;
		/* Quick and dirty numerical Eulerian differentiator */ 
		memcpy(mycoords,coords,sizeof(double) * mm->numvar);
		mycoords[var] += 0.0001;
		return (UserMetaModel_FuncVal(mm,mycoords) - UserMetaModel_FuncVal(mm,coords)) / 0.0001;
	}
}
/* Implements the Python functionality*/
void InitPython(UserMetaModel* mm) 
{
	// loading the Python script "pyInterface.py" and it functions "build()" and "funcval()"
	Py_Initialize();
	// Adding search paths for the pyInterface.py.
	PyRun_SimpleString("import sys");
	PyRun_SimpleString("sys.path.append('..')"); 
	PyRun_SimpleString("sys.path.append('../../metamodel')");
	PyRun_SimpleString("print \"Calling PyInterface\"");
	pName = PyString_FromString("pyInterface");
	pModule = PyImport_Import(pName);

	Py_DECREF(pName);

	//Importing functions build(), funcval() and funcgrad()
	if (pModule != NULL) {
	
		pFuncBuild = PyObject_GetAttrString(pModule, "build");
		if (pFuncBuild == NULL) {
			PyErr_Print();
			fprintf(stderr, "Failed to load function \"build()\"\n");
			mm->error = UMM_ERROR;
			mm->error_message = "Failed to load function \"build()\"";
		}
		pFuncVal = PyObject_GetAttrString(pModule, "funcval");
		if (pFuncVal == NULL) {
			PyErr_Print();
			fprintf(stderr, "Failed to load function \"funcval()\"\n");
			mm->error = UMM_ERROR;
			mm->error_message = "Failed to load function \"funcval()\"";
		}
		pFuncGrad = PyObject_GetAttrString(pModule, "funcgrad");
		if (pFuncGrad == NULL) {
			PyErr_Print();
			fprintf(stderr, "Failed to load function \"funcgrad()\", using predefined function.\n");
			userdefinedfuncgrad = 0;
		} else {
			userdefinedfuncgrad = 1;
		}
	}
	
	else {
		PyErr_Print();
		fprintf(stderr, "Failed to load module \"pyInterface.py\"\n");
		mm->error = UMM_ERROR;
		mm->error_message = "Failed to load module \"pyInterface.py\"";
	}
}

/* Running the loaded python functions with arguments*/
double RunPython(UserMetaModel* mm,PyObject *pArgs,PyObject *pFunc)
{
	PyObject *pValue;
	if (pModule != NULL) {
		if (pFunc && PyCallable_Check(pFunc)) {
			pValue = PyObject_CallObject(pFunc, pArgs);
			Py_DECREF(pArgs);
		}
		else {
			// Function is not found or not callable
			if (PyErr_Occurred()) PyErr_Print();
			fprintf(stderr, "Cannot find function\n");
			mm->error = UMM_ERROR;
			mm->error_message = "Python function is not loaded. Critical error.";
			return 0;
		}
	}
    else {
		//Module is not found
		if (PyErr_Occurred()) PyErr_Print();
		fprintf(stderr, "Cannot find module\n");
		mm->error = UMM_ERROR;
		mm->error_message = "Python module is not loaded. Critical error.";
		return 0;
    }

	//converting and checking the result. 
	double value;
	if ((pValue != NULL) && (pFunc != pFuncBuild)) {
		if (PyFloat_Check(pValue)) {
			//If it's a float everything's correct
			value = PyFloat_AsDouble(pValue);
		}else if(PyLong_Check(pValue)) {
			//If it's a double the number will be converted and a warning will be displayed
			value = PyLong_AsDouble(pValue);
			mm->error = UMM_WARNING;
			mm->error_message = "Result has been given as LONG. Please return float.";
			if (value == -1.0) {
				//Overflow error when PyLong_AsDouble is -1.0
				mm->error = UMM_WARNING;
				mm->error_message = "PyLong_AsDouble returned -1.0. May be overflow error. Please return float.";
			}
		}else if(PyInt_Check(pValue)) {
			//If it's a int the number will be converted (via Python and C++-cast) and a warning will be displayed
			value = (double) PyInt_AsLong(pValue);
			mm->error = UMM_WARNING;
			mm->error_message = "Result has been given as INT. Please return float.";
		}else{
			//Not a float, double or int. Error will be displayed.
			mm->error = UMM_ERROR;
			mm->error_message = "Result has not been given as numeric. Please return float.";
			value = 0;
		}
		Py_DECREF(pValue);
	} else if ((pValue != NULL) && (pFunc != pFuncBuild)) {
		//In case of missing return values. Happens when pyInterface.py is not correct
		mm->error = UMM_ERROR;
		mm->error_message = "Result was expected but not returned. Critical error.";
		value = 0;
	} else {
		//Just in the case of funcbuild running correctly
		value = 1;
	}
	return value;
}
