#include "ClusterReader.h"
#include "arr_desc.h"
#include "cluster_reader.h"
#include "data_types.h"

//clang-format off
typedef struct {
    PyObject_HEAD FILE *fp;
    uint32_t n_left;
    Py_ssize_t chunk;
} ClusterFileReader;
//clang-format on

// Constructor: sets the fp to NULL then tries to open the file
// raises python exception if something goes wrong
// returned object should mean file is open and ready to read
static int ClusterFileReader_init(ClusterFileReader *self, PyObject *args,
                                  PyObject *kwds) {

    // Parse file name, accepts string or pathlike objects
    char *fname = NULL;
    self->n_left = 0;
    self->chunk = 0;
    PyObject *fname_obj = NULL;
    PyObject *fname_bytes = NULL;
    Py_ssize_t len;

    static char *kwlist[] = {"fname", "chunk", NULL};

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|n", kwlist, &fname_obj,
                                     &self->chunk)) {
        return -1;
    }

    if (fname_obj != Py_None)
        if (!PyUnicode_FSConverter(fname_obj, &fname_bytes))
            return -1;

    PyBytes_AsStringAndSize(fname_bytes, &fname, &len);
#ifdef CR_VERBOSE
    printf("Opening: %s\n chunk: %lu\n", fname, self->chunk);
#endif

    self->fp = fopen((const char *)fname, "rb");

    // Keep the return code to not return before releasing buffer
    int rc = 0;

    // Raise python exception using information from errno
    if (self->fp == NULL) {
        PyErr_SetFromErrnoWithFilename(PyExc_OSError, fname);
        rc = -1;
    }
    // Release buffer
    Py_DECREF(fname_bytes);

    // Success or fail
    return rc;
}

// Custom destructor to make sure we close the file
static void ClusterFileReader_dealloc(ClusterFileReader *self) {
    if (self->fp) {
#ifdef CR_VERBOSE
        printf("Closing file\n");
#endif
        fclose(self->fp);
        self->fp = NULL;
    }
    Py_TYPE(self)->tp_free((PyObject *)self);
}

// read method
static PyObject *ClusterFileReader_read(ClusterFileReader *self,
                                        PyObject *args) {

    
    Py_ssize_t size = 0;
    PyObject *noise_obj = NULL;
    

    //refcount of the python object is not increased when parsing with O
    if (!PyArg_ParseTuple(args, "|nO", &size, &noise_obj)) {
        PyErr_SetString(PyExc_TypeError, "Could not parse args.");
        return NULL;
    }

    // Fall back on object default/config
    if (size == 0)
        size = self->chunk;

    npy_intp dims[] = {size};

    // If possible numpy will
    // use the underlying buffer, otherwise it will create a copy, for example
    // if data type is different or we pass in a list. The
    // NPY_ARRAY_C_CONTIGUOUS flag ensures that we have contiguous memory.

#ifdef CR_VERBOSE
    printf("Getting ready to read: %lu clusters. Noise map: %p\n", size,
           noise_obj);
#endif

    // If the user passed a noise map we fetch a pointer to that array as well
    int nx = 0, ny = 0;
    PyObject *noise_array = NULL; 
    double *noise_map = NULL;
    if (noise_obj) {
        //The user could have passes Py_None to indicate no noise map
        //in that case we do nothing. 
        if (noise_obj == Py_None){
            printf("Got Py_None as noise map, doing nothing\n");
        }else{
            //Check that we really got a numpy array and not any kind of PyObject
            if(!PyArray_CheckExact(noise_obj)){
                PyErr_SetString(PyExc_TypeError, "Noise map has to be a numpy array (or None).");
                //TODO! Cleanup? 
                return NULL;
            }

            noise_array =
                PyArray_FROM_OTF(noise_obj, NPY_DOUBLE, NPY_ARRAY_IN_ARRAY | NPY_ARRAY_ENSUREARRAY);
            
            int ndim_noise = PyArray_NDIM((PyArrayObject *)(noise_array));
            npy_intp *noise_shape = PyArray_SHAPE((PyArrayObject *)(noise_array));

            // For the C++ function call we need pointers (or another C++ type/data
            //  structure)
            if (ndim_noise == 2) {
                nx = noise_shape[0];
                ny = noise_shape[1];
                noise_map = (double *)(PyArray_DATA((PyArrayObject *)(noise_array)));
            } else {
                PyErr_SetString(PyExc_TypeError, "Noise map has to be 2D.");
                //TODO! Cleanup? 
                return NULL;
            }
        }
    }

    // Create an uninitialized numpy array
    const int ndim = 1;
    PyObject *clusters = PyArray_SimpleNewFromDescr(ndim, dims, cluster_dt());

    // Fill with zeros
    PyArray_FILLWBYTE((PyArrayObject *)clusters, 0);

    // Get a pointer to the array memory
    void *buf = PyArray_DATA((PyArrayObject *)clusters);

    // Call the standalone C code to read clusters from file
    // Here goes the looping, removing frame numbers etc.
    int n_read = 0;
    if (noise_map)
        n_read = read_clusters_with_cut(self->fp, size, buf, &self->n_left,
                                        noise_map, nx, ny);
    else
        n_read = read_clusters(self->fp, size, buf, &self->n_left);

    if (n_read != size) {
        // resize the array to match the number of read photons
        // this will reallocate memory

        // create a new_shape struct on the stack
        PyArray_Dims new_shape;

        // reuse dims for the shape
        dims[0] = n_read;
        new_shape.ptr = dims;
        new_shape.len = 1;

        // resize the array to match the number of clusters read
        PyArray_Resize((PyArrayObject *)clusters, &new_shape, 1, NPY_ANYORDER);
    }


    //refcount of noise_obj was not increased
    //XDECREF also works if noise_array is NULL
    Py_XDECREF(noise_array);
    return clusters;
}

/* // clusterize method */
/* static PyObject *ClusterFileReader_clusterize(ClusterFileReader *self,
 * PyObject *args) { */

/*     const int ndim = 1; */

/*     Py_ssize_t size = 0; */
/*     PyObject *data_obj; */
/*     if (!PyArg_ParseTuple(args, "nO", &size,&data_obj)) { */
/*      PyErr_SetString( */
/* 		      PyExc_TypeError, */
/* 		      "Could not parse args."); */
/*      return NULL; */
/*    } */

/*    // */

/*     // Create two numpy arrays from the passed objects, if possible numpy
 * will */
/*     // use the underlying buffer, otherwise it will create a copy, for
 * example */
/*     // if data type is different or we pass in a list. The */
/*     // NPY_ARRAY_C_CONTIGUOUS flag ensures that we have contiguous memory. */
/*     PyObject *data_array = PyArray_FROM_OTF(data_obj, NPY_INT32,
 * NPY_ARRAY_C_CONTIGUOUS); */
/*     int nx=0,ny=0; */
/*     int32_t *data=NULL; */

/*     // If parsing of a or b fails we throw an exception in Python */
/*     if (data_array ) { */

/*       int ndim_data = PyArray_NDIM((PyArrayObject *)(data_array)); */
/*       npy_intp *data_shape = PyArray_SHAPE((PyArrayObject *)(data_array)); */

/*     // For the C++ function call we need pointers (or another C++ type/data
 */
/*     // structure) */

/*       data = (int32_t *)(PyArray_DATA((PyArrayObject *)(data_array))); */

/*     /\* for (int i=0; i< ndim_noise; i++) { *\/ */
/*     /\*   printf("Dimension %d size %d pointer \n",i,noise_shape[i],
 * noise_map); *\/ */

/*     /\* } *\/ */

/*       if (ndim_data==2) { */

/* 	nx=data_shape[0]; */
/* 	ny=data_shape[1]; */
/* 	if (ny!=9) { */
/* 	  PyErr_SetString( */
/* 			  PyExc_TypeError, */
/* 			  "Wrong data type."); */
/* 	  //	printf("Data found size %d %d %d\n",nx,ny,ndim); */
/* 	} */

/*     } else { */
/*      PyErr_SetString( */
/* 		      PyExc_TypeError, */
/* 		      "Wrong data type."); */

/*       } */

/*     } */

/*     // Create an uninitialized numpy array */
/*     //npy_intp dims[] = {nx}; */
/*     // printf("%d %d\n",ndim,nx); */
/*     npy_intp dims[] = {nx}; */
/*     PyObject *ca = PyArray_SimpleNewFromDescr(ndim, dims,
 * cluster_analysis_dt()); */

/*     //  printf("1\n"); */

/*     // Fill with zeros */
/*     PyArray_FILLWBYTE((PyArrayObject *)ca, 0); */

/*     // printf("2\n"); */
/*     // Get a pointer to the array memory */
/*     void *buf = PyArray_DATA((PyArrayObject *)ca); */

/*     // Call the standalone C code to read clusters from file */
/*     // Here goes the looping, removing frame numbers etc. */

/*     //  printf("3\n"); */
/*     int n_read=analyze_clusters(nx,data,buf,size); */
/*     if (n_read != nx) { */
/*         // resize the array to match the number of read photons */
/*         // this will reallocate memory */

/*         // create a new_shape struct on the stack */
/*         PyArray_Dims new_shape; */

/*         // reuse dims for the shape */
/*         //dims[0] = n_read; */
/*         new_shape.ptr = n_read; */
/*         new_shape.len = 1; */

/*         // resize the array to match the number of clusters read */
/*         PyArray_Resize((PyArrayObject *)ca, &new_shape, 1, NPY_ANYORDER); */
/*     } */

/*     return ca; */

/* } */

// List all methods in our ClusterFileReader class
static PyMethodDef ClusterFileReader_methods[] = {
    {"read", (PyCFunction)ClusterFileReader_read, METH_VARARGS,
     "Read clusters"},
    /* {"clusterize", (PyCFunction)ClusterFileReader_clusterize, METH_VARARGS,
     */
    /*  "Analyze clusters"}, */
    {NULL, NULL, 0, NULL} /* Sentinel */
};

// Class defenition
static PyTypeObject ClusterFileReaderType = {
    PyVarObject_HEAD_INIT(NULL, 0).tp_name = "creader.ClusterFileReader",
    .tp_doc = PyDoc_STR("ClusterFileReader implemented in C"),
    .tp_basicsize = sizeof(ClusterFileReader),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .tp_new = PyType_GenericNew,
    .tp_dealloc = (destructor)ClusterFileReader_dealloc,
    .tp_init = (initproc)ClusterFileReader_init,
    .tp_methods = ClusterFileReader_methods,
};

PyObject *init_ClusterFileReader(PyObject *m) {
    import_array();
    if (PyType_Ready(&ClusterFileReaderType) < 0)
        return NULL;

    Py_INCREF(&ClusterFileReaderType);
    if (PyModule_AddObject(m, "ClusterFileReader",
                           (PyObject *)&ClusterFileReaderType) < 0) {
        Py_DECREF(&ClusterFileReaderType);
        Py_DECREF(m);
        return NULL;
    }
    return m;
}
