/*
 * Copyright (c) 2013-2014, Kevin Greenan (kmgreen2@gmail.com)
 * Copyright (c) 2014, Tushar Gohad (tusharsg@gmail.com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or
 * other materials provided with the distribution.  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 HOLDER 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.
 */

#include <stdint.h>
#include <stdio.h>
#include <paths.h>
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <math.h>
#include <bytesobject.h>
#include <liberasurecode/erasurecode.h>

#include <pyeclib_c.h>

/* Python 3 compatibility macros */
#if PY_MAJOR_VERSION >= 3
  #define PYTHON3
  #define MOD_ERROR_VAL NULL
  #define MOD_SUCCESS_VAL(val) val
  #define MOD_INIT(name) PyMODINIT_FUNC PyInit_##name(void)
  #define MOD_DEF(ob, name, doc, methods) \
          static struct PyModuleDef moduledef = { \
              PyModuleDef_HEAD_INIT, name, doc, -1, methods, }; \
          ob = PyModule_Create(&moduledef);
  #define PY_BUILDVALUE_OBJ_LEN(obj, objlen) \
          Py_BuildValue("y#", obj, (Py_ssize_t)objlen)
  #define PyInt_FromLong PyLong_FromLong
  #define PyString_FromString PyUnicode_FromString
  #define ENCODE_ARGS "Oy#"
  #define GET_METADATA_ARGS "Oy#i"
#else
  #define MOD_ERROR_VAL
  #define MOD_SUCCESS_VAL(val)
  #define MOD_INIT(name) void init##name(void)
  #define MOD_DEF(ob, name, doc, methods) \
          ob = Py_InitModule3(name, methods, doc);
  #define PY_BUILDVALUE_OBJ_LEN(obj, objlen) \
          Py_BuildValue("s#", obj, (Py_ssize_t)objlen)
  #define ENCODE_ARGS "Os#"
  #define GET_METADATA_ARGS "Os#i"
#endif


typedef struct pyeclib_byte_range {
  uint64_t offset;
  uint64_t length;
} pyeclib_byte_range_t;

/**
 * Prototypes for Python/C API methods
 */
static PyObject * pyeclib_c_init(PyObject *self, PyObject *args);
static void pyeclib_c_destructor(PyObject *obj);
static PyObject * pyeclib_c_get_segment_info(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_encode(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_reconstruct(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_decode(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_get_metadata(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_check_metadata(PyObject *self, PyObject *args);
static PyObject * pyeclib_c_liberasurecode_version(PyObject *self, PyObject *args);

static PyObject *import_class(const char *module, const char *cls)
{
    PyObject *s = PyImport_ImportModule(module);
    if (s == NULL) {
        return NULL;
    }
    return (PyObject *) PyObject_GetAttrString(s, cls);
}

/**
 * Allocate a buffer of a specific size and set its' contents
 * to the specified value.
 *
 * @param size integer size in bytes of buffer to allocate
 * @param value
 * @return pointer to start of allocated buffer or NULL on error
 */
void * alloc_and_set_buffer(int size, int value) {
    void * buf = NULL;  /* buffer to allocate and return */

    /* Allocate and zero the buffer, or set the appropriate error */
    buf = malloc((size_t) size);
    if (buf) {
        buf = memset(buf, value, (size_t) size);
    }
    return buf;
}

/**
 * Allocate a zero-ed buffer of a specific size.
 *
 * @param size integer size in bytes of buffer to allocate
 * @return pointer to start of allocated buffer or NULL on error
 */
void * alloc_zeroed_buffer(int size)
{
    return alloc_and_set_buffer(size, 0);
}

/**
 * Deallocate memory buffer if it's not NULL.  This methods returns NULL so
 * that you can free and reset a buffer using a single line as follows:
 *
 * my_ptr = check_and_free_buffer(my_ptr);
 *
 * @return NULL
 */
void * check_and_free_buffer(void * buf)
{
    if (buf)
        free(buf);
    return NULL;
}

void pyeclib_c_seterr(int ret, const char * prefix) {
    char *err_class;
    char *err_msg;
    char err[255];

    // If any error was previously set, we're explicitly ignoring it
    // to raise something new
    PyErr_Clear();

    switch (ret) {
        case -EBACKENDNOTAVAIL:
            err_class = "ECBackendInstanceNotAvailable";
            err_msg = "Backend instance not found";
            break;
        case -EINSUFFFRAGS:
            err_class = "ECInsufficientFragments";
            err_msg = "Insufficient number of fragments";
            break;
        case -EBACKENDNOTSUPP:
            err_class = "ECBackendNotSupported";
            err_msg = "Backend not supported";
            break;
        case -EINVALIDPARAMS:
            err_class = "ECInvalidParameter";
            err_msg = "Invalid arguments";
            break;
        case -EBADCHKSUM:
            err_class = "ECBadFragmentChecksum";
            err_msg = "Fragment integrity check failed";
            break;
        case -EBADHEADER:
            err_class = "ECInvalidFragmentMetadata";
            err_msg = "Fragment integrity check failed";
            break;
        case -ENOMEM:
            err_class = "ECOutOfMemory";
            err_msg = "Out of memory";
            break;
        case EDEADLK:
            err_class = "ECDriverError";
            err_msg = "Thread already owns lock";
            break;
        case EINVAL:
            err_class = "ECDriverError";
            err_msg = "Invalid read-write lock";
            break;
        default:
            err_class = "ECDriverError";
            err_msg = "Unknown error";
            break;
    }
    PyObject *eo = import_class("pyeclib.ec_iface", err_class);
    if (eo != NULL) {
        snprintf(err, 255,
                "%s ERROR: %s. Please inspect syslog for liberasurecode error report.",
                prefix, err_msg);
        PyErr_SetString(eo, err);
    }
}

static int stderr_fd;
static fpos_t stderr_fpos;
#ifndef _PATH_DEVNULL
#define _PATH_DEVNULL "/dev/null"
#endif

static void redirect_stderr(void)
{
    fflush(stderr);
    fgetpos(stderr, &stderr_fpos);
    stderr_fd = dup(fileno(stderr));
    freopen(_PATH_DEVNULL, "w", stderr);
}

static void restore_stderr(void)
{
    fflush(stderr);
    dup2(stderr_fd, fileno(stderr));
    close(stderr_fd);
    clearerr(stderr);
    fsetpos(stderr, &stderr_fpos);        /* for C9X */
}

/**
 * Constructor method for creating a new pyeclib object using the given parameters.
 *
 * @param k integer number of data elements
 * @param m integer number of checksum elements
 * @param hd hamming distance
 * @param backend_id erasure coding backend
 * @param use_inline_chksum type of inline fragment header checksum
 * @param use_algsig_chksum use algorithmic signature for fragment header checksum
 * @param validate only validate backend and params, close handle immediately
 * @return pointer to PyObject or NULL on error
 */
static PyObject *
pyeclib_c_init(PyObject *self, PyObject *args)
{
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *pyeclib_obj_handle = NULL;
  int k, m, hd = 0, validate = 0;
  int use_inline_chksum = 0, use_algsig_chksum = 0;
  const ec_backend_id_t backend_id;

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "iii|iiiii",
                        &k, &m, &backend_id, &hd, &use_inline_chksum,
                        &use_algsig_chksum, &validate)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_init");
    return NULL;
  }

  /* Allocate and initialize the pyeclib object */
  pyeclib_handle = (pyeclib_t *) alloc_zeroed_buffer(sizeof(pyeclib_t));
  if (NULL == pyeclib_handle) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_init");
    goto cleanup;
  }

  pyeclib_handle->ec_args.k = k;
  pyeclib_handle->ec_args.m = m;
  pyeclib_handle->ec_args.hd = hd;
  pyeclib_handle->ec_args.ct = use_inline_chksum ? CHKSUM_CRC32 : CHKSUM_NONE;

  if (validate)
    redirect_stderr();

  pyeclib_handle->ec_desc = liberasurecode_instance_create(backend_id, &(pyeclib_handle->ec_args));
  if (pyeclib_handle->ec_desc <= 0) {
    /* liberasurecode returns status in ec_desc as one of the error codes
     * (LIBERASURECODE_ERROR_CODES) defined in erasurecode.h */
    pyeclib_c_seterr(pyeclib_handle->ec_desc, "pyeclib_c_init");
    goto cleanup;
  }

  /* Prepare the python object to return */
  pyeclib_obj_handle = PyCapsule_New(pyeclib_handle, PYECC_HANDLE_NAME,
                                     pyeclib_c_destructor);

  /* Clean up the allocated memory on error, or update the ref count */
  if (pyeclib_obj_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_init");
    goto cleanup;
  } else {
    Py_INCREF(pyeclib_obj_handle);
  }

exit:
  if (validate)
    restore_stderr();
  return pyeclib_obj_handle;

cleanup:
  check_and_free_buffer(pyeclib_handle);
  pyeclib_obj_handle = NULL;
  goto exit;
}


static pyeclib_t *
_destroy(PyObject *obj, int in_destructor)
{
  pyeclib_t *pyeclib_handle = NULL;  /* pyeclib object to destroy */
  int ret;

  if (!PyCapsule_CheckExact(obj)) {
    if (!in_destructor) {
      pyeclib_c_seterr(-1, "pyeclib_c_destroy");
    }
    return NULL;
  }

  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(obj, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    if (!in_destructor) {
      pyeclib_c_seterr(-1, "pyeclib_c_destroy");
    }
    return NULL;
  }

  if (liberasurecode_get_version() < 0x010605) {
    /*
     * Prior to https://review.opendev.org/c/openstack/liberasurecode/+/929193
     * it won't actually be safe for us to destroy the instance.
     */
    return pyeclib_handle;
  }
  /*
   * Free up the liberasure instance using liberasurecode.
   * If it fails due to wrlock issues, fall back to check_and_free_buffer
   * to GC the instance.
   */
  ret = liberasurecode_instance_destroy(pyeclib_handle->ec_desc);
  if (ret != 0) {
    if (in_destructor) {
      /* destructor still wants to check_and_free */
      return pyeclib_handle;
    }
    pyeclib_c_seterr(ret, "pyeclib_c_destroy");
    return NULL;
  }
  return pyeclib_handle;
}

/**
 * Destroy method for cleaning up pyeclib object.
 */
static PyObject *
pyeclib_c_destroy(PyObject *self, PyObject *obj)
{
  if (_destroy(obj, 0) == NULL) {
    return NULL;
  }
  Py_RETURN_NONE;
}


/**
 * Destructor method for cleaning up pyeclib object.
 */
static void
pyeclib_c_destructor(PyObject *obj)
{
  pyeclib_t *pyeclib_handle = _destroy(obj, 1);
  check_and_free_buffer(pyeclib_handle);
}


/**
 * This function takes data length and a segment size and returns an object
 * containing:
 *
 * segment_size: size of the payload to give to encode()
 * last_segment_size: size of the payload to give to encode()
 * fragment_size: the fragment size returned by encode()
 * last_fragment_size: the fragment size returned by encode()
 * num_segments: number of segments
 *
 * This allows the caller to prepare requests when segmenting a data stream
 * to be EC'd.
 *
 * Since the data length will rarely be aligned to the segment size, the last
 * segment will be a different size than the others.
 *
 * There are restrictions on the length given to encode(), so calling this
 * before encode is highly recommended when segmenting a data stream.
 *
 * Minimum segment size depends on the underlying EC type (if it is less
 * than this, then the last segment will be slightly larger than the others,
 * otherwise it will be smaller).
 *
 * @param pyeclib_obj_handle
 * @param data_len integer length of data in bytes
 * @param segment_size integer length of segment in bytes
 * @return a python dictionary with segment information
 *
 */
static PyObject *
pyeclib_c_get_segment_info(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *ret_dict = NULL;               /* python dictionary to return */

  int data_len;                            /* data length from user in bytes */
  int segment_size, last_segment_size;     /* segment sizes in bytes */
  int num_segments;                        /* total number of segments */
  int fragment_size, last_fragment_size;   /* fragment sizes in bytes */
  int min_segment_size;                    /* EC algorithm's min. size (B) */

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "Oii", &pyeclib_obj_handle, &data_len, &segment_size)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
    return NULL;
  }

  /* The minimum segment size depends on the EC algorithm */
  min_segment_size = liberasurecode_get_minimum_encode_size(pyeclib_handle->ec_desc);
  if (min_segment_size < 0) {
      pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
      return NULL;
  }

  /* Get the number of segments */
  num_segments = (int)ceill((double)data_len / segment_size);

  /*
   * If there are two segments and the last is smaller than the
   * minimum size, then combine into a single segment
   */
  if (num_segments == 2 && data_len < (segment_size + min_segment_size)) {
    num_segments--;
  }

  /* Compute the fragment size from the segment size */
  if (num_segments == 1) {
    /*
     * There is one fragment, or two fragments, where the second is
     * smaller than the min_segment_size, just create one segment
     */

    /*
     * This will retrieve fragment_size calculated by liberasurecode with
     * specified backend.
     */

    fragment_size = liberasurecode_get_fragment_size(pyeclib_handle->ec_desc, data_len);
    if (fragment_size < 0) {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
	return NULL;
    }

    /* Segment size is the user-provided segment size */
    segment_size = data_len;
    last_fragment_size = fragment_size;
    last_segment_size = segment_size;
  } else {
    /*
     * There will be at least 2 segments, where the last exceeeds
     * the minimum segment size.
     */

    fragment_size = liberasurecode_get_fragment_size(pyeclib_handle->ec_desc, segment_size);
    if (fragment_size < 0) {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
	return NULL;
    }

    last_segment_size = data_len - (segment_size * (num_segments - 1));

    /*
     * The last segment is lower than the minimum size, so combine it
     * with the previous fragment
     */
    if (last_segment_size < min_segment_size) {
      // assert(num_segments > 2)?

      /* Add current "last segment" to second to last segment */
      num_segments--;
      last_segment_size = last_segment_size + segment_size;
    }

    last_fragment_size = liberasurecode_get_fragment_size(pyeclib_handle->ec_desc, last_segment_size);
    if (fragment_size < 0) {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_segment_info");
	return NULL;
    }
  }

  /* Add header to fragment sizes */
  last_fragment_size += sizeof(fragment_header_t);
  fragment_size += sizeof(fragment_header_t);

  /* Create and return the python dictionary of segment info */
  ret_dict = Py_BuildValue(
    "{s:i, s:i, s:i, s:i, s:i}",
    "segment_size", segment_size,
    "last_segment_size", last_segment_size,
    "fragment_size", fragment_size,
    "last_fragment_size", last_fragment_size,
    "num_segments", num_segments);
  if (NULL == ret_dict) {
    goto error;
  }

exit:
    return ret_dict;

error:
    // To prevent unexpected call, this is placed after return call
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_get_segment_info");
    Py_XDECREF(ret_dict);
    ret_dict = NULL;
    goto exit;

}


/**
 * Erasure encode a data buffer.
 *
 * @param pyeclib_obj_handle
 * @param data to encode
 * @return python list of encoded data and parity elements
 */
static PyObject *
pyeclib_c_encode(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  char **encoded_data = NULL;     /* array of k data buffers */
  char **encoded_parity = NULL;     /* array of m parity buffers */
  PyObject *list_of_strips = NULL;  /* list of encoded strips to return */
  char *data;                       /* param, data buffer to encode */
  Py_ssize_t data_len;              /* param, length of data buffer */
  uint64_t fragment_len;            /* length, in bytes of the fragments */
  int i;                            /* a counter */
  int ret = 0;

  /* Assume binary data (force "byte array" input) */
  if (!PyArg_ParseTuple(args, ENCODE_ARGS, &pyeclib_obj_handle, &data, &data_len)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_encode");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_encode");
    return NULL;
  }

  ret = liberasurecode_encode(pyeclib_handle->ec_desc, data, data_len, &encoded_data, &encoded_parity, &fragment_len);
  if (ret < 0) {
    pyeclib_c_seterr(ret, "pyeclib_c_encode");
    return NULL;
  }

  /* Create the python list of fragments to return */
  list_of_strips = PyList_New(pyeclib_handle->ec_args.k + pyeclib_handle->ec_args.m);
  if (NULL == list_of_strips) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_encode");
    return NULL;
  }

  /* Add data fragments to the python list to return */
  for (i = 0; i < pyeclib_handle->ec_args.k; i++) {
    PyList_SetItem(list_of_strips, i,
                    PY_BUILDVALUE_OBJ_LEN(encoded_data[i], fragment_len));
  }

  /* Add parity fragments to the python list to return */
  for (i = 0; i < pyeclib_handle->ec_args.m; i++) {
    PyList_SetItem(list_of_strips, pyeclib_handle->ec_args.k + i,
                    PY_BUILDVALUE_OBJ_LEN(encoded_parity[i], fragment_len));
  }

  liberasurecode_encode_cleanup(pyeclib_handle->ec_desc, encoded_data, encoded_parity);

  return list_of_strips;
}


/**
 * Return a list of lists with valid rebuild indexes given an EC algorithm
 * and a list of missing indexes.
 *
 * @param pyeclib_obj_handle
 * @param reconstruct_list list of missing fragments to reconstruct
 * @param exclude_list list of fragments to exclude from reconstruction
 * @return a list of lists of indexes to rebuild data from
 */
static PyObject *
pyeclib_c_get_required_fragments(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *reconstruct_list = NULL;        /* list of missing fragments to reconstruct */
  PyObject *exclude_list = NULL;        /* list of fragments to exclude from reconstruction */
  PyObject *fragment_idx_list = NULL;   /* list of req'd indexes to return */
  int *c_reconstruct_list = NULL;           /* c-array of missing indexes */
  int *c_exclude_list = NULL;           /* c-array of missing indexes */
  int num_missing;                      /* size of passed in missing list */
  int num_exclude;                      /* size of passed in exclude list */
  int i = 0;                            /* counters */
  int k, m;                             /* EC algorithm parameters */
  int *fragments_needed = NULL;         /* indexes of xor code fragments */
  int ret;                              /* return value for xor code */

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "OOO", &pyeclib_obj_handle, &reconstruct_list, &exclude_list)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_required_fragments");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_required_fragments");
    return NULL;
  }
  k = pyeclib_handle->ec_args.k;
  m = pyeclib_handle->ec_args.m;

  /* Generate -1 terminated c-array and bitmap of missing indexes */
  num_missing = (int) PyList_Size(reconstruct_list);
  c_reconstruct_list = (int *) alloc_zeroed_buffer((num_missing + 1) * sizeof(int));
  if (NULL == c_reconstruct_list) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_get_required_fragments");
    return NULL;
  }
  c_reconstruct_list[num_missing] = -1;
  for (i = 0; i < num_missing; i++) {
    PyObject *obj_idx = PyList_GetItem(reconstruct_list, i);
    long idx = PyLong_AsLong(obj_idx);
    c_reconstruct_list[i] = (int) idx;
  }

  num_exclude = (int) PyList_Size(exclude_list);
  c_exclude_list = (int *) alloc_zeroed_buffer((num_exclude + 1) * sizeof(int));
  if (NULL == c_exclude_list) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_get_required_fragments");
    goto exit;
  }
  c_exclude_list[num_exclude] = -1;
  for (i = 0; i < num_exclude; i++) {
    PyObject *obj_idx = PyList_GetItem(exclude_list, i);
    long idx = PyLong_AsLong(obj_idx);
    c_exclude_list[i] = (int) idx;
  }

  fragments_needed = alloc_zeroed_buffer(sizeof(int) * (k + m));
  if (NULL == fragments_needed) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_get_required_fragments");
    goto exit;
  }

  ret = liberasurecode_fragments_needed(pyeclib_handle->ec_desc, c_reconstruct_list,
                                        c_exclude_list, fragments_needed);
  if (ret < 0) {
    pyeclib_c_seterr(ret, "pyeclib_c_get_required_fragments");
    goto exit;
  }

  /* Post-process into a Python list */
  fragment_idx_list = PyList_New(0);
  if (NULL == fragment_idx_list) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_get_required_fragments");
    goto exit;
  }

  for (i = 0; fragments_needed[i] > -1; i++) {
    PyList_Append(fragment_idx_list, Py_BuildValue("i", fragments_needed[i]));
  }

exit:
  check_and_free_buffer(c_reconstruct_list);
  check_and_free_buffer(c_exclude_list);
  check_and_free_buffer(fragments_needed);

  return fragment_idx_list;
}


/**
 * Reconstruct a missing fragment from the the remaining fragments.
 *
 * TODO: If we are reconstructing a parity element, ensure that all of the
 * data elements are available!
 *
 * @param pyeclib_obj_handle
 * @param data_list k length list of data elements
 * @param parity_list m length list of parity elements
 * @param missing_idx_list list of the indexes of missing elements
 * @param destination_idx index of fragment to reconstruct
 * @param fragment_size size in bytes of the fragments
 * @return reconstructed destination fragment or NULL on error
 */
static PyObject *
pyeclib_c_reconstruct(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *fragments = NULL;           /* param, list of fragments */
  PyObject *reconstructed = NULL;       /* reconstructed object to return */
  char * c_reconstructed = NULL;        /* C string of reconstructed fragment */
  int fragment_len;                     /* param, size in bytes of fragment */
  int num_fragments;                    /* number of fragments passed in */
  char **c_fragments = NULL;            /* C array containing the fragment payloads */
  int destination_idx;                  /* param, index to reconstruct */
  int ret;                              /* decode matrix creation return val */
  int i = 0;                            /* a counter */

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "OOii", &pyeclib_obj_handle, &fragments,
                                        &fragment_len, &destination_idx)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_reconstruct");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_reconstruct");
    return NULL;
  }

  /* Pre-processing Python data structures */
  if (!PyList_Check(fragments)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_reconstruct");
    return NULL;
  }

  num_fragments = PyList_Size(fragments);

  c_fragments = (char **) alloc_zeroed_buffer(sizeof(char *) * num_fragments);
  if (NULL == c_fragments) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_reconstruct");
    goto error;
  }

  c_reconstructed = (char*) alloc_zeroed_buffer(sizeof(char) * fragment_len);
  if (NULL == c_fragments) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_reconstruct");
    goto error;
  }

  /* Put the fragments into an array of C strings */
  for (i = 0; i < num_fragments; i++) {
    PyObject *tmp_data = PyList_GetItem(fragments, i);
    Py_ssize_t len = 0;
    PyBytes_AsStringAndSize(tmp_data, &(c_fragments[i]), &len);
  }

  ret = liberasurecode_reconstruct_fragment(pyeclib_handle->ec_desc,
                                            c_fragments,
                                            num_fragments,
                                            fragment_len,
                                            destination_idx,
                                            c_reconstructed);
  if (ret < 0) {
    pyeclib_c_seterr(ret, "pyeclib_c_reconstruct");
    reconstructed = NULL;
  } else {
    reconstructed = PY_BUILDVALUE_OBJ_LEN(c_reconstructed, fragment_len);
  }

  goto out;

error:
  reconstructed = NULL;

out:
  check_and_free_buffer(c_fragments);
  check_and_free_buffer(c_reconstructed);

  return reconstructed;
}

/**
 * Decode a set of fragments into the original string
 *
 * @param pyeclib_obj_handle
 * @param data_list k length list of data elements
 * @param parity_list m length list of parity elements
 * @param missing_idx_list list of the indexes of missing elements
 * @param fragment_size size in bytes of the fragments
 * @return list of fragments
 */
static PyObject *
pyeclib_c_decode(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *fragments = NULL;             /* param, list of missing indexes */
  PyObject *ret_payload = NULL;           /* object to store original payload or ranges of payload */
  PyObject *ranges = NULL;                /* a list of tuples that represent byte ranges */
  PyObject *metadata_checks_obj = NULL;   /* boolean specifying if headers should be validated before decode */
  pyeclib_byte_range_t *c_ranges = NULL;  /* the byte ranges */
  int num_ranges = 0;                     /* number of specified ranges */
  int fragment_len;                       /* param, size in bytes of fragment */
  char **c_fragments = NULL;              /* k length array of data buffers */
  int num_fragments;                      /* param, number of fragments */
  char *c_orig_payload = NULL;            /* buffer to store original payload in */
  uint64_t range_payload_size = 0;        /* length of buffer used to store byte ranges */
  uint64_t orig_data_size = 0;            /* data size in bytes ,from fragment hdr */
  int i = 0;                              /* counters */
  int force_metadata_checks = 0;          /* validate the fragment headers before decoding */
  int ret = 0;

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "OOi|OO",&pyeclib_obj_handle, &fragments,
    &fragment_len, &ranges, &metadata_checks_obj)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode");
    return NULL;
  }

  /* If Python filled in None, we may get a reference.  If so, set the pointer to NULL */
  if (NULL != ranges && ranges == Py_None) {
    ranges = NULL;
  }

  /* Liberasurecode wants an integer, so convert the PyBool to an integer */
  if (NULL != metadata_checks_obj && PyObject_IsTrue(metadata_checks_obj)) {
    force_metadata_checks = 1;
  }

  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode");
    return NULL;
  }
  if (!PyList_Check(fragments)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode");
    return NULL;
  }

  num_fragments = PyList_Size(fragments);

  if (ranges) {
    num_ranges = PyList_Size(ranges);
  }

  if (pyeclib_handle->ec_args.k > num_fragments) {
    pyeclib_c_seterr(-EINSUFFFRAGS, "pyeclib_c_decode");
    return NULL;
  }

  if (num_ranges > 0) {
    c_ranges = (pyeclib_byte_range_t*)malloc(sizeof(pyeclib_byte_range_t) * num_ranges);
    if (NULL == c_ranges) {
        pyeclib_c_seterr(-ENOMEM, "pyeclib_c_decode");
        goto error;
    }
    for (i = 0; i < num_ranges; i++) {
      PyObject *tuple = PyList_GetItem(ranges, i);
      if (PyTuple_Size(tuple) == 2) {
        PyObject *py_begin = PyTuple_GetItem(tuple, 0);
        PyObject *py_end = PyTuple_GetItem(tuple, 1);
        long begin, end;

        if (PyLong_Check(py_begin))
            begin = PyLong_AsLong(py_begin);
#ifndef PYTHON3
        else if (PyInt_Check(py_begin))
            begin = PyInt_AsLong(py_begin);
#endif
        else {
          pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode invalid range");
          goto error;
        }
        if (PyLong_Check(py_end))
            end = PyLong_AsLong(py_end);
#ifndef PYTHON3
        else if (PyInt_Check(py_end))
            end = PyInt_AsLong(py_end);
#endif
        else {
          pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode invalid range");
          goto error;
        }

        c_ranges[i].offset = begin;
        c_ranges[i].length = end - begin + 1;

        range_payload_size += c_ranges[i].length;
      } else {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode invalid range");
        goto error;
      }
    }
  }

  c_fragments = (char **) alloc_zeroed_buffer(sizeof(char *) * num_fragments);
  if (NULL == c_fragments) {
    goto error;
  }

  /* Put the fragments into an array of C strings */
  for (i = 0; i < num_fragments; i++) {
    PyObject *tmp_data = PyList_GetItem(fragments, i);
    Py_ssize_t len = 0;
    PyBytes_AsStringAndSize(tmp_data, &(c_fragments[i]), &len);
  }

  ret = liberasurecode_decode(pyeclib_handle->ec_desc,
                            c_fragments,
                            num_fragments,
                            fragment_len,
                            force_metadata_checks,
                            &c_orig_payload,
                            &orig_data_size);

  if (ret < 0) {
    pyeclib_c_seterr(ret, "pyeclib_c_decode");
    goto error;
  }

  if (num_ranges == 0) {
    ret_payload = PY_BUILDVALUE_OBJ_LEN(c_orig_payload, orig_data_size);
  } else {
    ret_payload = PyList_New(num_ranges);
    if (NULL == ret_payload) {
        pyeclib_c_seterr(-ENOMEM, "pyeclib_c_decode");
        goto error;
    }
    range_payload_size = 0;
    for (i = 0; i < num_ranges; i++) {
      /* Check that range is within the original buffer */
      if (c_ranges[i].offset + c_ranges[i].length > orig_data_size) {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_decode invalid range");
        goto error;
      }
      PyList_SetItem(ret_payload, i,
        PY_BUILDVALUE_OBJ_LEN(c_orig_payload + c_ranges[i].offset, c_ranges[i].length));
    }
  }

  goto exit;

error:
  ret_payload = NULL;

exit:
  check_and_free_buffer(c_fragments);
  check_and_free_buffer(c_ranges);
  liberasurecode_decode_cleanup(pyeclib_handle->ec_desc, c_orig_payload);

  return ret_payload;
}


static const char* chksum_type_to_str(uint8_t chksum_type)
{
  const char *chksum_type_str = NULL;
  switch (chksum_type)
  {
    case CHKSUM_NONE:
      chksum_type_str = "none\0";
      break;
    case CHKSUM_CRC32:
      chksum_type_str = "crc32\0";
      break;
    case CHKSUM_MD5:
      chksum_type_str = "md5\0";
      break;
    default:
      chksum_type_str = "unknown\0";
  }

  return chksum_type_str;
}

static int chksum_length(uint8_t chksum_type)
{
  int length = 0;
  switch (chksum_type)
  {
    case CHKSUM_NONE:
      // None
      break;
    case CHKSUM_CRC32:
      // CRC 32
      length = 4;
      break;
    case CHKSUM_MD5:
      // MD5
      length = 16;
      break;
    default:
      length = 0;
  }
  return length;
}

static const char* backend_id_to_str(uint8_t backend_id)
{
  const char *backend_id_str = NULL;
  switch (backend_id)
  {
    case 0:
      backend_id_str = "null\0";
      break;
    case 1:
      backend_id_str = "jerasure_rs_vand\0";
      break;
    case 2:
      backend_id_str = "jerasure_rs_cauchy\0";
      break;
    case 3:
      backend_id_str = "flat_xor_hd\0";
      break;
    case 4:
      backend_id_str = "isa_l_rs_vand\0";
      break;
    case 6:
      backend_id_str = "liberasurecode_rs_vand\0";
      break;
    case 7:
      backend_id_str = "isa_l_rs_cauchy\0";
      break;
    case 8:
      backend_id_str = "libphazr\0";
      break;
    default:
      backend_id_str = "unknown\0";
  }

  return backend_id_str;
}

static char*
hex_encode_string(char *buf, uint32_t buf_len)
{
  char *hex_encoded_buf = (char*)alloc_zeroed_buffer((buf_len * 2) + 1);
  char *hex_encoded_ptr = hex_encoded_buf;
  uint32_t i;

  for (i = 0; i < buf_len; i++) {
    hex_encoded_ptr += sprintf(hex_encoded_ptr, "%.2x", (unsigned char)buf[i]);
  }

  hex_encoded_buf[buf_len * 2] = 0;

  return hex_encoded_buf;
}

static PyObject*
fragment_metadata_to_dict(fragment_metadata_t *fragment_metadata)
{
  const char *chksum_type_str = chksum_type_to_str(fragment_metadata->chksum_type);
  char *encoded_chksum = hex_encode_string((char*)fragment_metadata->chksum,
                                            chksum_length(fragment_metadata->chksum_type));
  const char *backend_id_str = backend_id_to_str(fragment_metadata->backend_id);
  PyObject* metadata_dict = Py_BuildValue(
    "{s:k, s:k, s:K, s:s, s:s, s:B, s:s, s:k}",
    "index", fragment_metadata->idx,
    "size", fragment_metadata->size,
    "orig_data_size", fragment_metadata->orig_data_size,
    "chksum_type", chksum_type_str,
    "chksum", encoded_chksum,
    "chksum_mismatch", fragment_metadata->chksum_mismatch,
    "backend_id", backend_id_str,
    "backend_version", fragment_metadata->backend_version);
  encoded_chksum = check_and_free_buffer(encoded_chksum);
  if (metadata_dict == NULL) {
    pyeclib_c_seterr(-ENOMEM, "fragment_metadata_to_dict");
    return NULL;
  }
  return metadata_dict;
}

/**
 * Obtain the metadata from a fragment.
 *
 * @param pyeclib_obj_handle
 * @param data fragment from user to extract metadata from
 * @param data_len size in bytes of the data fragment
 * @return fragment metadata or NULL on error
 */
static PyObject *
pyeclib_c_get_metadata(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t* pyeclib_handle = NULL;
  char *fragment = NULL;                            /* param, fragment from caller */
  fragment_metadata_t c_fragment_metadata;          /* structure to hold metadata */
  PyObject *fragment_metadata = NULL;               /* metadata object to return */
  Py_ssize_t fragment_len;                          /* fragment length */
  int formatted;                                    /* format the metadata in a dict */
  int ret;

  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, GET_METADATA_ARGS, &pyeclib_obj_handle, &fragment, &fragment_len, &formatted)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_metadata");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_get_metadata");
    return NULL;
  }

  ret = liberasurecode_get_fragment_metadata(fragment, &c_fragment_metadata);

  if (ret < 0) {
    pyeclib_c_seterr(ret, "pyeclib_c_get_metadata");
    fragment_metadata = NULL;
  } else {
    if (formatted) {
      fragment_metadata = fragment_metadata_to_dict(&c_fragment_metadata);
    } else {
      fragment_metadata = PY_BUILDVALUE_OBJ_LEN((char*)&c_fragment_metadata,
                                                  sizeof(fragment_metadata_t));
    }
  }

  return fragment_metadata;
}

/**
 * Confirm the health of the fragment metadata.
 *
 * TODO: Return a list containing tuples (index, problem).  An empty list means
 * everything is OK.
 *
 * @param pyeclib_obj_handle
 * @param fragment_metadata_list list of fragment metadata headers
 * @return dictionary containing 'status', 'reason' and 'bad_fragments', depending
 *         on the status
 *         NULL if the metadata could not be checked by the liberasurecode
 */
static PyObject*
pyeclib_c_check_metadata(PyObject *self, PyObject *args)
{
  PyObject *pyeclib_obj_handle = NULL;
  pyeclib_t *pyeclib_handle = NULL;
  PyObject *fragment_metadata_list = NULL;                /* param, fragment metadata */
  fragment_metadata_t *c_fragment_metadata = NULL;        /* metadata buffer for a single fragment */
  char **c_fragment_metadata_list = NULL;                 /* c version of metadata */
  int num_fragments;                                      /* k + m from EC algorithm */
  int k, m;                                               /* EC algorithm params */
  int size;                                               /* size for buf allocation */
  int ret = -1;                                           /* c return value */
  PyObject *ret_obj = NULL;                               /* python long to return */
  int i = 0;                                              /* a counter */


  /* Obtain and validate the method parameters */
  if (!PyArg_ParseTuple(args, "OO", &pyeclib_obj_handle, &fragment_metadata_list)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_encode");
    return NULL;
  }
  pyeclib_handle = (pyeclib_t*)PyCapsule_GetPointer(pyeclib_obj_handle, PYECC_HANDLE_NAME);
  if (pyeclib_handle == NULL) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_encode");
    return NULL;
  }
  k = pyeclib_handle->ec_args.k;
  m = pyeclib_handle->ec_args.m;
  num_fragments = k + m;
  if (num_fragments != PyList_Size(fragment_metadata_list)) {
    pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_encode");
    return NULL;
  }

  /* Allocate space for fragment signatures */
  size = sizeof(char * ) * num_fragments;
  c_fragment_metadata_list = (char **) alloc_zeroed_buffer(size);
  if (NULL == c_fragment_metadata_list) {
    pyeclib_c_seterr(-ENOMEM, "pyeclib_c_encode");
    goto error;
  }

  /* Populate the metadata into a C array */
  for (i = 0; i < num_fragments; i++) {
    PyObject *tmp_data = PyList_GetItem(fragment_metadata_list, i);
    Py_ssize_t len = 0;
    PyBytes_AsStringAndSize(tmp_data, &(c_fragment_metadata_list[i]), &len);
  }

  ret = liberasurecode_verify_stripe_metadata(pyeclib_handle->ec_desc, c_fragment_metadata_list,
                                              num_fragments);

  ret_obj = PyDict_New();
  if (ret == 0) {
    PyDict_SetItemString(ret_obj, "status", PyLong_FromLong((long)0));
  } else if (ret == -EINVALIDPARAMS) {
    PyDict_SetItemString(ret_obj, "status", PyLong_FromLong((long)-EINVALIDPARAMS));
    PyDict_SetItemString(ret_obj, "reason", PyString_FromString("Invalid arguments"));
    goto error;
  } else if (ret == -EBADCHKSUM) {
    PyDict_SetItemString(ret_obj, "status", PyLong_FromLong((long)-EINVALIDPARAMS));
    PyDict_SetItemString(ret_obj, "reason", PyString_FromString("Bad checksum"));
    PyObject *bad_chksums = PyList_New(0);
    for (i = 0; i < num_fragments; i++) {
      c_fragment_metadata = (fragment_metadata_t*)c_fragment_metadata_list[i];
      if (c_fragment_metadata->chksum_mismatch == 1) {
        PyList_Append(bad_chksums, PyLong_FromLong((long)c_fragment_metadata->idx));
      }
    }
    PyDict_SetItemString(ret_obj, "bad_fragments", bad_chksums);
  }

  goto exit;

error:
  ret_obj = NULL;

exit:
  free(c_fragment_metadata_list);

  return ret_obj;
}

static PyObject*
pyeclib_c_check_backend_available(PyObject *self, PyObject *args)
{
    const ec_backend_id_t backend_id;

    if (!PyArg_ParseTuple(args, "i", &backend_id)) {
        pyeclib_c_seterr(-EINVALIDPARAMS, "pyeclib_c_check_backend_available");
        return NULL;
    }

    if (liberasurecode_backend_available(backend_id)) {
        Py_RETURN_TRUE;
    }

    Py_RETURN_FALSE;
}

static PyObject*
pyeclib_c_liberasurecode_version(PyObject *self, PyObject *args) {
    return Py_BuildValue("k", liberasurecode_get_version());
}

static PyMethodDef PyECLibMethods[] = {
    {"init",  pyeclib_c_init, METH_VARARGS, "Initialize a new erasure encoder/decoder"},
    {"destroy",  pyeclib_c_destroy, METH_O, "Destroy an erasure encoder/decoder"},
    {"encode",  pyeclib_c_encode, METH_VARARGS, "Create parity using source data"},
    {"decode",  pyeclib_c_decode, METH_VARARGS, "Recover all lost data/parity"},
    {"reconstruct",  pyeclib_c_reconstruct, METH_VARARGS, "Recover selective data/parity"},
    {"get_required_fragments", pyeclib_c_get_required_fragments, METH_VARARGS, "Return the fragments required to reconstruct a set of missing fragments"},
    {"get_segment_info", pyeclib_c_get_segment_info, METH_VARARGS, "Return segment and fragment size information needed when encoding a segmented stream"},
    {"get_metadata", pyeclib_c_get_metadata, METH_VARARGS, "Get the integrity checking metadata for a fragment"},
    {"check_metadata", pyeclib_c_check_metadata, METH_VARARGS, "Check the integrity checking metadata for a set of fragments"},
    {"get_liberasurecode_version", pyeclib_c_liberasurecode_version, METH_NOARGS, "Get libersaurecode version in use"},
    {"check_backend_available", pyeclib_c_check_backend_available, METH_VARARGS, "Check if a backend is available"},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

MOD_INIT(pyeclib_c)
{
    PyObject *m;

    MOD_DEF(m, "pyeclib_c", NULL, PyECLibMethods);

    if (m == NULL)
        return MOD_ERROR_VAL;

    #ifdef Py_GIL_DISABLED
        /**
         * Change this to Py_MOD_GIL_NOT_USED when we're convinced that we
         * (and by extension, liberasurecode) won't segfault on free-threaded
         * Python */
        PyUnstable_Module_SetGIL(m, Py_MOD_GIL_USED);
    #endif

    return MOD_SUCCESS_VAL(m);
}
