#include <Python.h>
#include <puzzle.h>

#if PY_MAJOR_VERSION >= 3
#define PyInt_AsLong(x) (PyLong_AsLong((x)))

static PyObject *
Py_FindMethod(PyMethodDef *def, PyObject *self, char *name)
{
    // Here we do not check `__doc__` for simplicity

    while (def->ml_name != NULL) {
        if (name[0] == def->ml_name[0] && strcmp(name+1, def->ml_name+1) == 0) {
            return PyCFunction_New(def, self);
        }

        def++;
    }

    PyErr_SetString(PyExc_AttributeError, name);
    return NULL;
}
#endif

#undef UNUSED
#define UNUSED(var)     ((void)&var)

#define DEFAULT_MAX_WIDTH           (3000)
#define DEFAULT_MAX_HEIGHT          (3000)
#define DEFAULT_LAMBDAS             (9)
#define DEFAULT_P_RATIO             (2.0)
#define DEFAULT_NOISE_CUTOFF        (2)
#define DEFAULT_CONTRAST_BARRIER    (5)
#define DEFAULT_MAX_CROPPING_RATIO  (0.25)
#define DEFAULT_AUTOCROP            (1)

/* Safe XDECREF for object states that handles nested deallocations */
#define ZAP(v) do {\
        PyObject *tmp = (PyObject *)(v); \
        (v) = NULL; \
        Py_XDECREF(tmp); \
} while (0)

static PyTypeObject *p_Puzzle_Type = NULL;

typedef struct {
    PyObject_HEAD
    PyObject *dict;
    PuzzleContext context;
} PuzzleObject;

/**************************************************
// Utility functions
**************************************************/

/* Convert PuzzleCvec to tuple */
static PyObject *
cvec_to_tuple(PuzzleCvec *cvec)
{
    PyObject *tuple = PyTuple_New(cvec->sizeof_vec);
    if (!tuple) {
        return NULL;
    }

    size_t i = 0;
    for (i = 0; i < cvec->sizeof_vec; i++) {
        PyObject *value = Py_BuildValue("b", cvec->vec[i]);
        if (!value) {
            Py_DECREF(tuple);
            return NULL;
        }
        PyTuple_SetItem(tuple, i, value);
    }

    return tuple;
}

/* Convert tuple to PuzzleCvec */
static void
tuple_to_cvec(PyObject *tuple, PuzzleCvec *cvec)
{
    Py_ssize_t tuple_size = PyTuple_Size(tuple);
    signed char *cvec_vec = (signed char *)calloc(tuple_size, sizeof(signed char));

    Py_ssize_t i = 0;
    for (i = 0; i < tuple_size; i++) {
        PyObject *item = PyTuple_GetItem(tuple, i);
        cvec_vec[i] = (signed char)PyInt_AsLong(item);
    }

    cvec->sizeof_vec = tuple_size;
    cvec->vec = cvec_vec;
}

/* Convert PuzzleCompressedCvec to tuple */
static PyObject *
compressed_cvec_to_tuple(PuzzleCompressedCvec *compressed_cvec)
{
    // Create tuple
    PyObject *tuple = PyTuple_New(compressed_cvec->sizeof_compressed_vec);
    if (!tuple) {
        return NULL;
    }

    // Fill tuple
    size_t i = 0;
    for (i = 0; i < compressed_cvec->sizeof_compressed_vec; i++) {
        PyObject *value = Py_BuildValue("B", compressed_cvec->vec[i]);
        if (!value) {
            Py_DECREF(tuple);
            return NULL;
        }
        PyTuple_SetItem(tuple, i, value);
    }

    return tuple;
}

/* Convert tuple to PuzzleCompressedCvec */
static void
compressed_tuple_to_cvec(PyObject *compressed_tuple, PuzzleCompressedCvec *compressed_cvec)
{
    Py_ssize_t tuple_size = PyTuple_Size(compressed_tuple);
    unsigned char *cvec_vec = (unsigned char *)calloc(tuple_size, sizeof(unsigned char));

    Py_ssize_t i = 0;
    for (i = 0; i < tuple_size; i++) {
        PyObject *item = PyTuple_GetItem(compressed_tuple, i);
        cvec_vec[i] = (signed char)PyInt_AsLong(item);
    }

    compressed_cvec->sizeof_compressed_vec = tuple_size;
    compressed_cvec->vec = cvec_vec;
}

/**************************************************
// PuzzleObject methods
**************************************************/

/* Initialize PuzzleObject */
static PuzzleObject *
puzzle_new(PyObject *dummy)
{
    UNUSED(dummy);

    PuzzleObject *self = NULL;

    self = (PuzzleObject *)PyObject_New(PuzzleObject, p_Puzzle_Type);
    if (self == NULL) {
        return NULL;
    }

    puzzle_init_context(&self->context);
    self->dict = NULL;

    return self;
}

/* Destructor for PuzzleObject */
static void
puzzle_dealloc(PuzzleObject *self)
{
    ZAP(self->dict);

    puzzle_free_context(&self->context);

    PyObject_Del(self);
}

/* Get normalized distance between two image files */
static PyObject *
get_distance_from_file(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    const char *file_path_1;
    const char *file_path_2;

    if (!PyArg_ParseTuple(args, "ss", &file_path_1, &file_path_2)) {
        return NULL;
    }

    PuzzleCvec cvec_1, cvec_2;

    puzzle_init_cvec(&po->context, &cvec_1);
    puzzle_init_cvec(&po->context, &cvec_2);

    if (puzzle_fill_cvec_from_file(&po->context, &cvec_1, file_path_1) != 0) {
        PyErr_Format(PyExc_IOError, "Fail to read file: %s", file_path_1);
        return NULL;
    }
    if (puzzle_fill_cvec_from_file(&po->context, &cvec_2, file_path_2) != 0) {
        PyErr_Format(PyExc_IOError, "Fail to read file: %s", file_path_2);
        return NULL;
    }

    double distance = puzzle_vector_normalized_distance(&po->context, &cvec_1, &cvec_2, 1);

    puzzle_free_cvec(&po->context, &cvec_1);
    puzzle_free_cvec(&po->context, &cvec_2);

    return Py_BuildValue("d", distance);
}

/* Get normalized distance between two PuzzleCvec objects */
static PyObject *
get_distance_from_cvec(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    PyObject *cvec_tuple_1;
    PyObject *cvec_tuple_2;

    if (!PyArg_ParseTuple(args, "OO", &cvec_tuple_1, &cvec_tuple_2)) {
        return NULL;
    }

    if (!PyTuple_Check(cvec_tuple_1) || !PyTuple_Check(cvec_tuple_2)) {
        PyErr_Format(PyExc_TypeError, "Should pass in tuple objects");
        return NULL;
    }

    PuzzleCvec cvec_1, cvec_2;
    puzzle_init_cvec(&po->context, &cvec_1);
    puzzle_init_cvec(&po->context, &cvec_2);

    // Convert tuple to cvec
    tuple_to_cvec(cvec_tuple_1, &cvec_1);
    tuple_to_cvec(cvec_tuple_2, &cvec_2);

    double distance = puzzle_vector_normalized_distance(&po->context, &cvec_1, &cvec_2, 1);

    puzzle_free_cvec(&po->context, &cvec_1);
    puzzle_free_cvec(&po->context, &cvec_2);

    return Py_BuildValue("d", distance);
}

/* Calculate PuzzleCvec from image file */
static PyObject *
get_cvec_from_file(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    const char *file_path;

    if (!PyArg_ParseTuple(args, "s", &file_path)) {
        return NULL;
    }

    // Initialize cvec
    PuzzleCvec cvec;
    puzzle_init_cvec(&po->context, &cvec);

    // Fill cvec
    if (puzzle_fill_cvec_from_file(&po->context, &cvec, file_path) != 0) {
        PyErr_Format(PyExc_IOError, "Fail to read file: %s", file_path);
        return NULL;
    }

    // Convert cvec to tuple
    PyObject *cvec_tuple = cvec_to_tuple(&cvec);

    puzzle_free_cvec(&po->context, &cvec);

    return cvec_tuple;
}

/* Compress cvec tuple */
static PyObject *
compress_cvec(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    PyObject *cvec_tuple;

    if (!PyArg_ParseTuple(args, "O", &cvec_tuple)) {
        return NULL;
    }

    // Convert tuple to cvec
    PuzzleCvec cvec;
    puzzle_init_cvec(&po->context, &cvec);
    tuple_to_cvec(cvec_tuple, &cvec);

    // Compress cvec
    PuzzleCompressedCvec compressed_cvec;
    puzzle_init_compressed_cvec(&po->context, &compressed_cvec);
    puzzle_compress_cvec(&po->context, &compressed_cvec, &cvec);

    // Convert compressed cvec to tuple
    PyObject *compressed_cvec_tuple = compressed_cvec_to_tuple(&compressed_cvec);

    puzzle_free_cvec(&po->context, &cvec);
    puzzle_free_compressed_cvec(&po->context, &compressed_cvec);

    return compressed_cvec_tuple;
}

/* Uncompress cvec tuple */
static PyObject *
uncompress_cvec(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    PyObject *compressed_cvec_tuple;

    if (!PyArg_ParseTuple(args, "O", &compressed_cvec_tuple)) {
        return NULL;
    }

    // Convert tuple to cvec
    PuzzleCompressedCvec compressed_cvec;
    puzzle_init_compressed_cvec(&po->context, &compressed_cvec);
    compressed_tuple_to_cvec(compressed_cvec_tuple, &compressed_cvec);

    // Uncompress cvec
    PuzzleCvec cvec;
    puzzle_init_cvec(&po->context, &cvec);
    puzzle_uncompress_cvec(&po->context, &compressed_cvec, &cvec);

    // Convert uncompressed cvec to tuple
    PyObject *cvec_tuple = cvec_to_tuple(&cvec);

    puzzle_free_compressed_cvec(&po->context, &compressed_cvec);
    puzzle_free_cvec(&po->context, &cvec);

    return cvec_tuple;
}

/**************************************************
// Methods to set tunables
**************************************************/

static PyObject *
set_max_width(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    unsigned int max_width = DEFAULT_MAX_WIDTH;

    if (!PyArg_ParseTuple(args, "I", &max_width)) {
        return NULL;
    }

    int result = puzzle_set_max_width(&po->context, max_width);

    return Py_BuildValue("i", result);
}

static PyObject *
set_max_height(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    unsigned int max_height = DEFAULT_MAX_HEIGHT;

    if (!PyArg_ParseTuple(args, "I", &max_height)) {
        return NULL;
    }

    int result = puzzle_set_max_height(&po->context, max_height);

    return Py_BuildValue("i", result);
}

static PyObject *
set_lambdas(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    unsigned int lambdas = DEFAULT_LAMBDAS;

    if (!PyArg_ParseTuple(args, "I", &lambdas)) {
        return NULL;
    }

    int result = puzzle_set_lambdas(&po->context, lambdas);

    return Py_BuildValue("i", result);
}

static PyObject *
set_p_ratio(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    double p_ratio = DEFAULT_P_RATIO;

    if (!PyArg_ParseTuple(args, "d", &p_ratio)) {
        return NULL;
    }

    int result = puzzle_set_p_ratio(&po->context, p_ratio);

    return Py_BuildValue("i", result);
}

static PyObject *
set_noise_cutoff(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    double noise_cutoff = DEFAULT_NOISE_CUTOFF;

    if (!PyArg_ParseTuple(args, "d", &noise_cutoff)) {
        return NULL;
    }

    int result = puzzle_set_noise_cutoff(&po->context, noise_cutoff);

    return Py_BuildValue("i", result);
}

static PyObject *
set_contrast_barrier_for_cropping(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    double barrier = DEFAULT_CONTRAST_BARRIER;

    if (!PyArg_ParseTuple(args, "d", &barrier)) {
        return NULL;
    }

    int result = puzzle_set_contrast_barrier_for_cropping(&po->context, barrier);

    return Py_BuildValue("i", result);
}

static PyObject *
set_max_cropping_ratio(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    double ratio = DEFAULT_MAX_CROPPING_RATIO;

    if (!PyArg_ParseTuple(args, "d", &ratio)) {
        return NULL;
    }

    int result = puzzle_set_max_cropping_ratio(&po->context, ratio);

    return Py_BuildValue("i", result);
}

static PyObject *
set_autocrop(PyObject *self, PyObject *args)
{
    PuzzleObject *po = (PuzzleObject *)self;
    int enable = DEFAULT_AUTOCROP;

    if (!PyArg_ParseTuple(args, "i", &enable)) {
        return NULL;
    }

    int result = puzzle_set_autocrop(&po->context, enable);

    return Py_BuildValue("i", result);
}

/**************************************************
// Type definitions
**************************************************/

static PyMethodDef PyPuzzleMethods[] = {
    {"Puzzle", (PyCFunction)puzzle_new, METH_NOARGS, ""},
    {NULL, NULL, 0, NULL}
};

static PyMethodDef PuzzleObjectMethods[] = {
    {"get_distance_from_file", get_distance_from_file, METH_VARARGS, "Get normalized distance between two images."},
    {"get_distance_from_cvec", get_distance_from_cvec, METH_VARARGS, "Get normalized distance between two cvec tuples."},
    {"get_cvec_from_file", get_cvec_from_file, METH_VARARGS, "Get the cvec of an image."},
    {"compress_cvec", compress_cvec, METH_VARARGS, "Compress cvec."},
    {"uncompress_cvec", uncompress_cvec, METH_VARARGS, "Uncompress cvec."},
    {"set_max_width", set_max_width, METH_VARARGS, "Set the max width of images. Default is 3000 pixels."},
    {"set_max_height", set_max_height, METH_VARARGS, "Set the max height of images. Default is 3000 pixels."},
    {"set_lambdas", set_lambdas, METH_VARARGS, "Set the lambdas value. Images are divided in lambda x lambda blocks. Default is 9."},
    {"set_p_ratio", set_p_ratio, METH_VARARGS, "Set the p ratio. The p ratio determines the size of the centered zone which the average intensity of each block is based upon. Default is 2.0."},
    {"set_noise_cutoff", set_noise_cutoff, METH_VARARGS, "Set the noise cutoff value. If you raise that value, more zones with little difference of intensity will be considered as similar. Default is 2."},
    {"set_contrast_barrier_for_cropping", set_contrast_barrier_for_cropping, METH_VARARGS, "Set the tolerance of auto-cropping borders. Default is 5."},
    {"set_max_cropping_ratio", set_max_cropping_ratio, METH_VARARGS, "Set the safe-guard value againt unwanted excessive auto-cropping. Defalut is 0.25."},
    {"set_autocrop", set_autocrop, METH_VARARGS, "Any value except 0 will enable auto-cropping. It is enabled by default."},
    {NULL, NULL, 0, NULL}
};

static PyObject *
puzzle_get_attr(PuzzleObject *self, char *attrname)
{
    PyObject *value = NULL;
    if (value == NULL && self->dict !=NULL) {
        value = PyDict_GetItemString(self->dict, attrname);
        if (value != NULL ) {
            Py_INCREF(value);
            return value;
        }
    }

    return Py_FindMethod(PuzzleObjectMethods, (PyObject *)self, attrname);
}

static int
puzzle_set_attr(PuzzleObject *self, char *attrname, PyObject *value)
{
    if (value == NULL) {
        int rv = -1;
        if (self->dict != NULL) {
            rv = PyDict_DelItemString(self->dict, attrname);
        }
        if (rv < 0) {
            PyErr_SetString(PyExc_AttributeError, "Non-existing attribute");
        }
        return rv;
    }
    if (self->dict == NULL) {
        self->dict = PyDict_New();
        if (self->dict == NULL) {
            return -1;
        }
    }
    return PyDict_SetItemString(self->dict, attrname, value);
}

static PyTypeObject Puzzle_Type = {
    // PyObject_HEAD_INIT(&PyType_Type)
    // 0,                          /* ob_size */
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "pypuzzle.Puzzle",          /* tp_name */
    sizeof(PuzzleObject),       /* tp_basicsize */
    0,                          /* tp_itemsize */
    /* Methods */
    (destructor)puzzle_dealloc, /* tp_dealloc */
    0,                          /* tp_print */
    (getattrfunc)puzzle_get_attr,   /* tp_getattr */
    (setattrfunc)puzzle_set_attr,   /* tp_setattr */
    0,                          /* tp_compare */
    0,                          /* tp_repr */
    0,                          /* tp_as_number */
    0,                          /* tp_as_sequence */
    0,                          /* tp_as_mapping */
    0,                          /* tp_hash */
    0,                          /* tp_call */
    0,                          /* tp_str */
    0,                          /* tp_getattro */
    0,                          /* tp_setattro */
    0,                          /* tp_as_buffer */
    0,                          /* tp_flags */
    0,                          /* tp_doc */
    0,                          /* tp_traverse */
    0,                          /* tp_clear */
};

/**************************************************
// Module level
**************************************************/

#if PY_MAJOR_VERSION >= 3
static struct PyModuleDef Puzzle_Module = {
    PyModuleDef_HEAD_INIT,
    "pypuzzle",
    NULL,
    -1,
    PyPuzzleMethods
};

PyMODINIT_FUNC
PyInit_pypuzzle(void)
{
    p_Puzzle_Type = &Puzzle_Type;
    return PyModule_Create(&Puzzle_Module);
}
#else
PyMODINIT_FUNC
initpypuzzle(void)
{
    p_Puzzle_Type = &Puzzle_Type;
    (void) Py_InitModule("pypuzzle", PyPuzzleMethods);
}
#endif

int
main(int argc, char *argv[])
{
    // Pass argv[0] to the Python interpreter.
#if PY_MAJOR_VERSION < 3
    Py_SetProgramName(argv[0]);
#else
    wchar_t *name = Py_DecodeLocale(argv[0], NULL);
    Py_SetProgramName(name);
#endif

    // Initialize the Python interpreter.
    Py_Initialize();

#if PY_MAJOR_VERSION < 3
    // Add a static module.
    initpypuzzle();
#endif

    return 0;
}
