#include <float.h>

#include "pysearch_results.h"
#include "structmember.h"

#include "chemfp_internal.h"

/************ Search Result type ***************/


/* Help with cyclical garbage collection, in case someone does result.target_ids = result */
static int
SearchResults_traverse(SearchResults *self, visitproc visit, void *arg) {
  Py_VISIT(self->target_ids);
  return 0;
}

static int
SearchResults_clear_memory(SearchResults *self) {
  if (self->results) {
    chemfp_free_results(self->num_results, self->results);
    self->results = NULL;
  }
  self->num_results = 0;
  self->num_columns = 0;

  Py_CLEAR(self->target_ids);
  return 0;
}

static void
SearchResults_dealloc(SearchResults *self) {
  SearchResults_clear_memory(self);
  Py_TYPE(self)->tp_free((PyObject *) self);
}

static PyObject *
SearchResults_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
    SearchResults *self;

    self = (SearchResults *) type->tp_alloc(type, 0);
    if (self == NULL) {
      return NULL;
    }
    self->num_results = 0;
    self->num_columns = 0;
    self->results = NULL;
    Py_INCREF(Py_None);
    self->target_ids = Py_None;
    return (PyObject *)self;
}

static int
SearchResults_init(SearchResults *self, PyObject *args, PyObject *kwds)
{
  int num_results=0, num_columns=0;
  PyObject *target_ids=Py_None;
  chemfp_search_result *results;

  static char *kwlist[] = {"num_results", "num_columns", "target_ids", NULL};

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|O", kwlist, &num_results, &num_columns, &target_ids)) {
    return -1;
  }
  if (num_results < 0) {
    PyErr_SetString(PyExc_ValueError, "num_results must be non-negative");
    return -1;
  }
  if (num_columns < 0) {
    PyErr_SetString(PyExc_ValueError, "num_columns must be non-negative");
    return -1;
  }
  if (num_results == 0) {
    results = NULL;
  } else {
    results = chemfp_alloc_search_results(num_results);
    if (!results) {
      PyErr_NoMemory();
      return -1;
    }
  }

  self->num_results = num_results;
  self->num_columns = num_columns;
  self->results = results;

  Py_XINCREF(target_ids);
  Py_XDECREF(self->target_ids);
  self->target_ids = target_ids;

  return 0;
}

/* len(search_results) */
static Py_ssize_t
SearchResults_length(SearchResults *result) {
  return (Py_ssize_t) result->num_results;
}


static int
check_row(int num_results, int *row) {
  int row_ = *row;
  if (row_ < 0) {
    row_ = num_results + row_;
    if (row_ < 0) {
      PyErr_SetString(PyExc_IndexError, "row index is out of range");
      return 0;
    }
    *row = row_;
  } else if (row_ >= num_results) {
    PyErr_SetString(PyExc_IndexError, "row index is out of range");
    return 0;
  }
  return 1;
}

static int
check_min_max_score(PyObject *min_score_obj, PyObject *max_score_obj,
                    double *min_score, double *max_score) {
  double value;
  if (min_score_obj == Py_None) {
    *min_score = -HUGE_VAL;
  } else {
    value = PyFloat_AsDouble(min_score_obj);
    if (value == -1.0) {
      if (PyErr_Occurred()) {
        return 0;
      }
    }
    *min_score = value;
  }
  if (max_score_obj == Py_None) {
    *max_score = HUGE_VAL;
  } else {
    value = PyFloat_AsDouble(max_score_obj);
    if (value == -1.0) {
      if (PyErr_Occurred()) {
        return 0;
      }
    }
    *max_score = value;
  }
  return 1;
}

static int
check_interval(const char *interval, int *include_min, int *include_max) {
  switch (interval[0]) {
  case '(':
    *include_min = 0;
    break;
  case '[':
    *include_min = 1;
    break;
  default:
      PyErr_SetString(PyExc_ValueError, "First interval character must be '(' or '['");
      return 0;
  }
  switch (interval[1]) {
  case ')':
    *include_max = 0;
    break;
  case ']':
    *include_max = 1;
    break;
  default:
      PyErr_SetString(PyExc_ValueError, "Second interval character must be ')' or ']'");
      return 0;
  }
  if (interval[2]) {
    PyErr_SetString(PyExc_ValueError, "The interval may only contain two characters");
    return 0;
  }
  return 1;
}


static PyObject *
SearchResults_reorder_all(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"order", NULL};
  const char *ordering = "decreasing-score";
  int errval;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s:reorder_all", kwlist, &ordering)) {
    return NULL;
  }
  errval = chemfp_search_results_reorder(self->num_results, self->results, ordering);
  if (errval) {
    PyErr_SetString(PyExc_ValueError, chemfp_strerror(errval));
    return NULL;
  }
  Py_RETURN_NONE;
}

static PyObject *
SearchResults_reorder_row(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", "order", NULL};
  int row=-1;
  int errval;
  const char *ordering = "decreasing-score";
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|s:reorder_row", kwlist, &row, &ordering)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  errval = chemfp_search_result_reorder(self->results+row, ordering);
  if (errval) {
    PyErr_SetString(PyExc_ValueError, chemfp_strerror(errval));
    return NULL;
  }
  Py_RETURN_NONE;
}

#define COUNT_ALL_MACRO(expr)                           \
  for (row=0; row<num_rows; row++) {                    \
    num_hits = chemfp_get_num_hits(self->results+row);  \
    scores = self->results[row].scores;                 \
    for (i=0; i<num_hits; i++) {                        \
      if (expr) {                                       \
        count++;                                        \
      }                                                 \
    }                                                   \
  }

static PyObject *
SearchResults_count_all(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"min_score", "max_score", "interval", NULL};
  int row=-1;
  PyObject *min_score_obj=Py_None, *max_score_obj=Py_None;
  double min_score=0.0, max_score=1.0;
  int num_hits, num_rows;
  double *scores;
  int include_min=0, include_max=0;
  int count=0;
  int i;
  const char *interval = "[]";
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOs:count_all", kwlist,
                                   &min_score_obj, &max_score_obj, &interval)) {
    return NULL;
  }
  if (!check_min_max_score(min_score_obj, max_score_obj, &min_score, &max_score) ||
      !check_interval(interval, &include_min, &include_max)) {
    return NULL;
  }
  if ((min_score > max_score) ||
      (min_score == max_score && !(include_min && include_max))) {
    return PyInt_FromLong(0);
  }

  num_rows = self->num_results;
  if (include_min) {
    if (include_max) {
      /* [] -- Include both ends */
      if (min_score_obj == Py_None) {
        if (max_score_obj == Py_None) {
          /* Special case; just report the number of elements */
          for (row=0; row<num_rows; row++) {
            count += chemfp_get_num_hits(self->results+row);
          }
        } else {
          /* No lower bound */
          COUNT_ALL_MACRO(scores[i] <= max_score);
        }
      } else {
        if (max_score_obj == Py_None) {
          /* Lower bound but no upper bound */
          COUNT_ALL_MACRO(min_score <= scores[i]);
        } else {
          /* Definite lower and upper bound */
          COUNT_ALL_MACRO(min_score <= scores[i] && scores[i] <= max_score);
        }
      }
    } else {
      /* [) -- Include the minimum but not the maximum */
      if (min_score_obj == Py_None) {
        /* There is no minimum */
        COUNT_ALL_MACRO(scores[i] < max_score);
      } else {
        /* There is a minimum and a maximum test */
        COUNT_ALL_MACRO(min_score <= scores[i] && scores[i] < max_score);
      }
    }
  } else {
    if (include_max) {
      /* (] -- Exclude the minimum, include the maximum */
      if (max_score_obj == Py_None) {
        /* No specified max, so must only be greater than the lower bound */
        COUNT_ALL_MACRO(min_score < scores[i]);
      } else {
        COUNT_ALL_MACRO(min_score < scores[i] && scores[i] <= max_score);
      }
    } else {
      /* () -- Exclude the minimum and exclude the maximum */
      COUNT_ALL_MACRO(min_score < scores[i] && scores[i] < max_score);
    }
  }
  return PyInt_FromLong(count);
}

#define COUNT_ROW_MACRO(expr)                         \
  for (i=0; i<num_hits; i++) {                        \
    if (expr) {                                       \
      count++;                                        \
    }                                                 \
  }

static PyObject *
SearchResults_count_row(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", "min_score", "max_score", "interval", NULL};
  int row=-1;
  PyObject *min_score_obj=Py_None, *max_score_obj=Py_None;
  double min_score=0.0, max_score=1.0;
  int num_hits;
  double *scores;
  int include_min=0, include_max=0;
  int count=0;
  int i;
  const char *interval = "[]";
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OOs:count_row", kwlist,
                                   &row, &min_score_obj, &max_score_obj, &interval)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row) ||
      !check_min_max_score(min_score_obj, max_score_obj, &min_score, &max_score) ||
      !check_interval(interval, &include_min, &include_max)) {
    return NULL;
  }
  num_hits = chemfp_get_num_hits(self->results+row);
  scores = (self->results+row)->scores;

  if ((min_score > max_score) ||
      (min_score == max_score && (!include_min || !include_max))) {
    return PyInt_FromLong(0);
  }

  if (include_min) {
    if (include_max) {
      /* [] -- Include both ends */
      if (min_score_obj == Py_None) {
        if (max_score_obj == Py_None) {
          /* Special case; just report the number of elements */
          count = chemfp_get_num_hits(self->results+row);
        } else {
          /* No lower bound */
          COUNT_ROW_MACRO(scores[i] <= max_score);
        }
      } else {
        if (max_score_obj == Py_None) {
          /* Lower bound but no upper bound */
          COUNT_ROW_MACRO(min_score <= scores[i]);
        } else {
          /* Definite lower and upper bound */
          COUNT_ROW_MACRO(min_score <= scores[i] && scores[i] <= max_score);
        }
      }
    } else {
      /* [) -- Include the minimum but not the maximum */
      if (min_score_obj == Py_None) {
        /* There is no minimum */
        COUNT_ROW_MACRO(scores[i] < max_score);
      } else {
        /* There is a minimum and a maximum test */
        COUNT_ROW_MACRO(min_score <= scores[i] && scores[i] < max_score);
      }
    }
  } else {
    if (include_max) {
      /* (] -- Exclude the minimum, include the maximum */
      if (max_score_obj == Py_None) {
        /* No specified max, so must only be greater than the lower bound */
        COUNT_ROW_MACRO(min_score < scores[i]);
      } else {
        COUNT_ROW_MACRO(min_score < scores[i] && scores[i] <= max_score);
      }
    } else {
      /* () -- Exclude the minimum and exclude the maximum */
      COUNT_ROW_MACRO(min_score < scores[i] && scores[i] < max_score);
    }
  }
  return PyInt_FromLong(count);
}

#define CUMULATIVE_SCORE_ALL_MACRO(expr)                \
  for (row=0; row<num_rows; row++) {                    \
    num_hits = chemfp_get_num_hits(self->results+row);  \
    scores = self->results[row].scores;                 \
    for (i=0; i<num_hits; i++) {                        \
      if (expr) {                                       \
        score += scores[i];                             \
      }                                                 \
    }                                                   \
  }

static PyObject *
SearchResults_cumulative_score_all(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"min_score", "max_score", "interval", NULL};
  int row=-1;
  PyObject *min_score_obj=Py_None, *max_score_obj=Py_None;
  double min_score=0.0, max_score=1.0;
  int num_hits, num_rows;
  double *scores;
  int include_min=0, include_max=0;
  double score=0.0;
  int i;
  const char *interval = "[]";
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOs:cumulative_score_all", kwlist,
                                   &min_score_obj, &max_score_obj, &interval)) {
    return NULL;
  }
  if (!check_min_max_score(min_score_obj, max_score_obj, &min_score, &max_score) ||
      !check_interval(interval, &include_min, &include_max)) {
    return NULL;
  }
  if ((min_score > max_score) ||
      (min_score == max_score && !(include_min && include_max))) {
    return PyInt_FromLong(0);
  }

  num_rows = self->num_results;
  if (include_min) {
    if (include_max) {
      /* [] -- Include both ends */
      if (min_score_obj == Py_None) {
        if (max_score_obj == Py_None) {
          CUMULATIVE_SCORE_ALL_MACRO(1);
        } else {
          /* No lower bound */
          CUMULATIVE_SCORE_ALL_MACRO(scores[i] <= max_score);
        }
      } else {
        if (max_score_obj == Py_None) {
          /* Lower bound but no upper bound */
          CUMULATIVE_SCORE_ALL_MACRO(min_score <= scores[i]);
        } else {
          /* Definite lower and upper bound */
          CUMULATIVE_SCORE_ALL_MACRO(min_score <= scores[i] && scores[i] <= max_score);
        }
      }
    } else {
      /* [) -- Include the minimum but not the maximum */
      if (min_score_obj == Py_None) {
        /* There is no minimum */
        CUMULATIVE_SCORE_ALL_MACRO(scores[i] < max_score);
      } else {
        /* There is a minimum and a maximum test */
        CUMULATIVE_SCORE_ALL_MACRO(min_score <= scores[i] && scores[i] < max_score);
      }
    }
  } else {
    if (include_max) {
      /* (] -- Exclude the minimum, include the maximum */
      if (max_score_obj == Py_None) {
        /* No specified max, so must only be greater than the lower bound */
        CUMULATIVE_SCORE_ALL_MACRO(min_score < scores[i]);
      } else {
        CUMULATIVE_SCORE_ALL_MACRO(min_score < scores[i] && scores[i] <= max_score);
      }
    } else {
      /* () -- Exclude the minimum and exclude the maximum */
      CUMULATIVE_SCORE_ALL_MACRO(min_score < scores[i] && scores[i] < max_score);
    }
  }
  return PyFloat_FromDouble(score);
}

#define CUMULATIVE_SCORE_ROW_MACRO(expr)              \
  for (i=0; i<num_hits; i++) {                        \
    if (expr) {                                       \
      score += scores[i];                             \
    }                                                 \
  }


static PyObject *
SearchResults_cumulative_score_row(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", "min_score", "max_score", "interval", NULL};
  int row=-1;
  PyObject *min_score_obj=Py_None, *max_score_obj=Py_None;
  double min_score=0.0, max_score=1.0;
  int num_hits;
  double *scores;
  int include_min=0, include_max=0;
  double score=0.0;
  int i;
  const char *interval = "[]";
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i|OOs:cumulative_score_row", kwlist,
                                   &row, &min_score_obj, &max_score_obj, &interval)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row) ||
      !check_min_max_score(min_score_obj, max_score_obj, &min_score, &max_score) ||
      !check_interval(interval, &include_min, &include_max)) {
    return NULL;
  }
  num_hits = chemfp_get_num_hits(self->results+row);
  scores = (self->results+row)->scores;

  if ((min_score > max_score) ||
      (min_score == max_score && (!include_min || !include_max))) {
    return PyFloat_FromDouble(0.0);
  }

  if (include_min) {
    if (include_max) {
      /* [] -- Include both ends */
      if (min_score_obj == Py_None) {
        if (max_score_obj == Py_None) {
          CUMULATIVE_SCORE_ROW_MACRO(1);
        } else {
          /* No lower bound */
          CUMULATIVE_SCORE_ROW_MACRO(scores[i] <= max_score);
        }
      } else {
        if (max_score_obj == Py_None) {
          /* Lower bound but no upper bound */
          CUMULATIVE_SCORE_ROW_MACRO(min_score <= scores[i]);
        } else {
          /* Definite lower and upper bound */
          CUMULATIVE_SCORE_ROW_MACRO(min_score <= scores[i] && scores[i] <= max_score);
        }
      }
    } else {
      /* [) -- Include the minimum but not the maximum */
      if (min_score_obj == Py_None) {
        /* There is no minimum */
        CUMULATIVE_SCORE_ROW_MACRO(scores[i] < max_score);
      } else {
        /* There is a minimum and a maximum test */
        CUMULATIVE_SCORE_ROW_MACRO(min_score <= scores[i] && scores[i] < max_score);
      }
    }
  } else {
    if (include_max) {
      /* (] -- Exclude the minimum, include the maximum */
      if (max_score_obj == Py_None) {
        /* No specified max, so must only be greater than the lower bound */
        CUMULATIVE_SCORE_ROW_MACRO(min_score < scores[i]);
      } else {
        CUMULATIVE_SCORE_ROW_MACRO(min_score < scores[i] && scores[i] <= max_score);
      }
    } else {
      /* () -- Exclude the minimum and exclude the maximum */
      CUMULATIVE_SCORE_ROW_MACRO(min_score < scores[i] && scores[i] < max_score);
    }
  }
  return PyFloat_FromDouble(score);
}


static PyObject *
SearchResults_clear_all(SearchResults *self) {
  int i;
  for (i=0; i<self->num_results; i++) {
    chemfp_search_result_clear(self->results+i);
  }
  Py_RETURN_NONE;
}

static PyObject *
SearchResults_clear_row(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", NULL};
  int row;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i:clear", kwlist, &row)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  chemfp_search_result_clear(self->results+row);
  Py_RETURN_NONE;
}

static PyObject *
SearchResults_get_indices_and_scores(SearchResults *self, PyObject *args, PyObject *kwds) {
  int n, i;
  PyObject *hits, *obj;
  chemfp_search_result *result;
  static char *kwlist[] = {"row", NULL};
  int row;

  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i:get_indices", kwlist, &row)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  result = self->results+row;
  n = chemfp_get_num_hits(result);
  
  hits = PyList_New(n);
  if (!hits) {
    return NULL;
  }
  for (i=0; i<n; i++) {
    obj = Py_BuildValue("(id)", result->indices[i], result->scores[i]);
    if (!obj) {
      goto error;
    }
    PyList_SET_ITEM(hits, i, obj);
  }
  return hits;

 error:
  /* I could be smarter and only deallocate the ones which I know are present. */
  /* Proving that's correct is a bit trickier than I want to do. */
  for (i=0; i<n; i++) {
    Py_XDECREF(PyList_GET_ITEM(hits, i));
  }
  Py_DECREF(hits);
  return NULL;
}


static PyObject *
new_array(const char *datatype) {
  PyObject *array_module;
  static PyObject *array_new = NULL;
  if (!array_new) {
    array_module = PyImport_ImportModule("array");
    if (!array_module) {
      return NULL;
    }
    array_new = PyObject_GetAttrString(array_module, "array");
    if (!array_new) {
      return NULL;
    }
  }
  return PyObject_CallFunction(array_new, "s", datatype);
}

static PyObject *
data_blob_to_array(int num_hits, void *data_blob, const char *array_type, size_t num_bytes_per_item) {
  PyObject *array=NULL, *ignore=NULL, *fromstring=NULL;
  
  if (!num_hits) {
    /* Special case: return a tuple if there are no hits */
    /* (I'm not sure if this is a good idea, but it's fast. I justify it */
    /* by saying that this function returns back something which supports */
    /* len(), [index], and forward/reverse iteration */
    return PyTuple_New(0);
  }
  array = new_array(array_type);
  if (!array) {
    goto error;
  }
  fromstring = PyObject_GetAttrString(array, "fromstring");
  if (!fromstring) {
    goto error;
  }
  ignore = PyObject_CallFunction(fromstring, "s#", data_blob, num_hits*num_bytes_per_item);
  if (!ignore) {
    goto error;
  }

  Py_DECREF(ignore);
  Py_DECREF(fromstring);
  return array;

 error:
  if (fromstring) {
    Py_DECREF(fromstring);
  }
  if (array) {
    Py_DECREF(array);
  }
  return NULL;
}

static PyObject *
SearchResults_get_indices(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", NULL};
  int row;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i:get_indices", kwlist, &row)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  return data_blob_to_array(chemfp_get_num_hits(self->results+row),
                            self->results[row].indices,
                            "i", sizeof(int));
}

static PyObject *
SearchResults_get_scores(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", NULL};
  int row;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i:get_scores", kwlist, &row)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  return data_blob_to_array(chemfp_get_num_hits(self->results+row),
                            self->results[row].scores,
                            "d", sizeof(double));
}

static PyObject *
SearchResults_size(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", NULL};
  int row;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "i:size", kwlist, &row)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  return PyInt_FromLong(chemfp_get_num_hits(self->results+row));
}


static PyObject *
SearchResults_add_hit(SearchResults *self, PyObject *args, PyObject *kwds) {
  static char *kwlist[] = {"row", "column", "score", NULL};
  int row, column;
  double score;
  if (!PyArg_ParseTupleAndKeywords(args, kwds, "iid:_add_hit", kwlist,
                                   &row, &column, &score)) {
    return NULL;
  }
  if (!check_row(self->num_results, &row)) {
    return NULL;
  }
  if (column >= self->num_columns) {
    self->num_columns = column+1;
  }
  return PyInt_FromLong(chemfp_add_hit(self->results+row, column, score));
  Py_RETURN_NONE;
}


static PyMethodDef SearchResults_methods[] = {
  {"clear_all", (PyCFunction) SearchResults_clear_all, METH_VARARGS | METH_KEYWORDS,
   "Removes all hits from all of the search results"},
  {"_clear_row", (PyCFunction) SearchResults_clear_row, METH_VARARGS | METH_KEYWORDS,
   "(internal) Remove all hits from a given row result"},
  {"cumulative_score_all", (PyCFunction) SearchResults_cumulative_score_all, METH_VARARGS | METH_KEYWORDS,
   "The sum of all scores in all rows which are between `min_score` and `max_score`"},
  {"_cumulative_score_row", (PyCFunction) SearchResults_cumulative_score_row, METH_VARARGS | METH_KEYWORDS,
   "(internal) The sum of the scores which are between `min_score` and `max_score` for a given row"},
  {"count_all", (PyCFunction) SearchResults_count_all, METH_VARARGS | METH_KEYWORDS,
   "Count the number of hits with a score between `min_score` and `max_score`"},
  {"_count_row", (PyCFunction) SearchResults_count_row, METH_VARARGS | METH_KEYWORDS,
   "(internal) Count the number of hits with a score between `min_score` and `max_score` for a given row"},
  {"_get_indices", (PyCFunction) SearchResults_get_indices, METH_VARARGS | METH_KEYWORDS,
   "(internal) The list of target indices, in the current ordering for a given row"},
  {"_get_scores", (PyCFunction) SearchResults_get_scores, METH_VARARGS | METH_KEYWORDS,
   "(internal) The list of target scores, in the current ordering for a given row"},
  {"_get_indices_and_scores", (PyCFunction) SearchResults_get_indices_and_scores, METH_VARARGS | METH_KEYWORDS,
   "(internal) The list of (target index, score) pairs, in the current ordering for a given row"},
  {"_size", (PyCFunction) SearchResults_size, METH_VARARGS | METH_KEYWORDS,
   "(internal) The number of hits for a given row"},
  {"reorder_all", (PyCFunction) SearchResults_reorder_all, METH_VARARGS | METH_KEYWORDS,
   "Reorder the hits for all of the rows based on the requested ordering"},
  {"_reorder_row", (PyCFunction) SearchResults_reorder_row, METH_VARARGS | METH_KEYWORDS,
   "(internal) Reorder the hits based on the requested ordering for a given row"},
  {"_add_hit", (PyCFunction) SearchResults_add_hit, METH_VARARGS | METH_KEYWORDS,
   "(internal) Add a target index and hit score to a given row"},
  {NULL}
};

static PyMemberDef SearchResults_members[] = {
  {"_num_columns", T_INT, offsetof(SearchResults, num_columns), 0,
   "number of columns"},
  {"target_ids", T_OBJECT_EX, offsetof(SearchResults, target_ids), 0,
   "list of fingerprint identifiers"},
  {NULL}
};


static PySequenceMethods SearchResults_as_sequence = {
    (lenfunc)SearchResults_length,                       /* sq_length */
    NULL,       /* sq_concat */
    NULL,       /* sq_repeat */
    NULL,       /* sq_item */
    NULL,       /* sq_slice */
    NULL,       /* sq_ass_item */
    NULL,       /* sq_ass_slice */
    NULL,       /* sq_contains */
    NULL,       /* sq_inplace_concat */
    NULL        /* sq_inplace_repeat */
};



PyTypeObject chemfp_py_SearchResultsType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    "chemfp.search.SearchResults",       /*tp_name*/
    sizeof(SearchResults), /*tp_basicsize*/
    0,                         /*tp_itemsize*/
    (destructor) SearchResults_dealloc,  /*tp_dealloc*/
    0,                         /*tp_print*/
    0,                         /*tp_getattr*/
    0,                         /*tp_setattr*/
    0,                         /*tp_compare*/
    0,                         /*tp_repr*/
    0,                         /*tp_as_number*/
    &SearchResults_as_sequence,          /*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*/
    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC, /*tp_flags*/
    "Documentation goes here",                /* tp_doc */
    (traverseproc) SearchResults_traverse,    /* tp_traverse */
    (inquiry) SearchResults_clear_memory,     /* tp_clear */
    0,		               /* tp_richcompare */
    0,		               /* tp_weaklistoffset */
    0,		               /* tp_iter */
    0,		               /* tp_iternext */
    SearchResults_methods,     /* tp_methods */
    SearchResults_members,     /* tp_members */
    0,                         /* tp_getset */
    0,                         /* tp_base */
    0,                         /* tp_dict */
    0,                         /* tp_descr_get */
    0,                         /* tp_descr_set */
    0,                         /* tp_dictoffset */
    (initproc)SearchResults_init,      /* tp_init */
    0,                         /* tp_alloc */
    SearchResults_new          /* tp_new */
};
