/* -*- mia-c++  -*-
 *
 * This file is part of pymia - python bindings for MIA
 * Copyright (c) Leipzig, Madrid 1999-2014 Gert Wollny
 *
 * pymia is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with pymia; if not, see <http://www.gnu.org/licenses/>.
 *
 */

#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION

#include <Python.h>
#include <numpy/arrayobject.h>
#include <mia/core/msgstream.hh>
#include <mia/core/errormacro.hh>
#include <mia/2d/image.hh>
#include <mia/3d/image.hh>
#include <miaconfig.h>

NS_MIA_BEGIN

using std::unique_ptr; 
using std::runtime_error; 
using std::invalid_argument; 

/// \cond INTERNAL 

/**
   This is the placeholder temnplate of the creation of an image from a numpy array. 
   It needs to be specified for the actual image type. 
   \tparam in input pixel type 
   \tparam out output pixel type 
   \tparam Image the image type 
*/

template <typename in, typename out, template  <class> class Image>
struct get_image {
	static  Image<out> apply(PyArrayObject *input) {
		static_assert(sizeof(in) == 0, "If you end up here, you need to add a new instanciation of get_image structure"); 
	}
}; 


/**
   This is the line copy if the input and output data is of the same size and not boolean 
   (Boolean is special in the STL vector implementation) 
   \tparam Image the image type 
   \tparam the pixel type 
*/
template <template  <class> class Image, typename T> 
struct __dispatch_copy_line {
	static void apply(typename Image<T>::iterator out, char *data, int size) {
		memcpy(&out[0], data, size);
	}
}; 

/**
   This is the line copy if the data represents boolean 
   \tparam Image the image type 
   \tparam the pixel type 
*/

template <template  <class> class Image> 
struct __dispatch_copy_line<Image, bool> {
	static void apply(typename Image<bool>::iterator out, char *data, int size) {
		copy(data, data+size, out); 
	}
}; 

/**
   This structure is the template specialization of get_image for 2D images.
   \tparam in input pixel type 
   \tparam out output pixel type 
 */
template <typename in, typename out>
struct get_image<in, out, T2DImage > {
	static typename T2DImage<out>::Pointer apply(PyArrayObject *input) {
		TRACE_FUNCTION; 
		typedef typename T2DImage<out>::dimsize_type Dimsize;
		
		const npy_intp *dimensions = PyArray_DIMS(input); 
		Dimsize size(dimensions[1], dimensions[0]);
		T2DImage<out> *result = new T2DImage<out>(size); 
		typename T2DImage<out>::Pointer presult(result); 
	
		cvdebug() << "Create mia image of size " << size 
			  << " type " << __type_descr<out>::value 
			  << "\n"; 

		/////////////////////////////////////////////////////////////////////////////////////////
		// this code is taken from the on-line example about iterators and adapted to my needs
		// http://docs.scipy.org/doc/numpy/reference/c-api.iterator.html#simple-iteration-example
		// 
		auto iter = NpyIter_New(input, NPY_ITER_READONLY|
					NPY_ITER_EXTERNAL_LOOP|
					NPY_ITER_REFS_OK,NPY_KEEPORDER,  NPY_NO_CASTING, 
					NULL); 
		unique_ptr<NpyIter, int (*)(NpyIter *p)> piter(iter, NpyIter_Deallocate);
		if (!iter) 
			throw runtime_error("Unable create iterater for input array"); 
		
		auto iternext = NpyIter_GetIterNext(iter, NULL);
		if (iternext == NULL)
			throw runtime_error("Unable to iterate over input array"); 
		
		auto innerstride = NpyIter_GetInnerStrideArray(iter)[0];
		auto itemsize = NpyIter_GetDescrArray(iter)[0]->elsize;
		auto innersizeptr = NpyIter_GetInnerLoopSizePtr(iter);
		auto dataptrarray = NpyIter_GetDataPtrArray(iter);

		if (innerstride == sizeof(out)) {
			// fast copy if stride is equal to target item size 
			size_t y = 0; 
			do {
				__dispatch_copy_line<T2DImage, out>::apply(result->begin_at(0,y), dataptrarray[0], itemsize * (*innersizeptr));
				++y; 
			} while (iternext(iter));
		} else {
			auto ir = result->begin(); 
			do {
				npy_intp size = *innersizeptr;
				char *src = dataptrarray[0];
				for(npy_intp i = 0; i < size; i++, src += innerstride, ++ir) {
					*ir = *(in*)src;
				}
			} while (iternext(iter));
		}
		//
		// End of copying
		////////////////////////////////////////////////////////////////
		return presult; 
	}
}; 


/**
   This structure is the template specialization of get_image for 3D images.
   \tparam in input pixel type 
   \tparam out output pixel type 
 */
template <typename in, typename out>
struct get_image<in, out, T3DImage > {
	static typename T3DImage<out>::Pointer apply(PyArrayObject *input) {
		TRACE_FUNCTION; 
		typedef typename T3DImage<out>::dimsize_type Dimsize;
		
		const npy_intp *dimensions = PyArray_DIMS(input); 
		Dimsize size(dimensions[2], dimensions[1], dimensions[0]);

		cvdebug() << "Create mia image of size " << size 
			  << " and type " << __type_descr<out>::value << "\n"; 

		T3DImage<out> *result = new T3DImage<out>(size); 
		typename T3DImage<out>::Pointer presult(result); 
	
		/////////////////////////////////////////////////////////////////////////////////////////
		// this code is taken from the on-line example about iterators and adapted to my needs
		// http://docs.scipy.org/doc/numpy/reference/c-api.iterator.html#simple-iteration-example
		// 
		auto iter = NpyIter_New(input, NPY_ITER_READONLY|
					NPY_ITER_EXTERNAL_LOOP|
					NPY_ITER_REFS_OK, 
					NPY_KEEPORDER, 
					NPY_NO_CASTING, 
					NULL); 
		unique_ptr<NpyIter, int (*)(NpyIter *p)> piter(iter, NpyIter_Deallocate);
		if (!iter) 
			throw runtime_error("Unable create iterater for input array"); 
		
		auto iternext = NpyIter_GetIterNext(iter, NULL);
		if (iternext == NULL)
			throw runtime_error("Unable to iterate over input array"); 
		
		auto innerstride = NpyIter_GetInnerStrideArray(iter)[0];
		auto itemsize = NpyIter_GetDescrArray(iter)[0]->elsize;
		auto innersizeptr = NpyIter_GetInnerLoopSizePtr(iter);
		auto dataptrarray = NpyIter_GetDataPtrArray(iter);
		
		if (innerstride == sizeof(out)) {
			// fast copy if stride is equal to item size 
			size_t z = 0; 
			size_t y = 0; 

			do {
				__dispatch_copy_line<T3DImage, out>::apply(result->begin_at(0,y,z), dataptrarray[0], itemsize * (*innersizeptr));
				++y; 
				if ( y >= size.y ) 
					++z; 
			} while (iternext(iter));
		} else {
			auto ir = result->begin(); 
			do {
				npy_intp size = *innersizeptr;
				char *src = dataptrarray[0];
				for(npy_intp i = 0; i < size; i++, src += innerstride, ++ir) {
					*ir = *(in*)src;
				}
			} while (iternext(iter));
		}
		//
		// End of copying
		////////////////////////////////////////////////////////////////
		return presult; 
	}
}; 

/**
   \cond internal 
   Provide a mapping from C++ types to the numpy type constants 
*/

template <typename T> 
struct __mia_pixel_type_numarray_id {
	static const int value;
	static const char *name; 
}; 

template <typename T> 
const char *__mia_pixel_type_numarray_id<T>::name = "NPY_USERDEF"; 

template <typename T> 
const int __mia_pixel_type_numarray_id<T>::value = NPY_USERDEF; 


#define SPECIALICE_ID(type, VALUE)		\
template <>					\
struct __mia_pixel_type_numarray_id<type> {	\
	static const int value;		\
	static const char *name;		\
};						\
						\
					\
const char *__mia_pixel_type_numarray_id<type>::name = #VALUE; \
const int __mia_pixel_type_numarray_id<type>::value = VALUE;

SPECIALICE_ID(bool, NPY_BOOL);
SPECIALICE_ID(signed char, NPY_BYTE);
SPECIALICE_ID(unsigned char, NPY_UBYTE); 
SPECIALICE_ID(signed short, NPY_SHORT); 
SPECIALICE_ID(unsigned short, NPY_USHORT); 
SPECIALICE_ID(signed int, NPY_INT); 
SPECIALICE_ID(unsigned int, NPY_UINT); 
SPECIALICE_ID(signed long, NPY_LONG); 
SPECIALICE_ID(unsigned long, NPY_ULONG); 
SPECIALICE_ID(float, NPY_FLOAT); 
SPECIALICE_ID(double, NPY_DOUBLE); 



/**
   The MIA filter object to convert a 2D or 3D image to a PyArrayObject 
   
   
*/
struct FConvertToPyArray: public TFilter<PyArrayObject*> {
	template <typename T> 
	PyArrayObject * operator () (const T2DImage<T>& image) const; 

	template <typename T> 
	PyArrayObject * operator () (const T3DImage<T>& image) const; 

};


template <template <class> class Image, typename T> 
struct __dispatch_py_array_copy {
	static void apply(char *output, const Image<T>& image)	{
		memcpy(output, &image.begin()[0], image.size() * sizeof(T));
	}
};

template <template <class> class Image> 
struct __dispatch_py_array_copy<Image, bool> {
	static void apply(char *output, const Image<bool>& image)	{
		copy(image.begin(), image.end(), output); 
	}
}; 



template <typename T> 
PyArrayObject * FConvertToPyArray::operator () (const T2DImage<T>& image) const
{
	TRACE_FUNCTION; 
	npy_intp dims[2]; 
	dims[1] = image.get_size().x; 
	dims[0] = image.get_size().y;
	cvdebug() << "Create array of size " << image.get_size() << " numpy type " << __mia_pixel_type_numarray_id<T>::name <<"\n"; 
	PyArrayObject* out_array = (PyArrayObject*)(PyArray_SimpleNew(2, dims, __mia_pixel_type_numarray_id<T>::value)); 
	if (!out_array) {
		throw create_exception<runtime_error>("Unable to create output array of type '", 
						__mia_pixel_type_numarray_id<T>::value, "' and size ", image.get_size());
	}
	__dispatch_py_array_copy<T2DImage, T>::apply(PyArray_BYTES(out_array), image); 
	return out_array; 
}

template <typename T> 
PyArrayObject * FConvertToPyArray::operator () (const T3DImage<T>& image) const
{
	TRACE_FUNCTION; 
	npy_intp dims[3]; 
	dims[2] = image.get_size().x; 
	dims[1] = image.get_size().y;
	dims[0] = image.get_size().z;
	cvdebug() << "Create array of size " << image.get_size() 
		  << " numpy type " << __mia_pixel_type_numarray_id<T>::name << "("
		  <<__mia_pixel_type_numarray_id<T>::value<<")\n"; 
	
	PyArrayObject* out_array = (PyArrayObject*)(PyArray_SimpleNew(3, dims, __mia_pixel_type_numarray_id<T>::value)); 
	if (!out_array) {
		throw runtime_error("Unable to create output array"); 
	}
	__dispatch_py_array_copy<T3DImage, T>::apply(PyArray_BYTES(out_array), image); 
	return out_array; 
}


/// \endcond 

/**
   This template function creates a MIA image of the given template type from a PyArrayObject 
   (numpy.array) Currently, attributes are not kept. 

   \tparam Output image type, must be given explicitely  
   \param input input python array
   \remark currently, on 64 bit machines, the long type (which is the standard int type in 
   Python will be copied to int. This may clobber the data. 
   
 */

template <template <class> class Image>
typename Image<int>::Pointer mia_image_from_pyarray(PyArrayObject *input)
{
	TRACE_FUNCTION; 
	cvdebug() << "Get image numpy type " << PyArray_DESCR(input)->type_num 
		  << "and is " 
		  <<  ( PyArray_IS_C_CONTIGUOUS(input) ? " c-array " : " fortran array" )
		  <<"\n"; 
	switch (PyArray_DESCR(input)->type_num) {
	case NPY_BOOL:   return get_image<signed char, bool, Image>::apply(input);
	case NPY_BYTE:   return get_image<signed char, signed char, Image>::apply(input);
	case NPY_UBYTE:  return get_image<unsigned char, unsigned char, Image>::apply(input);
	case NPY_SHORT:  return get_image<signed short, signed short, Image>::apply(input);
	case NPY_USHORT: return get_image<unsigned short, unsigned short, Image>::apply(input);
	case NPY_INT:    return get_image<signed int, signed int, Image>::apply(input);
	case NPY_UINT:	 return get_image<unsigned int, unsigned int, Image>::apply(input);
#ifdef LONG_64BIT
	case NPY_LONG:   return get_image<signed long, signed long, Image>::apply(input);
	case NPY_ULONG:  return get_image<unsigned long, unsigned long, Image>::apply(input);
#endif 
	case NPY_FLOAT:  return get_image<float, float, Image>::apply(input); 
	case NPY_DOUBLE: return get_image<double, double, Image>::apply(input); 
	default:
		throw create_exception<invalid_argument>("mia doesn't support images of type  ", 
							 PyArray_DESCR(input)->type_num,  
							 ", If this is int64 then you are probably on a 32 bit platform.");
	}
}

/**
   Convert a MIA image to a python PyArrayObject (numpy.array)
   \tparam image 

*/


template <typename Image>
PyArrayObject * mia_pyarray_from_image(const Image& image)
{
	TRACE_FUNCTION; 
	FConvertToPyArray convert; 
	cvdebug() << "Image pixel type = " << image.get_pixel_type() << "\n"; 
	return mia::filter(convert, image); 
}

#define PYTHON_MIA_CALL_self_args(NAME) \
	static PyObject *NAME##call(PyObject *self, PyObject *args);	\
	static PyObject *NAME(PyObject *self, PyObject *args)		\
	{								\
		std::ostringstream msg;					\
		try {							\
			return NAME##call(self, args);			\
		}							\
		catch (std::runtime_error& x) {				\
			msg << "mia runtime error:'" << x.what() << "'"; \
		}							\
		catch (std::invalid_argument& x) {			\
			msg << "mia invalid argument:'" << x.what() << "'"; \
		}							\
		catch (std::exception& x) {				\
			msg << "mia exception: '" << x.what() << "'";	\
		}							\
		catch (...) {						\
			msg << "mia: unknown error";			\
		}							\
		PyErr_SetString(MiaError, msg.str().c_str());		\
		return NULL;						\
	}								\
	static PyObject *NAME##call(PyObject */*self*/, PyObject *args)

#define PYTHON_MIA_CALL_self_args_kwdict(NAME) \
	static PyObject *NAME##call(PyObject *self, PyObject *args, PyObject *keywds); \
	static PyObject *NAME(PyObject *self, PyObject *args, PyObject *keywds)	\
	{								\
		std::ostringstream msg;					\
		try {							\
			return NAME##call(self, args, keywds);		\
		}							\
		catch (std::runtime_error& x) {				\
			msg << "mia runtime error:'" << x.what() << "'"; \
		}							\
		catch (std::invalid_argument& x) {			\
			msg << "mia invalid argument:'" << x.what() << "'"; \
		}							\
		catch (std::exception& x) {				\
			msg << "mia exception: '" << x.what() << "'";	\
		}							\
		catch (...) {						\
			msg << "mia: unknown error";			\
		}							\
		PyErr_SetString(MiaError, msg.str().c_str());		\
		return NULL;						\
	}								\
	static PyObject *NAME##call(PyObject */*self*/, PyObject *args, PyObject *kwdict)



	
NS_MIA_END
