#ifndef OPENCV2X_PYTHON_WRAPPERS
#define OPENCV2X_PYTHON_WRAPPERS

#include "opencv2/core/core.hpp"

namespace cv
{

#define ERRWRAP2(expr) \
try \
{ \
    expr; \
} \
catch (const cv::Exception &e) \
{ \
    PyErr_SetString(opencv_error, e.what()); \
    return 0; \
}

static size_t REFCOUNT_OFFSET = (size_t)&(((PyObject*)0)->ob_refcnt) +
    (0x12345678 != *(const size_t*)"\x78\x56\x34\x12\0\0\0\0\0")*sizeof(int);

static inline PyObject* pyObjectFromRefcount(const int* refcount)
{
    return (PyObject*)((size_t)refcount - REFCOUNT_OFFSET);
}

static inline int* refcountFromPyObject(const PyObject* obj)
{
    return (int*)((size_t)obj + REFCOUNT_OFFSET);
}

class NumpyAllocator : public MatAllocator
{
public:
    NumpyAllocator() {}
    ~NumpyAllocator() {}
    
    void allocate(int dims, const int* sizes, int type, int*& refcount,
                  uchar*& datastart, uchar*& data, size_t* step)
    {
        static int ncalls = 0;
        printf("NumpyAllocator::allocate: %d\n", ncalls++);
        
        int depth = CV_MAT_DEPTH(type);
        int cn = CV_MAT_CN(type);
        const int f = (int)(sizeof(size_t)/8);
        int typenum = depth == CV_8U ? NPY_UBYTE : depth == CV_8S ? NPY_BYTE :
                      depth == CV_16U ? NPY_USHORT : depth == CV_16S ? NPY_SHORT :
                      depth == CV_32S ? NPY_INT : depth == CV_32F ? NPY_FLOAT :
                      depth == CV_64F ? NPY_DOUBLE : f*NPY_ULONGLONG + (f^1)*NPY_UINT;
        int i;
        npy_intp _sizes[CV_MAX_DIM+1];
        for( i = 0; i < dims; i++ )
            _sizes[i] = sizes[i];
        if( cn > 1 )
        {
            if( _sizes[dims-1] == 1 )
                _sizes[dims-1] = cn;
            else
                _sizes[dims++] = cn;
        }
        PyObject* o = PyArray_SimpleNew(dims, _sizes, typenum);
        if(!o)
            CV_Error_(CV_StsError, ("The numpy array of typenum=%d, ndims=%d can not be created", typenum, dims));
        refcount = refcountFromPyObject(o);
        npy_intp* _strides = PyArray_STRIDES(o);
        for( i = 0; i < dims-1; i++ )
            step[i] = (size_t)_strides[i];
        datastart = data = (uchar*)PyArray_DATA(o);
    }
    
    void deallocate(int* refcount, uchar* datastart, uchar* data)
    {
        static int ncalls = 0;
        printf("NumpyAllocator::deallocate: %d\n", ncalls++);
        
        if( !refcount )
            return;
        PyObject* o = pyObjectFromRefcount(refcount);
        Py_DECREF(o);
    }
};

NumpyAllocator g_numpyAllocator;
    
enum { ARG_NONE = 0, ARG_MAT = 1, ARG_SCALAR = 2 };

static int pyobjToMat(const PyObject* o, Mat& m, const char* name = "<unknown>", bool allowND=true)
{
    static int call_idx = 0;
    printf("pyobjToMatND: %d\n", call_idx++);
    
    if( !PyArray_Check(o) ) {
        if( o == Py_None )
            return ARG_NONE;
        failmsg("%s is not a numpy array", name);
        return -1;
    }
    
    int typenum = PyArray_TYPE(o);
    int type = typenum == NPY_UBYTE ? CV_8U : typenum == NPY_BYTE ? CV_8S :
               typenum == NPY_USHORT ? CV_16U : typenum == NPY_SHORT ? CV_16S : 
               typenum == NPY_INT || typenum == NPY_LONG ? CV_32S :
               typenum == NPY_FLOAT ? CV_32F :
               typenum == NPY_DOUBLE ? CV_64F : -1;
    
    if( type < 0 )
    {
        failmsg("%s data type = %d is not supported", name, typenum);
        return -1;
    }
    
    int ndims = PyArray_NDIM(o);
    if(ndims >= CV_MAX_DIM)
    {
        failmsg("%s dimensionality (=%d) is too high", name, ndims);
        return -1;
    }
    
    int size[CV_MAX_DIM+1];
    size_t step[CV_MAX_DIM+1], elemsize = CV_ELEM_SIZE(type);
    const npy_intp* _sizes = PyArray_DIMS(o);
    const npy_intp* _strides = PyArray_STRIDES(o);
    
    for(int i = 0; i < ndims; i++)
    {
        size[i] = (int)_sizes[i];
        step[i] = (size_t)_strides[i];
    }
    
    if( ndims == 0 || step[ndims-1] > elemsize ) {
        size[ndims] = 1;
        step[ndims] = elemsize;
        ndims++;
    }
    
    m = Mat(ndims, size, type, PyArray_DATA(o), step);
    
    if (!allowND)
    {
        if( ndims <= 2 )
            ;
        else if( ndims == 3 )
        {
            if( size[2] > CV_CN_MAX || step[1] != elemsize*size[2] )
            {
                failmsg("%s is not contiguous, thus it can not be interpreted as image", name);
                return -1;
            }
            m.dims--;
            m.flags = (m.flags & ~CV_MAT_TYPE_MASK) | CV_MAKETYPE(type, size[2]);
        }
        else
        {
            failmsg("%s is not contiguous or has more than 3 dimensions, thus it can not be interpreted as image", name);
            return -1;
        }
    }
    
    if( m.data )
    {
        m.refcount = refcountFromPyObject(o);
        ++*m.refcount; // protect the original numpy array from deallocation
                       // (since Mat destructor will decrement the reference counter)
    };
    m.allocator = &g_numpyAllocator;
    return ARG_MAT;
}

static void makeEmptyMat(Mat& m)
{
    m = Mat();
    m.allocator = &g_numpyAllocator;
}

static int pyobjToMat(const PyObject* o, Mat& m, const char* name = "<unknown>")
{
    Mat temp;
    int code = pyobjToMat(o, temp, name, false);
    if(code > 0)
        m = Mat(temp);
    return code;
}

static int pyobjToScalar(PyObject *o, Scalar& s, const char *name = "<unknown>")
{
    if (PySequence_Check(o)) {
        PyObject *fi = PySequence_Fast(o, name);
        if (fi == NULL)
            return -1;
        if (4 < PySequence_Fast_GET_SIZE(fi))
        {
            failmsg("Scalar value for argument '%s' is longer than 4", name);
            return -1;
        }
        for (Py_ssize_t i = 0; i < PySequence_Fast_GET_SIZE(fi); i++) {
            PyObject *item = PySequence_Fast_GET_ITEM(fi, i);
            if (PyFloat_Check(item) || PyInt_Check(item)) {
                s[i] = PyFloat_AsDouble(item);
            } else {
                failmsg("Scalar value for argument '%s' is not numeric", name);
                return -1;
            }
        }
        Py_DECREF(fi);
    } else {
        if (PyFloat_Check(o) || PyInt_Check(o)) {
            s[0] = PyFloat_AsDouble(o);
        } else {
            failmsg("Scalar value for argument '%s' is not numeric", name);
            return -1;
        }
    }
    return ARG_SCALAR;
}


static int pyobjToMatOrScalar(PyObject* obj, Mat& m, Scalar& s, const char* name, bool allowND)
{
    if( PyArray_Check(obj) || (obj == Py_None))
        return pyobjToMat(obj, m, name, allowND);

    return pyobjToScalar(obj, s, name);
}

static void pyc_add_mm(const Mat& a, const Mat& b, Mat& c, const Mat& mask) { add(a, b, c, mask); }
static void pyc_add_ms(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool) { add(a, s, c, mask); }
static void pyc_subtract_mm(const Mat& a, const Mat& b, Mat& c, const Mat& mask) { subtract(a, b, c, mask); }
static void pyc_subtract_ms(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool rev)
{
    if( !rev )
        subtract(a, s, c, mask);
    else
        subtract(s, a, c, mask);
}

static void pyc_and_mm(const Mat& a, const Mat& b, Mat& c, const Mat& mask) { bitwise_and(a, b, c, mask); }
static void pyc_and_ms(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool) { bitwise_and(a, s, c, mask); }
static void pyc_or_mm(const Mat& a, const Mat& b, Mat& c, const Mat& mask) { bitwise_or(a, b, c, mask); }
static void pyc_or_ms(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool) { bitwise_or(a, s, c, mask); }
static void pyc_xor_mm(const Mat& a, const Mat& b, Mat& c, const Mat& mask) { bitwise_xor(a, b, c, mask); }
static void pyc_xor_ms(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool) { bitwise_xor(a, s, c, mask); }
static void pyc_absdiff_mm(const Mat& a, const Mat& b, Mat& c, const Mat&) { absdiff(a, b, c); }
static void pyc_absdiff_ms(const Mat& a, const Scalar& s, Mat& c, const Mat&, bool) { absdiff(a, s, c); }
static void pyc_min_mm(const Mat& a, const Mat& b, Mat& c, const Mat&) { min(a, b, c); }
static void pyc_min_ms(const Mat& a, const Scalar& s, Mat& c, const Mat&, bool)
{
    CV_Assert( s.isReal() );
    min(a, s[0], c);
}
static void pyc_max_mm(const Mat& a, const Mat& b, Mat& c, const Mat&) { max(a, b, c); }
static void pyc_max_ms(const Mat& a, const Scalar& s, Mat& c, const Mat&, bool)
{
    CV_Assert( s.isReal() );
    max(a, s[0], c);
}

typedef void (*BinaryOp)(const Mat& a, const Mat& b, Mat& c, const Mat& mask);
typedef void (*BinaryOpS)(const Mat& a, const Scalar& s, Mat& c, const Mat& mask, bool rev);

static PyObject *pyopencv_binary_op(PyObject* args, PyObject* kw, BinaryOp binOp, BinaryOpS binOpS, bool supportMask)
{
    PyObject *pysrc1 = 0, *pysrc2 = 0, *pydst = 0, *pymask = 0;
    Mat src1, src2, dst, mask;
    Scalar alpha, beta;
    
    if( supportMask )
    {
        const char *keywords[] = { "src1", "src2", "dst", "mask", 0 };
        if( !PyArg_ParseTupleAndKeywords(args, kw, "OO|OO", (char**)keywords,
                                        &pysrc1, &pysrc2, &pydst, &pymask))
            return 0;
    }
    else
    {
        const char *keywords[] = { "src1", "src2", "dst", 0 };
        if( !PyArg_ParseTupleAndKeywords(args, kw, "OO|O", (char**)keywords,
                                         &pysrc1, &pysrc2, &pydst))
            return 0;
    }
    
    int code_src1 = pysrc1 ? pyobjToMatOrScalar(pysrc1, src1, alpha, "src1", true) : -1;
    int code_src2 = pysrc2 ? pyobjToMatOrScalar(pysrc2, src2, beta, "src2", true) : -1;
    int code_dst = pydst ? pyobjToMat(pydst, dst, "dst", true) : 0;
    int code_mask = pymask ? pyobjToMat(pymask, mask, "mask", true) : 0;
    
    if( code_src1 < 0 || code_src2 < 0 || code_dst < 0 || code_mask < 0 )
        return 0;
    
    if( code_src1 == ARG_SCALAR && code_src2 == ARG_SCALAR )
    {
        failmsg("Both %s and %s are scalars", "src1", "src2");
        return 0;
    }
    
    if( code_dst == 0 )
        makeEmptyMat(dst);
    
    ERRWRAP2(code_src1 != ARG_SCALAR && code_src2 != ARG_SCALAR ? binOp(src1, src2, dst, mask) :
             code_src1 != ARG_SCALAR ? binOpS(src2, alpha, dst, mask, true) : binOpS(src1, beta, dst, mask, false));
    
    PyObject* result = pyObjectFromRefcount(dst.refcount);
    int D = PyArray_NDIM(result);
    const npy_intp* sz = PyArray_DIMS(result);
    
    printf("Result: check = %d, ndims = %d, size = (%d x %d), typenum=%d\n", PyArray_Check(result),
           D, (int)sz[0], D >= 2 ? (int)sz[1] : 1, PyArray_TYPE(result));
    
    //Py_INCREF(result);
    return result;
}

static PyObject* pyopencv_add(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_add_mm, pyc_add_ms, true);
}

static PyObject* pyopencv_subtract(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_subtract_mm, pyc_subtract_ms, true);
}

static PyObject* pyopencv_and(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_and_mm, pyc_and_ms, true);
}

static PyObject* pyopencv_or(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_or_mm, pyc_or_ms, true);
}

static PyObject* pyopencv_xor(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_xor_mm, pyc_xor_ms, true);
}

static PyObject* pyopencv_absdiff(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_absdiff_mm, pyc_absdiff_ms, true);
}

static PyObject* pyopencv_min(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_min_mm, pyc_min_ms, true);
}

static PyObject* pyopencv_max(PyObject* self, PyObject* args, PyObject* kw)
{
    return pyopencv_binary_op(args, kw, pyc_max_mm, pyc_max_ms, true);
}

}
    
#endif