#include "RipTide.h"
#include "ndarray.h"
#include "Sort.h"
#include "MultiKey.h"
#include "MathWorker.h"
#include "Recycler.h"

#define LOGGING(...)
//#define LOGGING printf

#define PLOGGING(...)
//#define PLOGGING printf


//#define TSWAP(A, i, j) { T tmp = A[i]; A[i] = A[j]; A[j] = tmp; }
//
//template <typename T>
//void sort(T* A, int64_t left, int64_t right) {
//   if (right > left) {
//      // Choose outermost elements as pivots
//      if (A[left] > A[right]) TSWAP(A, left, right);
//
//      T p = A[left], q = A[right];
//
//      // Partition A according to invariant below
//      int64_t l = left + 1, g = right - 1, k = l;
//      while (k <= g) {
//         if (A[k] < p) {
//            TSWAP(A, k, l);
//            ++l;
//         }
//         else if (A[k] >= q) {
//            while (A[g] > q && k < g) --g;
//            TSWAP(A, k, g);
//            --g;
//            if (A[k] < p) {
//               TSWAP(A, k, l);
//               ++l;
//            }
//         }
//         ++k;
//      }
//      --l; ++g;
//
//      // Swap pivots to final place
//      TSWAP(A, left, l); TSWAP(A, right, g);
//
//      // Recursively sort partitions
//      sort(A, left, l - 1);
//      sort(A, l + 1, g - 1);
//      sort(A, g + 1, right);
//   }
//}



#define NPY_ENOMEM 1
#define NPY_ECOMP 2

static __inline int npy_get_msb(uint64_t unum)
{
   int depth_limit = 0;
   while (unum >>= 1) {
      depth_limit++;
   }
   return depth_limit;
}

#define SMALL_MERGESORT 16

#define PYA_QS_STACK (NPY_BITSOF_INTP * 2)  // 128
#define SMALL_QUICKSORT 15

#define T_LT(_X_,_Y_) (_X_ < _Y_)

// Anything compared to a nan will return 0
#define FLOAT_LT(_X_,_Y_) (_X_ < _Y_ || (_Y_ != _Y_ && _X_ == _X_))  


__inline bool COMPARE_LT(float X, float Y)   { return (X < Y || (Y != Y && X == X)); }
__inline bool COMPARE_LT(double X, double Y) { return (X < Y || (Y != Y && X == X)); }
__inline bool COMPARE_LT(long double X, long double Y) { return (X < Y || (Y != Y && X == X)); }
__inline bool COMPARE_LT(int32_t X, int32_t Y)   { return (X < Y); }
__inline bool COMPARE_LT(int64_t X, int64_t Y)   { return (X < Y); }
__inline bool COMPARE_LT(uint32_t X, uint32_t Y) { return (X < Y); }
__inline bool COMPARE_LT(uint64_t X, uint64_t Y) { return (X < Y); }
__inline bool COMPARE_LT(int8_t X, int8_t Y) { return (X < Y); }
__inline bool COMPARE_LT(int16_t X, int16_t Y) { return (X < Y); }
__inline bool COMPARE_LT(uint8_t X, uint8_t Y) { return (X < Y); }
__inline bool COMPARE_LT(uint16_t X, uint16_t Y) { return (X < Y); }

NPY_INLINE static int
STRING_LT(const char *s1, const char *s2, size_t len)
{
   const unsigned char *c1 = (unsigned char *)s1;
   const unsigned char *c2 = (unsigned char *)s2;
   size_t i;

   for (i = 0; i < len; ++i) {
      if (c1[i] != c2[i]) {
         return c1[i] < c2[i];
      }
   }
   return 0;
}

//---------------------------------
// Assumes Py_UCS4
// Assumes int is 32bits
NPY_INLINE static int
UNICODE_LT(const char *s1, const char *s2, size_t len)
{
   const unsigned int *c1 = (unsigned int *)s1;
   const unsigned int *c2 = (unsigned int *)s2;
   size_t i;

   size_t lenunicode = len / 4;

   for (i = 0; i < lenunicode; ++i) {
      if (c1[i] != c2[i]) {
         return c1[i] < c2[i];
      }
   }
   return 0;
}


NPY_INLINE static int
VOID_LT(const char *s1, const char *s2, size_t len)
{
   const unsigned char *c1 = (unsigned char *)s1;
   const unsigned char *c2 = (unsigned char *)s2;

   switch (len) {
   case 1:
      if (*c1 != *c2) {
         return *c1 < *c2;
      }
      return 0;
   case 2:
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return *(uint16_t*)c1 < *(uint16_t*)c2;
      }
      return 0;
   case 3:
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return *(uint16_t*)c1 < *(uint16_t*)c2;
      }
      c1 += 2;
      c2 += 2;
      if (*c1 != *c2) {
         return *c1 < *c2;
      }
      return 0;
   case 4:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return *(uint32_t*)c1 < *(uint32_t*)c2;
      }
      return 0;
   case 5:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return *(uint32_t*)c1 < *(uint32_t*)c2;
      }
      c1 += 4;
      c2 += 4;
      if (*c1 != *c2) {
         return *c1 < *c2;
      }
      return 0;
   case 6:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return *(uint32_t*)c1 < *(uint32_t*)c2;
      }
      c1 += 4;
      c2 += 4;
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return *(uint16_t*)c1 < *(uint16_t*)c2;
      }
      return 0;
   case 7:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return *(uint32_t*)c1 < *(uint32_t*)c2;
      }
      c1 += 4;
      c2 += 4;
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return *(uint16_t*)c1 < *(uint16_t*)c2;
      }
      c1 += 2;
      c2 += 2;
      if (*c1 != *c2) {
         return *c1 < *c2;
      }
      return 0;
   case 8:
      if (*(uint64_t*)c1 != *(uint64_t*)c2) {
         return *(uint64_t*)c1 < *(uint64_t*)c2;
      }
      return 0;
   default:
   {
      // compare 8 bytes at a time
      while (len > 8) {
         if (*(uint64_t*)c1 != *(uint64_t*)c2) {
            return *(uint64_t*)c1 < *(uint64_t*)c2;
         }
         c1 += 8;
         c2 += 8;
         len -= 8;
      }
      switch (len) {
      case 1:
         if (*c1 != *c2) {
            return *c1 < *c2;
         }
         return 0;
      case 2:
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return *(uint16_t*)c1 < *(uint16_t*)c2;
         }
         return 0;
      case 3:
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return *(uint16_t*)c1 < *(uint16_t*)c2;
         }
         c1 += 2;
         c2 += 2;
         if (*c1 != *c2) {
            return *c1 < *c2;
         }
         return 0;
      case 4:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return *(uint32_t*)c1 < *(uint32_t*)c2;
         }
         return 0;
      case 5:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return *(uint32_t*)c1 < *(uint32_t*)c2;
         }
         c1 += 4;
         c2 += 4;
         if (*c1 != *c2) {
            return *c1 < *c2;
         }
         return 0;
      case 6:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return *(uint32_t*)c1 < *(uint32_t*)c2;
         }
         c1 += 4;
         c2 += 4;
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return *(uint16_t*)c1 < *(uint16_t*)c2;
         }
         return 0;
      case 7:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return *(uint32_t*)c1 < *(uint32_t*)c2;
         }
         c1 += 4;
         c2 += 4;
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return *(uint16_t*)c1 < *(uint16_t*)c2;
         }
         c1 += 2;
         c2 += 2;
         if (*c1 != *c2) {
            return *c1 < *c2;
         }
         return 0;
      case 8:
         if (*(uint64_t*)c1 != *(uint64_t*)c2) {
            return *(uint64_t*)c1 < *(uint64_t*)c2;
         }
         return 0;
      default:
         return 0;
      }
   }
   }
   return 0;
}



NPY_INLINE static int
BINARY_LT(const char *s1, const char *s2, size_t len)
{
   const unsigned char *c1 = (unsigned char *)s1;
   const unsigned char *c2 = (unsigned char *)s2;

   switch (len) {
   case 1:
      if (*c1 != *c2) {
         return 1;
      }
      return 0;
   case 2:
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return 1;
      }
      return 0;
   case 3:
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return 1;
      }
      c1 += 2;
      c2 += 2;
      if (*c1 != *c2) {
         return 1;
      }
      return 0;
   case 4:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return 1;
      }
      return 0;
   case 5:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return 1;
      }
      c1 += 4;
      c2 += 4;
      if (*c1 != *c2) {
         return 1;
      }
      return 0;
   case 6:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return 1;
      }
      c1 += 4;
      c2 += 4;
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return 1;
      }
      return 0;
   case 7:
      if (*(uint32_t*)c1 != *(uint32_t*)c2) {
         return 1;
      }
      c1 += 4;
      c2 += 4;
      if (*(uint16_t*)c1 != *(uint16_t*)c2) {
         return 1;
      }
      c1 += 2;
      c2 += 2;
      if (*c1 != *c2) {
         return *c1 < *c2;
      }
      return 0;
   case 8:
      if (*(uint64_t*)c1 != *(uint64_t*)c2) {
         return 1;
      }
      return 0;
   default:
   {
      while (len > 8) {
         if (*(uint64_t*)c1 != *(uint64_t*)c2) {
            return 1;
         }
         c1 += 8;
         c2 += 8;
         len -= 8;
      }
      switch (len) {
      case 1:
         if (*c1 != *c2) {
            return 1;
         }
         return 0;
      case 2:
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return 1;
         }
         return 0;
      case 3:
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return 1;
         }
         c1 += 2;
         c2 += 2;
         if (*c1 != *c2) {
            return 1;
         }
         return 0;
      case 4:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return 1;
         }
         return 0;
      case 5:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return 1;
         }
         c1 += 4;
         c2 += 4;
         if (*c1 != *c2) {
            return 1;
         }
         return 0;
      case 6:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return 1;
         }
         c1 += 4;
         c2 += 4;
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return 1;
         }
         return 0;
      case 7:
         if (*(uint32_t*)c1 != *(uint32_t*)c2) {
            return 1;
         }
         c1 += 4;
         c2 += 4;
         if (*(uint16_t*)c1 != *(uint16_t*)c2) {
            return 1;
         }
         c1 += 2;
         c2 += 2;
         if (*c1 != *c2) {
            return 1;
         }
         return 0;
      case 8:
         if (*(uint64_t*)c1 != *(uint64_t*)c2) {
            return 1;
         }
         return 0;
      default:
         return 0;
      }
   }
   }
   return 0;
}


// see: https://github.com/certik/python-3.3/blob/master/Objects/unicodeobject.c
// we will assume
//static int
//unicode_compare(PyObject *str1, PyObject *str2)
//{
//   int kind1, kind2;
//   void *data1, *data2;
//   Py_ssize_t len1, len2, i;
//
//   kind1 = PyUnicode_KIND(str1);
//   kind2 = PyUnicode_KIND(str2);
//   data1 = PyUnicode_DATA(str1);
//   data2 = PyUnicode_DATA(str2);
//   len1 = PyUnicode_GET_LENGTH(str1);
//   len2 = PyUnicode_GET_LENGTH(str2);
//
//   for (i = 0; i < len1 && i < len2; ++i) {
//      Py_UCS4 c1, c2;
//      c1 = PyUnicode_READ(kind1, data1, i);
//      c2 = PyUnicode_READ(kind2, data2, i);
//
//      if (c1 != c2)
//         return (c1 < c2) ? -1 : 1;
//   }
//
//   return (len1 < len2) ? -1 : (len1 != len2);
//}



//NPY_INLINE static int
//FLOAT_LT(npy_float a, npy_float b)
//{
//   return a < b || (b != b && a == a);
//}
//
//
//NPY_INLINE static int
//DOUBLE_LT(npy_double a, npy_double b)
//{
//   return a < b || (b != b && a == a);
//}


#define int32_t_LT(_X_,_Y_) (_X_ < _Y_)
#define int32_t_SWAP(_X_,_Y_) { int32_t temp; temp=_X_; _X_=_Y_; _Y_=temp;}
#define INTP_SWAP(_X_,_Y_) { auto temp=_X_; _X_=_Y_; _Y_=temp;}

//#define T_SWAP(_X_, _Y_) { auto temp;  temp = _X_; _X_ = _Y_; _Y_ = temp; }
#define T_SWAP(_X_, _Y_)  std::swap(_X_,_Y_); 


////--------------------------------------------------------------------------------------
//static int
//orig_quicksort_(int32_t *start, int64_t num)
//{
//   int32_t vp;
//   int32_t *pl = start;
//   int32_t *pr = pl + num - 1;
//   int32_t *stack[PYA_QS_STACK];
//   int32_t **sptr = stack;
//   int32_t *pm, *pi, *pj, *pk;
//   int depth[PYA_QS_STACK];
//   int * psdepth = depth;
//   int cdepth = npy_get_msb(num) * 2;
//
//   for (;;) {
//      if (NPY_UNLIKELY(cdepth < 0)) {
//         orig_heapsort_(pl, pr - pl + 1);
//         goto stack_pop;
//      }
//      while ((pr - pl) > SMALL_QUICKSORT) {
//         /* quicksort partition */
//         pm = pl + ((pr - pl) >> 1);
//         if (int32_t_LT(*pm, *pl)) int32_t_SWAP(*pm, *pl);
//         if (int32_t_LT(*pr, *pm)) int32_t_SWAP(*pr, *pm);
//         if (int32_t_LT(*pm, *pl)) int32_t_SWAP(*pm, *pl);
//         vp = *pm;
//         pi = pl;
//         pj = pr - 1;
//         int32_t_SWAP(*pm, *pj);
//         for (;;) {
//            do ++pi; while (int32_t_LT(*pi, vp));
//            do --pj; while (int32_t_LT(vp, *pj));
//            if (pi >= pj) {
//               break;
//            }
//            int32_t_SWAP(*pi, *pj);
//         }
//         pk = pr - 1;
//         int32_t_SWAP(*pi, *pk);
//         /* push largest partition on stack */
//         if (pi - pl < pr - pi) {
//            *sptr++ = pi + 1;
//            *sptr++ = pr;
//            pr = pi - 1;
//         }
//         else {
//            *sptr++ = pl;
//            *sptr++ = pi - 1;
//            pl = pi + 1;
//         }
//         *psdepth++ = --cdepth;
//      }
//
//      /* insertion sort */
//      for (pi = pl + 1; pi <= pr; ++pi) {
//         vp = *pi;
//         pj = pi;
//         pk = pi - 1;
//         while (pj > pl && int32_t_LT(vp, *pk)) {
//            *pj-- = *pk--;
//         }
//         *pj = vp;
//      }
//   stack_pop:
//      if (sptr == stack) {
//         break;
//      }
//      pr = *(--sptr);
//      pl = *(--sptr);
//      cdepth = *(--psdepth);
//   }
//
//   return 0;
//}
//
//
//static int
//orig_aquicksort_(int32_t *vv, int64_t* tosort, int64_t num)
//{
//   int32_t *v = vv;
//   int32_t vp;
//   int64_t *pl = tosort;
//   int64_t *pr = tosort + num - 1;
//   int64_t *stack[PYA_QS_STACK];
//   int64_t **sptr = stack;
//   int64_t *pm, *pi, *pj, *pk, vi;
//   int depth[PYA_QS_STACK];
//   int * psdepth = depth;
//   int cdepth = npy_get_msb(num) * 2;
//
//   for (;;) {
//      if (NPY_UNLIKELY(cdepth < 0)) {
//         orig_aheapsort_(vv, pl, pr - pl + 1);
//         goto stack_pop;
//      }
//
//      while ((pr - pl) > SMALL_QUICKSORT) {
//         /* quicksort partition */
//         pm = pl + ((pr - pl) >> 1);
//         if (int32_t_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
//         if (int32_t_LT(v[*pr], v[*pm])) INTP_SWAP(*pr, *pm);
//         if (int32_t_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
//         vp = v[*pm];
//         pi = pl;
//         pj = pr - 1;
//         INTP_SWAP(*pm, *pj);
//         for (;;) {
//            do ++pi; while (int32_t_LT(v[*pi], vp));
//            do --pj; while (int32_t_LT(vp, v[*pj]));
//            if (pi >= pj) {
//               break;
//            }
//            INTP_SWAP(*pi, *pj);
//         }
//         pk = pr - 1;
//         INTP_SWAP(*pi, *pk);
//         /* push largest partition on stack */
//         if (pi - pl < pr - pi) {
//            *sptr++ = pi + 1;
//            *sptr++ = pr;
//            pr = pi - 1;
//         }
//         else {
//            *sptr++ = pl;
//            *sptr++ = pi - 1;
//            pl = pi + 1;
//         }
//         *psdepth++ = --cdepth;
//      }
//
//      /* insertion sort */
//      for (pi = pl + 1; pi <= pr; ++pi) {
//         vi = *pi;
//         vp = v[vi];
//         pj = pi;
//         pk = pi - 1;
//         while (pj > pl && int32_t_LT(vp, v[*pk])) {
//            *pj-- = *pk--;
//         }
//         *pj = vi;
//      }
//   stack_pop:
//      if (sptr == stack) {
//         break;
//      }
//      pr = *(--sptr);
//      pl = *(--sptr);
//      cdepth = *(--psdepth);
//   }
//
//   return 0;
//}
//
//
//
//
//
//
//

//-----------------------------------------------------------------------------------------------
template <typename T>
/*static*/ int
heapsort_(T *start, int64_t n)
{
   T     tmp, *a;
   int64_t i, j, l;

   /* The array needs to be offset by one for heapsort indexing */
   a = (T *)start - 1;

   for (l = n >> 1; l > 0; --l) {
      tmp = a[l];
      for (i = l, j = l << 1; j <= n;) {
         if (j < n && T_LT(a[j], a[j + 1])) {
            j += 1;
         }
         if (T_LT(tmp, a[j])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   for (; n > 1;) {
      tmp = a[n];
      a[n] = a[1];
      n -= 1;
      for (i = 1, j = 2; j <= n;) {
         if (j < n && T_LT(a[j], a[j + 1])) {
            j++;
         }
         if (T_LT(tmp, a[j])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   return 0;
}


//-----------------------------------------------------------------------------------------------
template <typename T, typename UINDEX>
static int
aheapsort_(T *vv, UINDEX *tosort, UINDEX n)
{
   T *v = vv;
   UINDEX *a, i, j, l, tmp;
   /* The arrays need to be offset by one for heapsort indexing */
   a = tosort - 1;

   for (l = n >> 1; l > 0; --l) {
      tmp = a[l];
      for (i = l, j = l << 1; j <= n;) {
         if (j < n && T_LT(v[a[j]], v[a[j + 1]])) {
            j += 1;
         }
         if (T_LT(v[tmp], v[a[j]])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   for (; n > 1;) {
      tmp = a[n];
      a[n] = a[1];
      n -= 1;
      for (i = 1, j = 2; j <= n;) {
         if (j < n && T_LT(v[a[j]], v[a[j + 1]])) {
            j++;
         }
         if (T_LT(v[tmp], v[a[j]])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   return 0;
}


//-----------------------------------------------------------------------------------------------
template <typename T, typename UINDEX>
static int
aheapsort_float(T *vv, UINDEX *tosort, UINDEX n)
{
   T *v = vv;
   UINDEX *a, i, j, l, tmp;
   /* The arrays need to be offset by one for heapsort indexing */
   a = tosort - 1;

   for (l = n >> 1; l > 0; --l) {
      tmp = a[l];
      for (i = l, j = l << 1; j <= n;) {
         if (j < n && FLOAT_LT(v[a[j]], v[a[j + 1]])) {
            j += 1;
         }
         if (FLOAT_LT(v[tmp], v[a[j]])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   for (; n > 1;) {
      tmp = a[n];
      a[n] = a[1];
      n -= 1;
      for (i = 1, j = 2; j <= n;) {
         if (j < n && FLOAT_LT(v[a[j]], v[a[j + 1]])) {
            j++;
         }
         if (FLOAT_LT(v[tmp], v[a[j]])) {
            a[i] = a[j];
            i = j;
            j += j;
         }
         else {
            break;
         }
      }
      a[i] = tmp;
   }

   return 0;
}



   //--------------------------------------------------------------------------------------
   // N.B. This function isn't static like the others because it's used in a couple of places in GroupBy.cpp.
   template <typename T>
   /*static*/ int
   quicksort_(T *start, int64_t num)
   {
      T vp;
      T *pl = start;
      T *pr = pl + num - 1;
      T *stack[PYA_QS_STACK];
      T **sptr = stack;
      T *pm, *pi, *pj, *pk;

      int depth[PYA_QS_STACK];
      int * psdepth = depth;
      int cdepth = npy_get_msb(num) * 2;

      for (;;) {
         if (NPY_UNLIKELY(cdepth < 0)) {
            heapsort_<T>(pl, pr - pl + 1);
            goto stack_pop;
         }
         while ((pr - pl) > SMALL_QUICKSORT) {
            /* quicksort partition */
            pm = pl + ((pr - pl) >> 1);
            if (COMPARE_LT(*pm, *pl)) T_SWAP(*pm, *pl);
            if (COMPARE_LT(*pr, *pm)) T_SWAP(*pr, *pm);
            if (COMPARE_LT(*pm, *pl)) T_SWAP(*pm, *pl);
            vp = *pm;
            pi = pl;
            pj = pr - 1;
            T_SWAP(*pm, *pj);
            for (;;) {
               do ++pi; while (COMPARE_LT(*pi, vp));
               do --pj; while (COMPARE_LT(vp, *pj));
               if (pi >= pj) {
                  break;
               }
               T_SWAP(*pi, *pj);
            }
            pk = pr - 1;
            T_SWAP(*pi, *pk);
            /* push largest partition on stack */
            if (pi - pl < pr - pi) {
               *sptr++ = pi + 1;
               *sptr++ = pr;
               pr = pi - 1;
            }
            else {
               *sptr++ = pl;
               *sptr++ = pi - 1;
               pl = pi + 1;
            }
            *psdepth++ = --cdepth;
         }

         /* insertion sort */
         for (pi = pl + 1; pi <= pr; ++pi) {
            vp = *pi;
            pj = pi;
            pk = pi - 1;
            while (pj > pl && COMPARE_LT(vp, *pk)) {
               *pj-- = *pk--;
            }
            *pj = vp;
         }
      stack_pop:
         if (sptr == stack) {
            break;
         }
         pr = *(--sptr);
         pl = *(--sptr);
         cdepth = *(--psdepth);
      }

      return 0;
   }


   //-----------------------------------------------------------------------------------------------
   template <typename T, typename UINDEX>
   static int
   aquicksort_(T *vv, UINDEX* tosort, UINDEX num)
   {
      T *v = vv;
      T vp;
      UINDEX *pl = tosort;
      UINDEX *pr = tosort + num - 1;
      UINDEX *stack[PYA_QS_STACK];
      UINDEX **sptr = stack;
      UINDEX *pm, *pi, *pj, *pk, vi;
      int depth[PYA_QS_STACK];
      int * psdepth = depth;
      int cdepth = npy_get_msb(num) * 2;

      for (;;) {
         if (NPY_UNLIKELY(cdepth < 0)) {
            aheapsort_<T,UINDEX>(vv, pl, (UINDEX)(pr - pl + 1));
            goto stack_pop;
         }

         while ((pr - pl) > SMALL_QUICKSORT) {
            /* quicksort partition */
            pm = pl + ((pr - pl) >> 1);
            if (COMPARE_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
            if (COMPARE_LT(v[*pr], v[*pm])) INTP_SWAP(*pr, *pm);
            if (COMPARE_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
            vp = v[*pm];
            pi = pl;
            pj = pr - 1;
            INTP_SWAP(*pm, *pj);
            for (;;) {
               do ++pi; while (COMPARE_LT(v[*pi], vp));
               do --pj; while (COMPARE_LT(vp, v[*pj]));
               if (pi >= pj) {
                  break;
               }
               INTP_SWAP(*pi, *pj);
            }
            pk = pr - 1;
            INTP_SWAP(*pi, *pk);
            /* push largest partition on stack */
            if (pi - pl < pr - pi) {
               *sptr++ = pi + 1;
               *sptr++ = pr;
               pr = pi - 1;
            }
            else {
               *sptr++ = pl;
               *sptr++ = pi - 1;
               pl = pi + 1;
            }
            *psdepth++ = --cdepth;
         }

         /* insertion sort */
         for (pi = pl + 1; pi <= pr; ++pi) {
            vi = *pi;
            vp = v[vi];
            pj = pi;
            pk = pi - 1;
            while (pj > pl && COMPARE_LT(vp, v[*pk])) {
               *pj-- = *pk--;
            }
            *pj = vi;
         }
      stack_pop:
         if (sptr == stack) {
            break;
         }
         pr = *(--sptr);
         pl = *(--sptr);
         cdepth = *(--psdepth);
      }

      return 0;
   }


   //-----------------------------------------------------------------------------------------------
   template <typename T, typename UINDEX>
   static int
   aquicksort_float(T *vv, UINDEX* tosort, UINDEX num)
   {
      T *v = vv;
      T vp;
      UINDEX *pl = tosort;
      UINDEX *pr = tosort + num - 1;
      UINDEX *stack[PYA_QS_STACK];
      UINDEX **sptr = stack;
      UINDEX *pm, *pi, *pj, *pk, vi;
      int depth[PYA_QS_STACK];
      int * psdepth = depth;
      int cdepth = npy_get_msb(num) * 2;

      for (;;) {
         if (NPY_UNLIKELY(cdepth < 0)) {
            aheapsort_float<T,UINDEX>(vv, pl, (UINDEX)(pr - pl + 1));
            goto stack_pop;
         }

         while ((pr - pl) > SMALL_QUICKSORT) {
            /* quicksort partition */
            pm = pl + ((pr - pl) >> 1);
            if (FLOAT_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
            if (FLOAT_LT(v[*pr], v[*pm])) INTP_SWAP(*pr, *pm);
            if (FLOAT_LT(v[*pm], v[*pl])) INTP_SWAP(*pm, *pl);
            vp = v[*pm];
            pi = pl;
            pj = pr - 1;
            INTP_SWAP(*pm, *pj);
            for (;;) {
               do ++pi; while (FLOAT_LT(v[*pi], vp));
               do --pj; while (FLOAT_LT(vp, v[*pj]));
               if (pi >= pj) {
                  break;
               }
               INTP_SWAP(*pi, *pj);
            }
            pk = pr - 1;
            INTP_SWAP(*pi, *pk);
            /* push largest partition on stack */
            if (pi - pl < pr - pi) {
               *sptr++ = pi + 1;
               *sptr++ = pr;
               pr = pi - 1;
            }
            else {
               *sptr++ = pl;
               *sptr++ = pi - 1;
               pl = pi + 1;
            }
            *psdepth++ = --cdepth;
         }

         /* insertion sort */
         for (pi = pl + 1; pi <= pr; ++pi) {
            vi = *pi;
            vp = v[vi];
            pj = pi;
            pk = pi - 1;
            while (pj > pl && FLOAT_LT(vp, v[*pk])) {
               *pj-- = *pk--;
            }
            *pj = vi;
         }
      stack_pop:
         if (sptr == stack) {
            break;
         }
         pr = *(--sptr);
         pl = *(--sptr);
         cdepth = *(--psdepth);
      }

      return 0;
   }


   //--------------------------------------------------------------------------------------
   template <typename T>
   /*static*/ void 
   mergesort0_(T *pl, T *pr, T *pw)
   {
      T vp, *pi, *pj, *pk, *pm;

      if (pr - pl > SMALL_MERGESORT) {
         /* merge sort */
         pm = pl + ((pr - pl) >> 1);
         mergesort0_(pl, pm, pw);
         mergesort0_(pm, pr, pw);

#ifndef USE_MEMCPY
         memcpy(pw, pl, (pm - pl) * sizeof(T));
#else
         pi = pw;
         pj = pl;
         while (pj < pm) {
            *pi++ = *pj++;
         }
#endif

         pi = pw + (pm - pl);
         pj = pw;
         pk = pl;
         while (pj < pi && pm < pr) {
            if (T_LT(*pm, *pj)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
#ifdef USE_MEMCPY
         diff = pi - pj;
         if (diff > 0) {
            memcpy(pk, pj, sizeof(T)*diff);
            pk += diff;
            pj += diff;
         }
#else
         while (pj < pi) {
            *pk++ = *pj++;
         }
#endif
      }
      else {
         /* insertion sort */
         for (pi = pl + 1; pi < pr; ++pi) {
            vp = *pi;
            pj = pi;
            pk = pi - 1;
            while (pj > pl && T_LT(vp, *pk)) {
               *pj-- = *pk--;
            }
            *pj = vp;
         }
      }
   }


   //-----------------------------------------------------------------------------------------------
   template <typename T>
   /*static*/ int 
   mergesort_(T *start, int64_t num)
   {
      T *pl, *pr, *pw;

      pl = start;
      pr = pl + num;
      pw = (T*)WORKSPACE_ALLOC((num / 2) * sizeof(T));
      if (pw == NULL) {
         return -1;
      }
      mergesort0_(pl, pr, pw);

      WORKSPACE_FREE(pw);
      return 0;
   }



   //-----------------------------------------------------------------------------------------------
   // binary mergesort with no recursion (num must be power of 2)
   // TJD NOTE: Does not appear much faster
   template <typename T>
   static int 
   mergesort_binary_norecursion(T *start, int64_t num)
   {
      T *pl, *pr, *pw, *pm, *pk, *pi, *pj;

      pl = start;
      pr = pl + num;
      pw = (T*)WORKSPACE_ALLOC((num / 2) * sizeof(T));
      if (pw == NULL) {
         return -1;
      }

      T* pEnd;
      
      pEnd = pr - SMALL_MERGESORT;

      while (pl < pEnd) {

         T* pMiddle;
         T  vp;

         pMiddle = pl + SMALL_MERGESORT;

         /* insertion sort */
         for (pi = pl + 1; pi < pMiddle; ++pi) {
            vp = *pi;
            pj = pi;
            pk = pi - 1;
            while (pj > pl && T_LT(vp, *pk)) {
               *pj-- = *pk--;
            }
            *pj = vp;
         }

         pl += SMALL_MERGESORT;

      }

      //-- reamining for insertion sort
      pEnd = pr;
      while (pl < pEnd) {

         T  *pMiddle;
         T  vp;

         pMiddle = pl + SMALL_MERGESORT;

         /* insertion sort */
         for (pi = pl + 1; pi < pMiddle; ++pi) {
            vp = *pi;
            pj = pi;
            pk = pi - 1;
            while (pj > pl && T_LT(vp, *pk)) {
               *pj-- = *pk--;
            }
            *pj = vp;
         }

         pl += SMALL_MERGESORT;

      }

      // START MERGING ----------------------------------
      //----------------------------------------------------------------
      int64_t startSize = SMALL_MERGESORT;

      pEnd = start + num;
      pl = start;

      while ((pl + startSize) < pEnd) {

         while ((pl + startSize) < pEnd) {

            pr = pl + (2 * startSize);
            pm = pl + ((pr - pl) >> 1);

            // memcpy first half into workspace
            memcpy(pw, pl, (pm - pj) * sizeof(T));

            // merge
            pi = pw + (pm - pl);
            pj = pw;
            pk = pl;

            while (pj < pi && pm < pr) {
               if (T_LT(*pm, *pj)) {
                  *pk++ = *pm++;
               }
               else {
                  *pk++ = *pj++;
               }
            }

            // memcpy 
            while (pj < pi) {
               *pk++ = *pj++;
            }

            // move to next segment to merger
            pl = pr;
         }

         // Now merge again
         pl = start;
         startSize <<= 1;
      }

      //printf("%llu  vs %llu\n", startSize, num);
      //mergesort0_(pl, pr, pw);

      WORKSPACE_FREE(pw);
      return 0;
   }



   //--------------------------------------------------------------------------------------
   template <typename T>
   static void 
   mergesort0_float(T *pl, T *pr, T *pw, T* head)
   {
      T vp, *pi, *pj, *pk, *pm;

      if (pr - pl > SMALL_MERGESORT) {
         /* merge sort */
         pm = pl + ((pr - pl) >> 1);
         mergesort0_float(pl, pm, pw, head);
         mergesort0_float(pm, pr, pw, head);


         pi = pw;
         pj = pl;

// TD NOTE: Jan 2018 -- the memcpy improves float sorting slightly, it does not improve INT copying for some reason...
#ifndef USE_MEMCPY
         int64_t diff = pm - pj;
         memcpy(pi, pj, diff * sizeof(T));
         pi += diff;
         pj += diff;
#else
         while (pj < pm) {
            *pi++ = *pj++;
         }
#endif
         pi = pw + (pm - pl);
         pj = pw;
         pk = pl;
         while (pj < pi && pm < pr) {
            if (FLOAT_LT(*pm, *pj)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }

#ifdef USE_MEMCPY
         diff = pi - pj;
         if (diff > 0) {
            memcpy(pk, pj, sizeof(T)*diff);
            pk += diff;
            pj += diff;
         }
#else
         while (pj < pi) {
            *pk++ = *pj++;
         }
#endif
      }
      else {
         /* insertion sort */
         for (pi = pl + 1; pi < pr; ++pi) {
            vp = *pi;
            pj = pi;
            pk = pi - 1;
            while (pj > pl && FLOAT_LT(vp, *pk)) {
               *pj-- = *pk--;
            }
            *pj = vp;
         }
      }
   }


   //-----------------------------------------------------------------------------------------------
   template <typename T>
   static int 
   mergesort_float(T *start, int64_t num)
   {
      T *pl, *pr, *pw;

      pl = start;
      pr = pl + num;
      pw = (T*)WORKSPACE_ALLOC((num / 2) * sizeof(T));
      if (pw == NULL) {
         return -1;
      }
      mergesort0_float(pl, pr, pw, start);

      WORKSPACE_FREE(pw);
      return 0;
   }



   //-----------------------------------------------------------------------------------------------
  template <typename UINDEX>
  static void
  amergesort0_string(UINDEX *pl, UINDEX *pr, const char *strItem, UINDEX *pw, int64_t strlen)
   {
      const char* vp;
      UINDEX vi, *pi, *pj, *pk, *pm;

      if (pr - pl > SMALL_MERGESORT) {
         /* merge sort */
         pm = pl + ((pr - pl) >> 1);
         amergesort0_string(pl, pm, strItem, pw, strlen);
         amergesort0_string(pm, pr, strItem, pw, strlen);
         for (pi = pw, pj = pl; pj < pm;) {
            *pi++ = *pj++;
         }
         pi = pw + (pm - pl);
         pj = pw;
         pk = pl;
         while (pj < pi && pm < pr) {
            if (STRING_LT(strItem + (*pm)*strlen, strItem + (*pj)*strlen, strlen)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
         while (pj < pi) {
            *pk++ = *pj++;
         }
      }
      else {
         /* insertion sort */
         for (pi = pl + 1; pi < pr; ++pi) {
            vi = *pi;
            vp = strItem + (vi*strlen);
            pj = pi;
            pk = pi - 1;
            while (pj > pl && STRING_LT(vp, strItem + (*pk)*strlen, strlen)) {
               *pj-- = *pk--;
            }
            *pj = vi;
         }
      }
   }


  //-----------------------------------------------------------------------------------------------
  template <typename UINDEX>
  static void
  amergesort0_unicode(UINDEX *pl, UINDEX *pr, const char *strItem, UINDEX *pw, int64_t strlen)
  {
     const char* vp;
     UINDEX vi, *pi, *pj, *pk, *pm;

     if (pr - pl > SMALL_MERGESORT) {
        /* merge sort */
        pm = pl + ((pr - pl) >> 1);
        amergesort0_unicode(pl, pm, strItem, pw, strlen);
        amergesort0_unicode(pm, pr, strItem, pw, strlen);
        for (pi = pw, pj = pl; pj < pm;) {
           *pi++ = *pj++;
        }
        pi = pw + (pm - pl);
        pj = pw;
        pk = pl;
        while (pj < pi && pm < pr) {
           if (UNICODE_LT(strItem + (*pm)*strlen, strItem + (*pj)*strlen, strlen)) {
              *pk++ = *pm++;
           }
           else {
              *pk++ = *pj++;
           }
        }
        while (pj < pi) {
           *pk++ = *pj++;
        }
     }
     else {
        /* insertion sort */
        for (pi = pl + 1; pi < pr; ++pi) {
           vi = *pi;
           vp = strItem + (vi*strlen);
           pj = pi;
           pk = pi - 1;
           while (pj > pl && UNICODE_LT(vp, strItem + (*pk)*strlen, strlen)) {
              *pj-- = *pk--;
           }
           *pj = vi;
        }
     }
  }



  //-----------------------------------------------------------------------------------------------
  template <typename UINDEX>
  static void
  amergesort0_void(UINDEX *pl, UINDEX *pr, const char *strItem, UINDEX *pw, int64_t strlen)
  {
     const char* vp;
     UINDEX vi, *pi, *pj, *pk, *pm;

     if (pr - pl > SMALL_MERGESORT) {
        /* merge sort */
        pm = pl + ((pr - pl) >> 1);
        amergesort0_void(pl, pm, strItem, pw, strlen);
        amergesort0_void(pm, pr, strItem, pw, strlen);
        for (pi = pw, pj = pl; pj < pm;) {
           *pi++ = *pj++;
        }
        pi = pw + (pm - pl);
        pj = pw;
        pk = pl;
        while (pj < pi && pm < pr) {
           if (VOID_LT(strItem + (*pm)*strlen, strItem + (*pj)*strlen, strlen)) {
              *pk++ = *pm++;
           }
           else {
              *pk++ = *pj++;
           }
        }
        while (pj < pi) {
           *pk++ = *pj++;
        }
     }
     else {
        /* insertion sort */
        for (pi = pl + 1; pi < pr; ++pi) {
           vi = *pi;
           vp = strItem + (vi*strlen);
           pj = pi;
           pk = pi - 1;
           while (pj > pl && VOID_LT(vp, strItem + (*pk)*strlen, strlen)) {
              *pj-- = *pk--;
           }
           *pj = vi;
        }
     }
  }


   //-----------------------------------------------------------------------------------------------
   template <typename T, typename UINDEX> 
   static void
   // T= data type == float32/float64
   // UINDEX = int32_t or int64_t
   amergesort0_float(UINDEX *pl, UINDEX *pr, T *v, UINDEX *pw, int64_t strlen=0)
   {
      T vp;
      UINDEX vi, *pi, *pj, *pk, *pm;

      //PLOGGING("merging %llu bytes of data at %p\n", pr - pl, v);

      if (pr - pl > SMALL_MERGESORT) {
         /* merge sort */
         pm = pl + ((pr - pl) >> 1);
         amergesort0_float(pl, pm, v, pw);
         amergesort0_float(pm, pr, v, pw);

         //if (COMPARE_LT(v[*pm], v[*(pm - 1)])) {
         {
            // MERGES DATA, memcpy
#ifdef USE_MEMCPY
            //int64_t bytescopy = (pm - pl) * sizeof(UINDEX);
            //aligned_block_copy_backwards((int64_t*)pw, (int64_t*)pl, bytescopy);
            //   
            memcpy(pw, pl, (pm-pl) * sizeof(UINDEX));

            //pi = pw +(pm-pl) -1;
            //pj = pl + (pm-pl) -1;
            //while (pj >= pl) {
            //   *pi-- = *pj--;
            //}

#else
         // Copy left side into workspace
            pi = pw;
            pj = pl;
            while (pj < pm) {
               *pi++ = *pj++;
            }
#endif
            // merge
            pi = pw + (pm - pl);
            pj = pw;
            pk = pl;

            while (pj < pi && pm < pr) {
               if (COMPARE_LT(v[*pm], v[*pj])) {
                  *pk++ = *pm++;
               }
               else {
                  *pk++ = *pj++;
               }
            }
            while (pj < pi) {
               *pk++ = *pj++;
            }
         }
      }
      else {
         /* insertion sort */
         for (pi = pl + 1; pi < pr; ++pi) {
            vi = *pi;
            vp = v[vi];
            pj = pi;
            pk = pi - 1;
            while (pj > pl && COMPARE_LT(vp, v[*pk])) {
               *pj-- = *pk--;
            }
            *pj = vi;
         }
      }
   }


   //-----------------------------------------------------------------------------------------------
   // T= data type == int16,int32,uint32,int64.uint64
   // UINDEX = int32_t or int64_t
   template <typename T, typename UINDEX>
   static void 
   amergesort0_(UINDEX *pl, UINDEX *pr, T *v, UINDEX *pw)
   {
      T vp;
      UINDEX vi, *pi, *pj, *pk, *pm;

      if (pr - pl > SMALL_MERGESORT) {
         /* merge sort */
         pm = pl + ((pr - pl) >> 1);
         amergesort0_(pl, pm, v, pw);
         amergesort0_(pm, pr, v, pw);

         // check if already sorted
         // if the first element on the right is less than the last element on the left
         //printf("comparing %d to %d ", (int)pm[0], (int)pm[-1]);
         if (T_LT(v[*pm], v[*(pm-1)])) {

            for (pi = pw, pj = pl; pj < pm;) {
               *pi++ = *pj++;
            }
            pi = pw + (pm - pl);
            pj = pw;
            pk = pl;
            while (pj < pi && pm < pr) {
               if (T_LT(v[*pm], v[*pj])) {
                  *pk++ = *pm++;
               }
               else {
                  *pk++ = *pj++;
               }
            }
            while (pj < pi) {
               *pk++ = *pj++;
            }
         }

      }
      else {
         /* insertion sort */
         for (pi = pl + 1; pi < pr; ++pi) {
            vi = *pi;
            vp = v[vi];
            pj = pi;
            pk = pi - 1;
            while (pj > pl && T_LT(vp, v[*pk])) {
               *pj-- = *pk--;
            }
            *pj = vi;
         }
      }
   }



   //-----------------------------------------------------------------------------------------------
   // allocates workspace
   template <typename T, typename UINDEX>
   static int 
   amergesort_(T *v, UINDEX *tosort, UINDEX num)
   {
      UINDEX *pl, *pr, *pworkspace;

      pl = tosort;
      pr = pl + num;

      pworkspace = (UINDEX*)WORKSPACE_ALLOC((num / 2) * sizeof(UINDEX));
      if (pworkspace == NULL) {
         return -1;
      }
      amergesort0_(pl, pr, v, pworkspace);
      WORKSPACE_FREE(pworkspace);

      return 0;
   }


   template <typename T, typename UINDEX>
   static int
   amergesort_float(T *v, UINDEX *tosort, UINDEX num)
   {
      UINDEX *pl, *pr, *pworkspace;

      pl = tosort;
      pr = pl + num;

      pworkspace = (UINDEX*)WORKSPACE_ALLOC((num / 2) * sizeof(UINDEX));
      if (pworkspace == NULL) {
         return -1;
      }
      amergesort0_float(pl, pr, v, pworkspace);
      WORKSPACE_FREE(pworkspace);

      return 0;
   }


   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeString(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      //PLOGGING("string calling with %llu   %p  %p  %p  %p\n", totalLen, pl, pr, pValue, pWorkSpace);
      amergesort0_string(pl, pr, pValue, pWorkSpace, strlen);
   }


   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeUnicode(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      //PLOGGING("unicode calling with %llu   %p  %p  %p  %p\n", totalLen, pl, pr, pValue, pWorkSpace);
      amergesort0_unicode(pl, pr, pValue, pWorkSpace, strlen);
   }

   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeVoid(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      //PLOGGING("void calling with %llu   %p  %p  %p  %p\n", totalLen, pl, pr, pValue, pWorkSpace);
      amergesort0_void(pl, pr, pValue, pWorkSpace, strlen);
   }

   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename T, typename UINDEX>
   static void
   ParMergeNormal(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      T* pValue = (T*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      PLOGGING("normal calling with %llu   %p  %p  %p  %p\n", totalLen, pl, pr, pValue, pWorkSpace);
      amergesort0_(pl, pr, pValue, pWorkSpace);
   }

   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename T, typename UINDEX> 
   static void
   ParMergeFloat(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      T* pValue = (T*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      PLOGGING("float calling with %llu   %p  %p  %p  %p\n", totalLen, pl, pr, pValue, pWorkSpace);
      amergesort0_float(pl, pr, pValue, pWorkSpace);
   }



   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeMergeString(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      UINDEX *pi, *pj, *pk, *pm;
      pm = pl + ((pr - pl) >> 1);


      //if (COMPARE_LT(v[*pm], v[*(pm - 1)])) {

      //   for (pi = pw, pj = pl; pj < pm;) {
      //      *pi++ = *pj++;
      //   }
      //   pi = pw + (pm - pl);
      //   pj = pw;
      //   pk = pl;
      //   while (pj < pi && pm < pr) {
      //      if (COMPARE_LT(v[*pm], v[*pj])) {
      //         *pk++ = *pm++;
      //      }
      //      else {
      //         *pk++ = *pj++;
      //      }
      //   }
      //   while (pj < pi) {
      //      *pk++ = *pj++;
      //   }
      //}

      // BUG BUG doing lexsort on two arrays: string, int.  Once sorted, resorting does not work.
      if (true || STRING_LT(pValue + (*pm)*strlen, pValue + (*pm-1)*strlen, strlen)) {

         //printf("%lld %lld %lld %lld\n", (int64_t)pValue[*pl], (int64_t)pValue[*(pm - 1)], (int64_t)pValue[*pm], (int64_t)pValue[*(pr - 1)]);
         //printf("%lld %lld %lld %lld %lld\n", (int64_t)*pl, (int64_t)*(pm - 2), (int64_t)*(pm - 1), (int64_t)*pm, (int64_t)*(pr - 1));

         memcpy(pWorkSpace, pl, (pm - pl) * sizeof(UINDEX));

         // pi is end of workspace
         pi = pWorkSpace + (pm - pl);
         pj = pWorkSpace;
         pk = pl;

         while (pj < pi && pm < pr) {
            if (STRING_LT(pValue +(*pm)*strlen, pValue + (*pj)*strlen, strlen)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
         while (pj < pi) {
            *pk++ = *pj++;
         }

         //printf("last items %lld %lld %lld\n", pk[-3], pk[-2], pk[-1]);
      }
      else {
         PLOGGING("**Already sorted string %lld\n", (int64_t)(*pm));

      }

   }



   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeMergeUnicode(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      UINDEX *pi, *pj, *pk, *pm;
      pm = pl + ((pr - pl) >> 1);

      if (true || UNICODE_LT(pValue + (*pm)*strlen, pValue + (*pm - 1)*strlen, strlen)) {

         //printf("%lld %lld %lld %lld\n", (int64_t)pValue[*pl], (int64_t)pValue[*(pm - 1)], (int64_t)pValue[*pm], (int64_t)pValue[*(pr - 1)]);
         //printf("%lld %lld %lld %lld %lld\n", (int64_t)*pl, (int64_t)*(pm - 2), (int64_t)*(pm - 1), (int64_t)*pm, (int64_t)*(pr - 1));

         memcpy(pWorkSpace, pl, (pm - pl) * sizeof(UINDEX));

         // pi is end of workspace
         pi = pWorkSpace + (pm - pl);
         pj = pWorkSpace;
         pk = pl;

         while (pj < pi && pm < pr) {
            if (UNICODE_LT(pValue + (*pm)*strlen, pValue + (*pj)*strlen, strlen)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
         while (pj < pi) {
            *pk++ = *pj++;
         }

         //printf("last items %lld %lld %lld\n", pk[-3], pk[-2], pk[-1]);
      }
      else {
         PLOGGING("**Already sorted unicode %lld\n", (int64_t)(*pm));


      }

   }

   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   template <typename UINDEX>
   static void
   ParMergeMergeVoid(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      const char* pValue = (char*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      UINDEX *pi, *pj, *pk, *pm;
      pm = pl + ((pr - pl) >> 1);

      // BUG BUG doing lexsort on two arrays: string, int.  Once sorted, resorting does not work.
      if (true || VOID_LT(pValue + (*pm)*strlen, pValue + (*pm - 1)*strlen, strlen)) {

         memcpy(pWorkSpace, pl, (pm - pl) * sizeof(UINDEX));

         // pi is end of workspace
         pi = pWorkSpace + (pm - pl);
         pj = pWorkSpace;
         pk = pl;

         while (pj < pi && pm < pr) {
            if (VOID_LT(pValue + (*pm)*strlen, pValue + (*pj)*strlen, strlen)) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
         while (pj < pi) {
            *pk++ = *pj++;
         }

         //printf("last items %lld %lld %lld\n", pk[-3], pk[-2], pk[-1]);
      }
      else {
         PLOGGING("**Already sorted void %lld\n", (int64_t)(*pm));

      }

   }


   //---------------------------------------------------------------------------
   // Called to combine the result of the left and right merge
   // T is type to sort -- int32_t, Float64, etc.
   // UINDEX is the argsort index -- int32_t or int64_t often
   //
   template <typename T, typename UINDEX>
   static void
   ParMergeMerge(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1) {
      UINDEX *pl, *pr;

      UINDEX* pWorkSpace = (UINDEX*)pWorkSpace1;
      UINDEX* pToSort = (UINDEX*)pToSort1;
      T* pValue = (T*)pValue1;

      pl = pToSort;
      pr = pl + totalLen;

      UINDEX *pi, *pj, *pk, *pm;
      pm = pl + ((pr - pl) >> 1);


      //if (COMPARE_LT(v[*pm], v[*(pm - 1)])) {

      //   for (pi = pw, pj = pl; pj < pm;) {
      //      *pi++ = *pj++;
      //   }
      //   pi = pw + (pm - pl);
      //   pj = pw;
      //   pk = pl;
      //   while (pj < pi && pm < pr) {
      //      if (COMPARE_LT(v[*pm], v[*pj])) {
      //         *pk++ = *pm++;
      //      }
      //      else {
      //         *pk++ = *pj++;
      //      }
      //   }
      //   while (pj < pi) {
      //      *pk++ = *pj++;
      //   }
      //}

      // quickcheck to see if we have to copy
      // BUG BUG doing lexsort on two arrays: string, int.  Once sorted, resorting does not work.
      if (true || COMPARE_LT(pValue[*pm], pValue[*(pm - 1)])) {

         //printf("%lld %lld %lld %lld\n", (int64_t)pValue[*pl], (int64_t)pValue[*(pm - 1)], (int64_t)pValue[*pm], (int64_t)pValue[*(pr - 1)]);
         //printf("%lld %lld %lld %lld %lld\n", (int64_t)*pl, (int64_t)*(pm - 2), (int64_t)*(pm - 1), (int64_t)*pm, (int64_t)*(pr - 1));

         memcpy(pWorkSpace, pl, (pm - pl) * sizeof(UINDEX));

         // pi is end of workspace
         pi = pWorkSpace + (pm - pl);
         pj = pWorkSpace;
         pk = pl;

         while (pj < pi && pm < pr) {
            if (COMPARE_LT(pValue[*pm], pValue[*pj])) {
               *pk++ = *pm++;
            }
            else {
               *pk++ = *pj++;
            }
         }
         while (pj < pi) {
            *pk++ = *pj++;
         }

         //printf("last items %lld %lld %lld\n", pk[-3], pk[-2], pk[-1]);
      }
      else {
         PLOGGING("**Already sorted %lld\n", (int64_t)(*pm));
      }

   }


   typedef void(*MERGE_STEP_ONE)(void* pValue, void* pToSort, int64_t num, int64_t strlen, void* pWorkSpace);
   typedef void(*MERGE_STEP_TWO)(void* pValue1, void* pToSort1, int64_t totalLen, int64_t strlen, void* pWorkSpace1);
   //--------------------------------------------------------------------
   struct MERGE_STEP_ONE_CALLBACK {
      MERGE_STEP_ONE MergeCallbackOne;
      MERGE_STEP_TWO MergeCallbackTwo;

      void* pValues;
      void* pToSort;
      int64_t ArrayLength;

      // set to 0 if not a string, otherwise the string length
      int64_t StrLen;

      void* pWorkSpace;
      int64_t MergeBlocks;
      int64_t TypeSizeInput;
      int64_t TypeSizeOutput;

      // used to synchronize parallel merges
      int64_t EndPositions[8];
      int64_t Level[3];

   } stParMergeCallback;



   //------------------------------------------------------------------------------
   //  Concurrent callback from multiple threads
   static bool ParMergeThreadCallback(struct stMATH_WORKER_ITEM* pstWorkerItem, int core, int64_t workIndex) {
      MERGE_STEP_ONE_CALLBACK* Callback = (MERGE_STEP_ONE_CALLBACK*)pstWorkerItem->WorkCallbackArg;
      bool didSomeWork = false;

      int64_t index;
      int64_t workBlock;


      // As long as there is work to do
      while ((index = pstWorkerItem->GetNextWorkIndex(&workBlock)) > 0) {
         // First index is 1 so we subtract
         index--;

         //PLOGGING("[%d] DoWork start loop -- %lld  index: %lld\n", core, workIndex, index);

         // the very first index starts at 0
         int64_t pFirst = 0;
         
         if (index >= 1) {
            pFirst = Callback->EndPositions[index - 1];
         }

         int64_t pSecond = Callback->EndPositions[index];
         char* pToSort1 = (char*)(Callback->pToSort);

         int64_t MergeSize = (pSecond - pFirst);
         int64_t OffsetSize = pFirst;
         PLOGGING("%d : MergeOne index: %llu  %lld  %lld  %lld\n", core, index, pFirst, MergeSize, OffsetSize);

         // Workspace uses half the size
         //char* pWorkSpace1 = (char*)pWorkSpace + (offsetAdjToSort / 2);
         char* pWorkSpace1 = (char*)Callback->pWorkSpace + (OffsetSize * Callback->TypeSizeOutput);

         Callback->MergeCallbackOne(Callback->pValues, pToSort1 + (OffsetSize * Callback->TypeSizeOutput), MergeSize, Callback->StrLen, pWorkSpace1);

         int64_t bitshift = 1LL << index;
         
         // Now find the buddy bit (adjacent bit)
         int64_t buddy = 0;
         if (index & 1) {
            buddy = 1LL << (index-1);
         }
         else {
            buddy = 1LL << (index + 1);
         }

         // Get back which bits were set before the OR operation
         int64_t result = FMInterlockedOr(&Callback->Level[0], bitshift);

         // Check if our buddy was already set
         PLOGGING("index: %lld  %lld %lld -- %s\n", index, buddy, (result & buddy), buddy == (result & buddy) ? "GOOD" : "WAIT");

         if (buddy == (result & buddy)) {
            // Move to next level -- 4 things to sort
            index = index / 2;
            index = index * 2;
            index += 1;

            pFirst = 0;

            if (index >= 2) {
               pFirst = Callback->EndPositions[index - 2];
            }

            pSecond = Callback->EndPositions[index];
            pToSort1 = (char*)(Callback->pToSort);
            MergeSize = (pSecond - pFirst);
            OffsetSize = pFirst;

            PLOGGING("%d : MergeTwo index: %llu  %lld  %lld  %lld\n", core, index, pFirst, MergeSize, OffsetSize);
            pWorkSpace1 = (char*)Callback->pWorkSpace + (OffsetSize * Callback->TypeSizeOutput);
            Callback->MergeCallbackTwo(Callback->pValues, pToSort1 + (OffsetSize * Callback->TypeSizeOutput), MergeSize, Callback->StrLen, pWorkSpace1);

            index /= 2;
            bitshift = 1LL << index;

            // Now find the buddy bit (adjacent bit)
            buddy = 0;
            if (index & 1) {
               buddy = 1LL << (index - 1);
            }
            else {
               buddy = 1LL << (index + 1);
            }

            // Get back which bits were set before the OR operation
            result = FMInterlockedOr(&Callback->Level[1], bitshift);

            // Check if our buddy was already set
            PLOGGING("index -- LEVEL 2: %lld  %lld %lld -- %s\n", index, buddy, (result & buddy), buddy == (result & buddy) ? "GOOD" : "WAIT");

            if (buddy == (result & buddy)) {
               // Move to next level -- 2 things to sort
               index = index / 2;
               index = index * 4;
               index += 3;

               pFirst = 0;

               if (index >= 4) {
                  pFirst = Callback->EndPositions[index - 4];
               }

               pSecond = Callback->EndPositions[index];
               pToSort1 = (char*)(Callback->pToSort);
               MergeSize = (pSecond - pFirst);
               OffsetSize = pFirst;

               PLOGGING("%d : MergeThree index: %llu  %lld  %lld  %lld\n", core, index, pFirst, MergeSize, OffsetSize);
               pWorkSpace1 = (char*)Callback->pWorkSpace + (OffsetSize * Callback->TypeSizeOutput);
               Callback->MergeCallbackTwo(Callback->pValues, pToSort1 + (OffsetSize * Callback->TypeSizeOutput), MergeSize, Callback->StrLen, pWorkSpace1);


               index /= 4;
               bitshift = 1LL << index;

               // Now find the buddy bit (adjacent bit)
               buddy = 0;
               if (index & 1) {
                  buddy = 1LL << (index - 1);
               }
               else {
                  buddy = 1LL << (index + 1);
               }

               // Get back which bits were set before the OR operation
               result = FMInterlockedOr(&Callback->Level[2], bitshift);

               if (buddy == (result & buddy)) {
                  // Final merge
                  PLOGGING("%d : MergeFinal index: %llu  %lld  %lld  %lld\n", core, index, 0LL, Callback->ArrayLength, 0LL);
                  stParMergeCallback.MergeCallbackTwo(Callback->pValues, Callback->pToSort, Callback->ArrayLength, Callback->StrLen, Callback->pWorkSpace);
               }

            }

         }

         // Indicate we completed a block
         didSomeWork = true;

         // tell others we completed this work block
         pstWorkerItem->CompleteWorkBlock();
      }

      return didSomeWork;
   }

   //========================================================================
   //
   enum PAR_SORT_TYPE {
      Normal =0,
      Float = 1,
      String = 2,
      Unicode = 3,
      Void = 4
   };

   typedef int(*SINGLE_MERGESORT)(
      void*          pValuesT,
      void*          pToSortU,
      int64_t          arrayLength,
      int64_t          strlen,
      PAR_SORT_TYPE  sortType);
   
   //------------------------------------------------------------------------
   // single threaded version
   // T is the dtype int32/float32/float64/etc.
   // UINDEX is either int32_t or int64_t
   // Returns -1 on failure
   template <typename T, typename UINDEX>
   static int
   single_amergesort(
      void*          pValuesT,
      void*          pToSortU,
      int64_t          arrayLength,
      int64_t          strlen,
      PAR_SORT_TYPE  sortType)
   {
      T*             pValues = (T*)pValuesT;
      UINDEX*        pToSort = (UINDEX*)pToSortU;

      // single threaded sort
      UINDEX *pWorkSpace;

      pWorkSpace = (UINDEX*)WORKSPACE_ALLOC((arrayLength / 2) * sizeof(UINDEX));
      if (pWorkSpace == NULL) {
         return -1;
      }

      switch (sortType) {
      case PAR_SORT_TYPE::Float:
         amergesort0_float(pToSort, pToSort + arrayLength, pValues, pWorkSpace);
         break;
      case PAR_SORT_TYPE::String:
         amergesort0_string(pToSort, pToSort + arrayLength, (const char*)pValues, pWorkSpace, strlen);
         break;
      case PAR_SORT_TYPE::Unicode:
         amergesort0_unicode(pToSort, pToSort + arrayLength, (const char*)pValues, pWorkSpace, strlen);
         break;
      case PAR_SORT_TYPE::Void:
         amergesort0_void(pToSort, pToSort + arrayLength, (const char*)pValues, pWorkSpace, strlen);
         break;
      default:
         amergesort0_(pToSort, pToSort + arrayLength, pValues, pWorkSpace);
      }

      WORKSPACE_FREE(pWorkSpace);
      return 0;
   }


   //------------------------------------------------------------------------
   // parallel version
   // if strlen==0, then not string (int or float)
   // If pCutOffs is not null, will go parallel per partition
   // If pCutOffs is null, the entire array is sorted
   // If the array is large enough, a parallel merge sort is invoked
   // Returns -1 on failure
   template <typename T, typename UINDEX> 
   static int
   par_amergesort(
      int64_t*         pCutOffs,      // May be NULL (if so no partitions)
      int64_t          cutOffLength,

      T*             pValues,
      UINDEX*        pToSort, 

      int64_t          arrayLength,
      int64_t          strlen, 
      PAR_SORT_TYPE  sortType)
   {
      if (pCutOffs) {
         PLOGGING("partition version col: %lld  %p  %p  %p\n", cutOffLength, pToSort, pToSort + arrayLength, pValues);

         struct stPSORT {
            SINGLE_MERGESORT  funcSingleMerge;
            int64_t*            pCutOffs;       // May be NULL (if so no partitions)
            int64_t             cutOffLength;

            char*             pValues;
            char*             pToSort;
            int64_t             strlen;
            PAR_SORT_TYPE     sortType;

            int64_t             sizeofT;
            int64_t             sizeofUINDEX;

         } psort;

         psort.funcSingleMerge = single_amergesort<T,UINDEX>;
         psort.pCutOffs = pCutOffs;
         psort.cutOffLength = cutOffLength;
         psort.pValues = (char*)pValues;
         psort.pToSort = (char*)pToSort;
         psort.strlen = strlen;
         psort.sortType = sortType;

         psort.sizeofUINDEX = sizeof(UINDEX);
         if (strlen > 0) {
            psort.sizeofT = strlen;
         }
         else {
            psort.sizeofT = sizeof(T);
         }

         // Use threads per partition
         auto lambdaPSCallback = [](void* callbackArgT, int core, int64_t workIndex) -> bool {
            stPSORT* callbackArg = (stPSORT*)callbackArgT;
            int64_t t = workIndex;
            int64_t partLength;
            int64_t partStart;

            if (t == 0) {
               partStart = 0;
            }
            else {
               partStart = callbackArg->pCutOffs[t - 1];
            }

            partLength = callbackArg->pCutOffs[t] - partStart;

            PLOGGING("[%lld] start: %lld  length: %lld\n", t, partStart, partLength);

            // shift the data pointers to match the partition
            // call a single threaded merge
            callbackArg->funcSingleMerge(
               callbackArg->pValues + (partStart * callbackArg->sizeofT),
               callbackArg->pToSort + (partStart * callbackArg->sizeofUINDEX),
               partLength,
               callbackArg->strlen,
               callbackArg->sortType);

            return true;
         };

         g_cMathWorker->DoMultiThreadedWork((int)cutOffLength, lambdaPSCallback, &psort);

      } else

      // If size is large, go parallel
      if (arrayLength >= CMathWorker::WORK_ITEM_BIG) {

         PLOGGING("Parallel version  %p  %p  %p\n", pToSort,  pToSort + arrayLength, pValues);
         // Divide into 8 jobs
         // Allocate all memory up front?
         //UINDEX* pWorkSpace = (UINDEX*)WORKSPACE_FREE(((arrayLength / 2) + 256)* sizeof(UINDEX));
         void* pWorkSpace = NULL;
         uint64_t allocSize = arrayLength * sizeof(UINDEX);

         //(UINDEX*)WORKSPACE_ALLOC(arrayLength * sizeof(UINDEX));
         pWorkSpace=WorkSpaceAllocLarge(allocSize);

         if (pWorkSpace == NULL) {
            return -1;
         }

         MERGE_STEP_ONE mergeStepOne = NULL;
         
         switch (sortType) {
         case PAR_SORT_TYPE::Float:
            mergeStepOne = ParMergeFloat<T, UINDEX>;
            break;
         case PAR_SORT_TYPE::String:
            mergeStepOne = ParMergeString<UINDEX>;
            break;
         case PAR_SORT_TYPE::Unicode:
            mergeStepOne = ParMergeUnicode<UINDEX>;
            break;
         case PAR_SORT_TYPE::Void:
            mergeStepOne = ParMergeVoid<UINDEX>;
            break;
         default:
            mergeStepOne = ParMergeNormal<T, UINDEX>;
         }

         stMATH_WORKER_ITEM* pWorkItem = g_cMathWorker->GetWorkItem(arrayLength);

         if (pWorkItem == NULL) {

            // Threading not allowed for this work item, call it directly from main thread
            mergeStepOne(pValues, pToSort, arrayLength, strlen, pWorkSpace);
         }
         else {

            pWorkItem->DoWorkCallback = ParMergeThreadCallback;
            pWorkItem->WorkCallbackArg = &stParMergeCallback;

            stParMergeCallback.MergeCallbackOne = mergeStepOne;
            switch (sortType) {

            case PAR_SORT_TYPE::String:
               stParMergeCallback.MergeCallbackTwo = ParMergeMergeString< UINDEX>;
               break;
            case PAR_SORT_TYPE::Unicode:
               stParMergeCallback.MergeCallbackTwo = ParMergeMergeUnicode< UINDEX>;
               break;
            case PAR_SORT_TYPE::Void:
               stParMergeCallback.MergeCallbackTwo = ParMergeMergeVoid< UINDEX>;
               break;
            default:
               // Last Merge
               stParMergeCallback.MergeCallbackTwo = ParMergeMerge<T, UINDEX>;
            };


            stParMergeCallback.pValues = pValues;
            stParMergeCallback.pToSort = pToSort;
            stParMergeCallback.ArrayLength = arrayLength;
            stParMergeCallback.StrLen = strlen;
            stParMergeCallback.pWorkSpace = pWorkSpace;
            stParMergeCallback.TypeSizeInput = sizeof(T);
            if (strlen) {
               stParMergeCallback.TypeSizeInput = strlen;
            }
            stParMergeCallback.TypeSizeOutput = sizeof(UINDEX);

            //NOTE set this value to 2,4 or 8
            stParMergeCallback.MergeBlocks = 8;

            for (int i = 0; i < 3; i++) {
               stParMergeCallback.Level[i] = 0;
            }

            if (stParMergeCallback.MergeBlocks == 2) {

               stParMergeCallback.EndPositions[1] = arrayLength;
               stParMergeCallback.EndPositions[0] = arrayLength / 2;
            }
            else if (stParMergeCallback.MergeBlocks == 4) {

               stParMergeCallback.EndPositions[3] = arrayLength;
               stParMergeCallback.EndPositions[1] = arrayLength / 2;
               stParMergeCallback.EndPositions[2] = stParMergeCallback.EndPositions[1] + (stParMergeCallback.EndPositions[3] - stParMergeCallback.EndPositions[1]) / 2;
               stParMergeCallback.EndPositions[0] = 0 + (stParMergeCallback.EndPositions[1] - 0) / 2;
            }

            else {
               // We use an 8 way merge, we need the size breakdown
               stParMergeCallback.EndPositions[7] = arrayLength;
               stParMergeCallback.EndPositions[3] = arrayLength / 2;
               stParMergeCallback.EndPositions[5] = stParMergeCallback.EndPositions[3] + (stParMergeCallback.EndPositions[7] - stParMergeCallback.EndPositions[3]) / 2;
               stParMergeCallback.EndPositions[1] = 0 + (stParMergeCallback.EndPositions[3] - 0) / 2;
               stParMergeCallback.EndPositions[6] = stParMergeCallback.EndPositions[5] + (stParMergeCallback.EndPositions[7] - stParMergeCallback.EndPositions[5]) / 2;
               stParMergeCallback.EndPositions[4] = stParMergeCallback.EndPositions[3] + (stParMergeCallback.EndPositions[5] - stParMergeCallback.EndPositions[3]) / 2;
               stParMergeCallback.EndPositions[2] = stParMergeCallback.EndPositions[1] + (stParMergeCallback.EndPositions[3] - stParMergeCallback.EndPositions[1]) / 2;
               stParMergeCallback.EndPositions[0] = 0 + (stParMergeCallback.EndPositions[1] - 0) / 2;
            }

            // This will notify the worker threads of a new work item
            g_cMathWorker->WorkMain(pWorkItem, stParMergeCallback.MergeBlocks, 0, 1, false);

         }

         // Free temp memory used
         WorkSpaceFreeAllocLarge(pWorkSpace, allocSize);

      }
      else {

         // single threaded sort
         return   
         single_amergesort<T, UINDEX>(
            pValues,
            pToSort,
            arrayLength,
            strlen,
            sortType);
      }

      return 0;
   }




   //-----------------------------------------------------------------------------------------------
   // Sorts in place
   // TODO: Make multithreaded like
   template <typename T>
   static int
   SortInPlace(void* pDataIn1, int64_t arraySize1, SORT_MODE mode) {

      int result = 0;

      switch (mode) {
      case SORT_MODE::SORT_MODE_QSORT:
         result = quicksort_((T*)pDataIn1, arraySize1);
         break;

      case SORT_MODE::SORT_MODE_MERGE:
         result = mergesort_((T*)pDataIn1, arraySize1);
         break;

      case SORT_MODE::SORT_MODE_HEAP:
         result = heapsort_<T>((T*)pDataIn1, arraySize1);
         break;

      }

      if (result != 0) {
         printf("**Error sorting.  size %llu   mode %d\n", arraySize1, mode);
      }

      return result;
   }



   //-----------------------------------------------------------------------------------------------
   // Sorts in place
   template <typename T>
   static int
   SortInPlaceFloat(void* pDataIn1, int64_t arraySize1, SORT_MODE mode) {

      int result = 0;

      switch (mode) {
      case SORT_MODE::SORT_MODE_QSORT:
         result = quicksort_((T*)pDataIn1, arraySize1);
         break;

      case SORT_MODE::SORT_MODE_MERGE:
         result = mergesort_float((T*)pDataIn1, arraySize1);
         break;

      case SORT_MODE::SORT_MODE_HEAP:
         result = heapsort_<T>((T*)pDataIn1, arraySize1);
         break;

      }

      if (result != 0) {
         printf("**Error sorting.  size %llu   mode %d\n", (int64_t)arraySize1, mode);
      }

      return result;
   }

   //-----------------------------------------------------------------------------------------------
   template <typename T, typename UINDEX>
   static int
   SortIndex(
      int64_t* pCutOffs, int64_t cutOffLength, void* pDataIn1, UINDEX* toSort, UINDEX arraySize1, SORT_MODE mode) {

      int result = 0;

      switch (mode) {
      case SORT_MODE::SORT_MODE_QSORT:
         result = aquicksort_<T,UINDEX>((T*)pDataIn1, (UINDEX*)toSort, arraySize1);
         break;

      //case SORT_MODE::SORT_MODE_MERGE:
      //   result = amergesort_<T, UINDEX>((T*)pDataIn1, (UINDEX*)toSort, arraySize1);
      //   break;
      case SORT_MODE::SORT_MODE_MERGE:
         result = par_amergesort<T, UINDEX>(pCutOffs, cutOffLength, (T*)pDataIn1, (UINDEX*)toSort, arraySize1, 0, PAR_SORT_TYPE::Normal);
         break;

      case SORT_MODE::SORT_MODE_HEAP:
         result = aheapsort_<T, UINDEX>((T*)pDataIn1, (UINDEX*)toSort, arraySize1);
         break;

      }

      if (result != 0) {
         printf("**Error sorting.  size %llu   mode %d\n", (int64_t)arraySize1, mode);
      }

      return result;
   }



   //-----------------------------------------------------------------------------------------------
   template <typename T, typename UINDEX>
   static int
   SortIndexFloat(int64_t* pCutOffs, int64_t cutOffLength, void* pDataIn1, UINDEX* toSort, UINDEX arraySize1, SORT_MODE mode) {

      int result = 0;

      switch (mode) {
      case SORT_MODE::SORT_MODE_QSORT:
         result = aquicksort_float<T, UINDEX>((T*)pDataIn1, (UINDEX*)toSort, arraySize1);
         break;

      case SORT_MODE::SORT_MODE_MERGE:
         result = par_amergesort<T, UINDEX>(pCutOffs, cutOffLength, (T*)pDataIn1, (UINDEX*)toSort, arraySize1, 0, PAR_SORT_TYPE::Float);
         break;

      case SORT_MODE::SORT_MODE_HEAP:
         result = aheapsort_float<T, UINDEX>((T*)pDataIn1, (UINDEX*)toSort, arraySize1);
         break;

      }

      if (result != 0) {
         printf("**Error sorting.  size %llu   mode %d\n", (int64_t)arraySize1, mode);
      }

      return result;
   }


   //-----------------------------------------------------------------------------------------------
   template <typename UINDEX>
   static int
   SortIndexString(int64_t* pCutOffs, int64_t cutOffLength, const char* pDataIn1, UINDEX* toSort, UINDEX arraySize1, SORT_MODE mode, UINDEX strlen) {

      int result = 0;
      switch (mode) {
      default:
      case SORT_MODE::SORT_MODE_MERGE:
         result = par_amergesort<char, UINDEX>(pCutOffs, cutOffLength, (char*)pDataIn1, (UINDEX*)toSort, arraySize1, strlen, PAR_SORT_TYPE::String);
         break;

      }

      return result;
   }


   //-----------------------------------------------------------------------------------------------
   template <typename UINDEX>
   static int
   SortIndexUnicode(int64_t* pCutOffs, int64_t cutOffLength, const char* pDataIn1, UINDEX* toSort, UINDEX arraySize1, SORT_MODE mode, UINDEX strlen) {

      int result = 0;
      switch (mode) {
      default:
      case SORT_MODE::SORT_MODE_MERGE:
         result = par_amergesort<char, UINDEX>(pCutOffs, cutOffLength, (char*)pDataIn1, (UINDEX*)toSort, arraySize1, strlen, PAR_SORT_TYPE::Unicode);
         break;

      }

      return result;
   }

   //-----------------------------------------------------------------------------------------------
   template <typename UINDEX>
   static int
   SortIndexVoid(int64_t* pCutOffs, int64_t cutOffLength, const char* pDataIn1, UINDEX* toSort, UINDEX arraySize1, SORT_MODE mode, UINDEX strlen) {

      int result = 0;
      switch (mode) {
      default:
      case SORT_MODE::SORT_MODE_MERGE:
         result = par_amergesort<char, UINDEX>(pCutOffs, cutOffLength, (char*)pDataIn1, (UINDEX*)toSort, arraySize1, strlen, PAR_SORT_TYPE::Void);
         break;
      }

      return result;
   }




//================================================================================================
//===============================================================================
   static void SortArray(void* pDataIn1, int64_t arraySize1, int32_t arrayType1, SORT_MODE mode) {
      switch (arrayType1) {
      case NPY_STRING:
         SortInPlace<char>(pDataIn1, arraySize1, mode);
         break;
      case NPY_BOOL:
         SortInPlace<bool>(pDataIn1, arraySize1, mode);
         break;
      case NPY_INT8:
         SortInPlace<int8_t>(pDataIn1, arraySize1, mode);
         break;
      case NPY_INT16:
         SortInPlace<int16_t>(pDataIn1, arraySize1, mode);
         break;
      CASE_NPY_INT32:
         SortInPlace<int32_t>(pDataIn1, arraySize1, mode);
         break;
      CASE_NPY_INT64:
         SortInPlace<int64_t>(pDataIn1, arraySize1, mode);
         break;
      case NPY_UINT8:
         SortInPlace<uint8_t>(pDataIn1, arraySize1, mode);
         break;
      case NPY_UINT16:
         SortInPlace<uint16_t>(pDataIn1, arraySize1, mode);
         break;
      CASE_NPY_UINT32:
         SortInPlace<uint32_t>(pDataIn1, arraySize1, mode);
         break;
      CASE_NPY_UINT64:
         SortInPlace<uint64_t>(pDataIn1, arraySize1, mode);
         break;
      case NPY_FLOAT:
         SortInPlaceFloat<float>(pDataIn1, arraySize1, mode);
         break;
      case NPY_DOUBLE:
         SortInPlaceFloat<double>(pDataIn1, arraySize1, mode);
         break;
      case NPY_LONGDOUBLE:
         SortInPlaceFloat<long double>(pDataIn1, arraySize1, mode);
         break;
      default:
         printf("SortArray does not understand type %d\n", arrayType1);
         break;
      }
   }


//===============================================================================
// Pass in a numpy array, it will be sorted in place
// the same array is returned
// TODO: Make multithreaded
PyObject* SortInPlace(PyObject *self, PyObject *args) {
   PyArrayObject *inArr1 = NULL;
   int sortMode;

   if (!PyArg_ParseTuple(args, "O!i", &PyArray_Type, &inArr1, &sortMode)) return NULL;

   int32_t arrayType1 = PyArray_TYPE(inArr1);
   int ndim = PyArray_NDIM(inArr1);
   npy_intp* dims = PyArray_DIMS(inArr1);

   int64_t arraySize1 = CalcArrayLength(ndim, dims);
   void* pDataIn1 = PyArray_BYTES(inArr1);

   //printf("Arary size %llu  type %d\n", arraySize1, arrayType1);

   SORT_MODE mode = (SORT_MODE)sortMode;

   SortArray(pDataIn1, arraySize1, arrayType1, mode);

   Py_IncRef((PyObject*)inArr1);
   return SetFastArrayView(inArr1);
}


//===============================================================================
// Pass in a numpy array, it will be sorted in place
// the same array is returned
// Arg1: iKey (array of uniques)
// Arg2: iUniqueSort (resorting of iUnique)
//
// NOTE: every element of iKey must be an index into iUniqueSort (the max element = max(iUniqueSort)-1)
// Output:
//   Arg1 = iUniqueSort[Arg1[i]]
PyObject* SortInPlaceIndirect(PyObject *self, PyObject *args) {
   PyArrayObject *inArr1 = NULL;
   PyArrayObject *inSort = NULL;
   
   // THIS CODE IS NOT FINSIHED

   if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &inArr1, &PyArray_Type, &inSort)) return NULL;

   int32_t arrayType1 = PyArray_TYPE(inArr1);
   int32_t sortType = PyArray_TYPE(inSort);

   int64_t arraySize1 = ArrayLength(inArr1);
   int64_t sortSize = ArrayLength(inSort);


   if (arrayType1 == NPY_INT32 && sortType == NPY_INT32) {
      int32_t* pDataIn = (int32_t*)PyArray_BYTES(inArr1);
      int32_t* pSort = (int32_t*)PyArray_BYTES(inSort);

      int32_t* inverseSort = (int32_t*)WORKSPACE_ALLOC(sortSize * sizeof(int32_t));
      for (int i = 0; i < sortSize; i++) {
         inverseSort[pSort[i]] = i;
      }

      for (int i = 0; i < arraySize1; i++) {
         pDataIn[i] = inverseSort[pDataIn[i]];
      }

      WORKSPACE_FREE(inverseSort);
   }
   else if (arrayType1 == NPY_INT32 && sortType == NPY_INT64) {
      int32_t* pDataIn = (int32_t*)PyArray_BYTES(inArr1);
      int64_t* pSort = (int64_t*)PyArray_BYTES(inSort);

      int64_t* inverseSort = (int64_t*)WORKSPACE_ALLOC(sortSize * sizeof(int64_t));
      for (int64_t i = 0; i < sortSize; i++) {
         inverseSort[pSort[i]] = i;
      }

      for (int i = 0; i < arraySize1; i++) {
         pDataIn[i] = (int32_t)inverseSort[pDataIn[i]];
      }
      WORKSPACE_FREE(inverseSort);

   } else if (arrayType1 == NPY_INT64 && sortType == NPY_INT64) {
      int64_t* pDataIn = (int64_t*)PyArray_BYTES(inArr1);
      int64_t* pSort = (int64_t*)PyArray_BYTES(inSort);

      int64_t* inverseSort = (int64_t*)WORKSPACE_ALLOC(sortSize * sizeof(int64_t));
      for (int64_t i = 0; i < sortSize; i++) {
         inverseSort[pSort[i]] = i;
      }

      for (int64_t i = 0; i < arraySize1; i++) {
         pDataIn[i] = inverseSort[pDataIn[i]];
      }
      WORKSPACE_FREE(inverseSort);

   }
   else {
      printf("**SortInplaceIndirect failure!  arrays must be int32 or int64\n");
   }

   Py_IncRef((PyObject*)inArr1);
   return SetFastArrayView(inArr1);
}


//===============================================================================
// Pass in a numpy array, it will be copied and sorted in the return array
// the same array is returned
PyObject* Sort(PyObject *self, PyObject *args) {
   PyArrayObject *inArr1 = NULL;
   int sortMode;

   if (!PyArg_ParseTuple(args, "O!i", &PyArray_Type, &inArr1, &sortMode)) return NULL;

   int32_t arrayType1 = PyArray_TYPE(inArr1);
   int ndim = PyArray_NDIM(inArr1);
   npy_intp* dims = PyArray_DIMS(inArr1);

   int64_t arraySize1 = CalcArrayLength(ndim, dims);

   // The output is a boolean where the nth item was found
   PyArrayObject* duplicateArray = AllocateNumpyArray(ndim, dims, arrayType1);

   if (duplicateArray == NULL) {
      PyErr_Format(PyExc_ValueError, "Sort out of memory");
      return NULL;
   }
   
   void* pDataIn1 = PyArray_BYTES(inArr1);
   void* pDataOut1 = PyArray_BYTES(duplicateArray);

   int64_t itemSize = NpyItemSize((PyObject*)inArr1);

   memcpy(pDataOut1, pDataIn1, arraySize1 * itemSize);

   SORT_MODE mode = (SORT_MODE)sortMode;

   SortArray(pDataOut1, arraySize1, arrayType1, mode);

   return SetFastArrayView(duplicateArray);
}

//------------------------------------------------------------------------------------------
// Internal and can be called from groupby
// caller must allocate the pDataOut1 as int64_t with size arraySize1
// UINDEX = int32_t or int64_t
template <typename UINDEX>
static void SortIndex(
   int64_t*      pCutOffs,
   int64_t       cutOffLength,

   void*       pDataIn1,
   UINDEX      arraySize1, 
   UINDEX*     pDataOut1, 
   SORT_MODE   mode, 
   int32_t       arrayType1, 
   UINDEX      strlen) {

   switch (arrayType1) {
   case NPY_UNICODE:
      SortIndexUnicode<UINDEX>(pCutOffs, cutOffLength, (const char*)pDataIn1, pDataOut1, arraySize1, mode, strlen);
      break;
   case NPY_VOID:
      SortIndexVoid<UINDEX>(pCutOffs, cutOffLength, (const char*)pDataIn1, pDataOut1, arraySize1, mode, strlen);
      break;
   case NPY_STRING:
      SortIndexString<UINDEX>(pCutOffs, cutOffLength, (const char*)pDataIn1, pDataOut1, arraySize1, mode, strlen);
      break;
   case NPY_BOOL:
   case NPY_INT8:
      SortIndex<int8_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_INT16:
      SortIndex<int16_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   CASE_NPY_INT32:
      SortIndex<int32_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   CASE_NPY_INT64:
      SortIndex<int64_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_UINT8:
      SortIndex<uint8_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_UINT16:
      SortIndex<uint16_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   CASE_NPY_UINT32:
      SortIndex<uint32_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   CASE_NPY_UINT64:
      SortIndex<uint64_t, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_FLOAT:
      SortIndexFloat<float, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_DOUBLE:
      SortIndexFloat<double, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   case NPY_LONGDOUBLE:
      SortIndexFloat<long double, UINDEX>(pCutOffs, cutOffLength, pDataIn1, pDataOut1, arraySize1, mode);
      break;
   default:
      printf("SortIndex does not understand type %d\n", arrayType1);
   }

}

//================================================================================
typedef int(*IS_SORTED_FUNC)(const char* pDataIn1, int64_t arraySize1, int64_t strlennotused);
//-----------------------------------------------------------------------------------------------
template <typename T>
static int
IsSortedFloat(const char* pDataIn1, int64_t arraySize1, int64_t strlennotused) {

   int result = 0;
   T* pData = (T*)pDataIn1;

   int64_t i = arraySize1 - 1;

   while ((i > 0) && (pData[i] != pData[i])) {
      i--;
   }

   while ((i > 0) && pData[i] >= pData[i - 1]) {
      i--;
   }

   return i <= 0;
}


//-----------------------------------------------------------------------------------------------
template <typename T>
static int
IsSorted(const char* pDataIn1, int64_t arraySize1, int64_t strlennotused) {

   int result = 0;
   T* pData = (T*)pDataIn1;

   int64_t i = arraySize1 - 1;

   while ((i > 0) && pData[i] >= pData[i - 1]) {
      i--;
   }

   return i <= 0;
}


//-----------------------------------------------------------------------------------------------
static int
IsSortedString(const char* pData, int64_t arraySize1, int64_t strlen) {

   int result = 0;

   int64_t i = arraySize1 - 1;

   while ((i > 0) && !(STRING_LT(&pData[i*strlen], &pData[(i - 1)*strlen], strlen))) {
      i--;
   }

   return i <= 0;
}


//-----------------------------------------------------------------------------------------------
static int
IsSortedUnicode(const char* pData, int64_t arraySize1, int64_t strlen) {

   int result = 0;

   int64_t i = arraySize1 - 1;

   while ((i > 0) && !(UNICODE_LT(&pData[i*strlen], &pData[(i - 1)*strlen], strlen))) {
      i--;
   }

   return i <= 0;
}

//-----------------------------------------------------------------------------------------------
static int
IsSortedVoid(const char* pData, int64_t arraySize1, int64_t strlen) {

   int result = 0;

   int64_t i = arraySize1 - 1;

   while ((i > 0) && !(VOID_LT(&pData[i*strlen], &pData[(i - 1)*strlen], strlen))) {
      i--;
   }

   return i <= 0;
}



//===============================================================================
// returns True or False
// Nans at the end are fine and still considered sorted
PyObject* IsSorted(PyObject *self, PyObject *args) {
   PyArrayObject *inArr1 = NULL;

   if (!PyArg_ParseTuple(args, "O!", &PyArray_Type, &inArr1)) return NULL;

   int32_t arrayType1 = PyArray_TYPE(inArr1);
   int ndim = PyArray_NDIM(inArr1);
   npy_intp* dims = PyArray_DIMS(inArr1);

   int64_t itemSize = PyArray_ITEMSIZE(inArr1);

   if (ndim != 1 || itemSize != PyArray_STRIDE(inArr1, 0)) {
      PyErr_Format(PyExc_ValueError, "IsSorted arrays must be one dimensional and contiguous.  ndim is %d\n", ndim);
      return NULL;
   }

   int64_t arraySize1 = CalcArrayLength(ndim, dims);
   void* pDataIn1 = PyArray_BYTES(inArr1);

   LOGGING("issorted size %llu  type %d\n", arraySize1, arrayType1);

   int64_t result = 0;
   IS_SORTED_FUNC pSortedFunc = NULL;

   switch (arrayType1) {
   case NPY_BOOL:
   case NPY_INT8:
      pSortedFunc = IsSorted<int8_t>;
      break;
   case NPY_INT16:
      pSortedFunc = IsSorted<int16_t>;
      break;
   CASE_NPY_INT32:
      pSortedFunc = IsSorted<int32_t>;
      break;
   CASE_NPY_INT64:
      pSortedFunc = IsSorted<int64_t>;
      break;
   case NPY_UINT8:
      pSortedFunc = IsSorted<uint8_t>;
      break;
   case NPY_UINT16:
      pSortedFunc = IsSorted<uint16_t>;
      break;
   CASE_NPY_UINT32:
      pSortedFunc = IsSorted<uint32_t>;
      break;
   CASE_NPY_UINT64:
      pSortedFunc = IsSorted<uint64_t>;
      break;
   case NPY_FLOAT:
      pSortedFunc = IsSortedFloat<float>;
      break;
   case NPY_DOUBLE:
      pSortedFunc = IsSortedFloat<double>;
      break;
   case NPY_LONGDOUBLE:
      pSortedFunc = IsSortedFloat<long double>;
      break;
   case NPY_VOID:
      pSortedFunc = IsSortedVoid;
      break;
   case NPY_STRING:
      pSortedFunc = IsSortedString;
      break;
   case NPY_UNICODE:
      pSortedFunc = IsSortedUnicode;
      break;

   default:
      PyErr_Format(PyExc_ValueError, "IsSorted does not understand type %d\n", arrayType1);
      return NULL;
   }


   // MT callback
   struct IsSortedCallbackStruct {
      int64_t             IsSorted;
      IS_SORTED_FUNC    pSortedFunc;
      const char*       pDataIn1;
      int64_t             ArraySize;
      int64_t             ItemSize;
   } stISCallback{ 1, pSortedFunc, (const char*)pDataIn1, arraySize1, itemSize };

   // This is the routine that will be called back from multiple threads
   auto lambdaISCallback = [](void* callbackArgT, int core, int64_t start, int64_t length) -> bool {
      IsSortedCallbackStruct* cb = (IsSortedCallbackStruct*)callbackArgT;

      // check if short circuited (any segment not sorted)
      if (cb->IsSorted) {
         // If not the first segment, then overlap by going back
         if (start != 0) {
            start--;
            length++;
         }
         int result = cb->pSortedFunc(cb->pDataIn1 + (start * cb->ItemSize), length, cb->ItemSize);

         // on success, return true 
         if (result) return true;

         // on failure, set the failure flag and return false
         cb->IsSorted = 0;
      }

      return false;
   };

   // A zero length array is considered sorted
   g_cMathWorker->DoMultiThreadedChunkWork(arraySize1, lambdaISCallback, &stISCallback);

   result = stISCallback.IsSorted;

   if (result) {
      Py_INCREF(Py_True);
      return Py_True;
   }
   else {
      Py_INCREF(Py_False);
      return Py_False;

   }

}


//===============================================================================
// checks for kwargs cutoff
// if exists, and is int64_t, returns pointer and length of cutoffs
// cutoffLength of -1 indicates an error
int64_t* GetCutOffs(PyObject *kwargs, int64_t& cutoffLength) {
   // Check for cutoffs kwarg to see if going into parallel mode
   if (kwargs && PyDict_Check(kwargs)) {
      PyArrayObject* pCutOffs = NULL;
      // Borrowed reference
      // Returns NULL if key not present
      pCutOffs = (PyArrayObject*)PyDict_GetItemString(kwargs, "cutoffs");

      if (pCutOffs != NULL && PyArray_Check(pCutOffs)) {
         switch (PyArray_TYPE(pCutOffs)) {
         CASE_NPY_INT64:
            cutoffLength = ArrayLength(pCutOffs);
            return (int64_t*)PyArray_BYTES(pCutOffs);
         default:
            printf("Bad cutoff dtype... make sure int64_t\n");
            cutoffLength = -1;
            return NULL;
         }
      }
   }
   cutoffLength = 0;
   return NULL;
}

//===============================================================================

template<typename UINDEX>
static bool ARangeCallback(void* callbackArgT, int core, int64_t start, int64_t length) {

   UINDEX* pDataOut = (UINDEX*)callbackArgT;
   UINDEX istart = (UINDEX)start;
   UINDEX iend = istart + (UINDEX)length;

   for (UINDEX i = istart; i < iend; i++) {
      pDataOut[i] = i;
   }

   return true;
}


// index must be int32_t or int64_t
static PyArrayObject* GetKwargIndex(PyObject *kwargs, int64_t& indexLength, int &dtype) {
   // Check for 'index' kwarg to see if prime lexsort
   if (kwargs && PyDict_Check(kwargs)) {
      PyArrayObject* pStartIndex = NULL;
      // Borrowed reference
      // Returns NULL if key not present
      pStartIndex = (PyArrayObject*)PyDict_GetItemString(kwargs, "index");

      if (pStartIndex != NULL && PyArray_Check(pStartIndex)) {
         indexLength = ArrayLength(pStartIndex);

         switch (PyArray_TYPE(pStartIndex)) {
         CASE_NPY_INT64:
            dtype = NPY_INT64;
            return pStartIndex;
         CASE_NPY_INT32:
            dtype = NPY_INT32;
            return pStartIndex;
         default:
            printf("Bad index dtype... make sure int64_t or int32_t\n");
            indexLength = -1;
            return NULL;
         }
      }
   }
   indexLength = 0;
   return NULL;
}



//===============================================================================
// LexSort32 and LexSort64 funnel into here
// Kwargs:
//--------
// cutoffs=
// index=  specify start index instead of doing arange
// groups=True (also group)
// base_index=
// ascending??
//
// Returns
// -------
// Fancy index in desired sort order
// when group=True
// Adds iKey
// Adds iFirstKey
// Add  nCount
// NOTE: From nCount, and iFirstKey can build
//
template<typename UINDEX>
PyObject* LexSort(PyObject *self, PyObject *args, PyObject *kwargs) {
   CMultiListPrepare mlp(args);

   if (mlp.aInfo && mlp.tupleSize > 0) {
      int64_t    arraySize1 = mlp.totalRows;

      int64_t    cutOffLength = 0;
      int64_t*   pCutOffs = GetCutOffs(kwargs, cutOffLength);

      if (pCutOffs && pCutOffs[cutOffLength - 1] != arraySize1) {
         PyErr_Format(PyExc_ValueError, "LexSort last cutoff length does not match array length %lld", arraySize1);
         return NULL;
      }
      if (cutOffLength == -1) {
         PyErr_Format(PyExc_ValueError, "LexSort 'cutoffs' must be an array of type int64_t");
         return NULL;
      }

      int indexDType = 0;
      int64_t indexLength = 0;

      PyArrayObject* index = GetKwargIndex(kwargs, indexLength, indexDType);

      PyArrayObject* result = NULL;
      
      if (indexLength == -1) {
         PyErr_Format(PyExc_ValueError, "LexSort 'index' must be an array of type int64_t or int32_t");
         return NULL;
      }

      if (indexLength > 0) {
         if (indexLength > arraySize1) {
            PyErr_Format(PyExc_ValueError, "LexSort 'index' is larger than value array");
            return NULL;
         }

         if (sizeof(UINDEX) == 8 && indexDType != NPY_INT64) {
            PyErr_Format(PyExc_ValueError, "LexSort 'index' is not int64_t");
            return NULL;
         }

         if (sizeof(UINDEX) == 4 && indexDType != NPY_INT32) {
            PyErr_Format(PyExc_ValueError, "LexSort 'index' is not int32_t");
            return NULL;
         }

         // reduce what we sort to the startindex
         arraySize1 = indexLength;
         result = index;
         Py_IncRef((PyObject*)result);
      }
      else {
         result = AllocateLikeNumpyArray(mlp.aInfo[0].pObject, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);
      }

      if (result) {
         // Return the sorted index
         UINDEX* pDataOut = (UINDEX*)PyArray_BYTES(result);

         // BUG? what if we have index= and cutoffs= ??
         if (pCutOffs) {
            LOGGING("Have cutoffs %lld\n", cutOffLength);

            // For cutoffs, prep the indexes with 0:n for each partition
            UINDEX* pCounter = pDataOut;

            int64_t startPos = 0;
            for (int64_t j = 0; j < cutOffLength; j++) {

               int64_t endPos = pCutOffs[j];
               int64_t partitionLength = endPos - startPos;
               for (UINDEX i = 0; i < partitionLength; i++) {
                  *pCounter++ = i;
               }
               startPos = endPos;
            }
         }
         else {

            // If the user did not provide a start index, we make one
            if (index == NULL) {
               g_cMathWorker->DoMultiThreadedChunkWork(arraySize1, ARangeCallback<UINDEX>, pDataOut);
            }
         }

         if (pCutOffs) {
            // Turn off caching of large memory allocs
            g_cMathWorker->NoCaching = true;
         }

         // When multiple arrays are passed, we sort in order of how it is passed
         // Thus, the last array is the last sort, and therefore determines the primary sort order
         for (UINDEX i = 0; i < mlp.tupleSize; i++) {
            // For each array...
            SortIndex<UINDEX>(pCutOffs, cutOffLength, mlp.aInfo[i].pData, (UINDEX)arraySize1, pDataOut, SORT_MODE::SORT_MODE_MERGE, mlp.aInfo[i].NumpyDType, (UINDEX)mlp.aInfo[i].ItemSize);

         }

         if (pCutOffs) {
            g_cMathWorker->NoCaching = false;
         }
         return (PyObject*)result;
      }
   }

   Py_INCREF(Py_None);
   return Py_None;
}


//===============================================================================
// Returns int32_t
PyObject* LexSort32(PyObject *self, PyObject *args, PyObject *kwargs) {

   return LexSort<int32_t>(self, args, kwargs);
}


//===============================================================================
// Returns int64_t
PyObject* LexSort64(PyObject *self, PyObject *args, PyObject *kwargs) {

   return LexSort<int64_t>(self, args, kwargs);
}


//===============================================================================
//===============================================================================
// checks for kwargs filter
// if exists, and is bool, returns pointer and length of bool
static bool* GetFilter(PyObject *kwargs, int64_t& filterLength) {
   // Check for cutoffs kwarg to see if going into parallel mode
   if (kwargs && PyDict_Check(kwargs)) {
      PyArrayObject* pFilter = NULL;
      // Borrowed reference
      // Returns NULL if key not present
      pFilter = (PyArrayObject*)PyDict_GetItemString(kwargs, "filter");

      if (pFilter != NULL && PyArray_Check(pFilter)) {
         switch (PyArray_TYPE(pFilter)) {
         case NPY_BOOL:
            filterLength = ArrayLength(pFilter);
            return (bool*)PyArray_BYTES(pFilter);
         }
      }
   }
   filterLength = 0;
   return NULL;
}


static int64_t GetBaseIndex(PyObject *kwargs) {
   if (kwargs && PyDict_Check(kwargs)) {
      PyObject* pBaseIndex = NULL;
      // Borrowed reference
      // Returns NULL if key not present
      pBaseIndex = PyDict_GetItemString(kwargs, "base_index");
      if (pBaseIndex != NULL && PyLong_Check(pBaseIndex)) {

         long baseindex = PyLong_AsLong(pBaseIndex);

         // only zero or one allowed
         if (baseindex == 0) return 0;
      }
   }
   return 1;
}


//================================================================================
// Group via lexsort
// CountOut
//================================================================================
//typedef int64_t *(GROUP_INDEX_FUNC)()

template <typename T, typename UINDEX>
static int64_t GroupIndexStep2(
   void*       pDataIn1,
   UINDEX      arraySize1,
   UINDEX*     pDataIndexIn,
   UINDEX*     pGroupOut,
   UINDEX*     pFirstOut,
   UINDEX*     pCountOut,
   bool*       pFilter,
   int64_t       base_index,
   UINDEX      strlen = 0) {

   T*          pDataIn = (T*)pDataIn1;
   UINDEX      curIndex = pDataIndexIn[0];
   UINDEX      curCount = 1;
   T           val1 = pDataIn[curIndex];
   UINDEX      baseIndex = (UINDEX)base_index;
   UINDEX      curGroup = 0;

   // NOTE: filtering does not work!!
   //if (pFilter) {
   //   // base index must be 1
   //   // Invalid bin init
   //   // currently nothing filtered out
   //   pCountOut[0] = 0;

   //   UINDEX invalid = *(UINDEX*)GetInvalid<UINDEX>();

   //   pFirstOut[0] = invalid;
   //   val1 = -1;

   //   // TJD NOTE...
   //   // Think have to see how many filtered out values up front
   //   curCount = 1;

   //   UINDEX zeroCount = 0;

   //   for (UINDEX i = 0; i < arraySize1; i++) {
   //      curIndex = pDataIndexIn[i];

   //      if (pFilter[i]) {
   //         T val2 = pDataIn[curIndex];

   //         if (val1 == val2) {
   //            curCount++;
   //            pGroupOut[curIndex] = curGroup + 1;
   //         }
   //         else {
   //            curGroup++;
   //            pCountOut[curGroup] = curCount;
   //            pFirstOut[curGroup] = curIndex;
   //            pGroupOut[curIndex] = curGroup + 1;
   //            val1 = val2;
   //            curCount = 1;
   //         }
   //      }
   //      else {
   //         zeroCount++;
   //         pGroupOut[curIndex] = 0;
   //         if (pFirstOut[0] == invalid) {
   //            pFirstOut[0] = curIndex;
   //         }
   //      }
   //   }
   //   curGroup++;
   //   pCountOut[curGroup] = curCount;

   //   // the zero count tally
   //   pCountOut[0] = zeroCount;

   //   return curGroup;
   //}


    {
      if (base_index == 0) {
         // SHIFT countout

         // Invalid bin init
         // currently nothing filtered out
         pFirstOut[0] = curIndex;
         pGroupOut[curIndex] = 0;

         for (UINDEX i = 1; i < arraySize1; i++) {
            curIndex = pDataIndexIn[i];
            T val2 = pDataIn[curIndex];

            if (val1 == val2) {
               curCount++;
               pGroupOut[curIndex] = curGroup;
            }
            else {
               pCountOut[curGroup] = curCount;
               curGroup++;
               pFirstOut[curGroup] = curIndex;
               pGroupOut[curIndex] = curGroup;
               val1 = val2;
               curCount = 1;
            }
         }
         pCountOut[curGroup] = curCount;
         curGroup++;
      }
      else {
         // Invalid bin init
         // currently nothing filtered out
         pCountOut[0] = 0;

         pFirstOut[0] = curIndex;
         pGroupOut[curIndex] = 1;

         for (UINDEX i = 1; i < arraySize1; i++) {
            curIndex = pDataIndexIn[i];
            T val2 = pDataIn[curIndex];

            if (val1 == val2) {
               curCount++;
               pGroupOut[curIndex] = curGroup + 1;
            }
            else {
               curGroup++;
               pCountOut[curGroup] = curCount;
               pFirstOut[curGroup] = curIndex;
               pGroupOut[curIndex] = curGroup + 1;
               val1 = val2;
               curCount = 1;
            }
         }
         curGroup++;
         pCountOut[curGroup] = curCount;
      }
      return curGroup;
   }
}


template <typename T, typename UINDEX>
static int64_t GroupIndexStep2String(
   void*       pDataIn1,
   UINDEX      arraySize1,
   UINDEX*     pDataIndexIn,
   UINDEX*     pGroupOut,
   UINDEX*     pFirstOut,
   UINDEX*     pCountOut,
   bool*       pFilter,
   int64_t       base_index,
   int64_t       strlen) {

   T*          pDataIn = (T*)pDataIn1;
   UINDEX      curIndex = pDataIndexIn[0];
   UINDEX      curCount = 1;
   T*          val1 = &pDataIn[curIndex*strlen];
   UINDEX      baseIndex = (UINDEX)base_index;
   UINDEX      curGroup = 0;

   // Invalid bin init when base_index is 1
   pCountOut[0] = 0;

   pFirstOut[0] = curIndex;
   pGroupOut[curIndex] = baseIndex;

   for (UINDEX i = 1; i < arraySize1; i++) {
      curIndex = pDataIndexIn[i];
      T* val2 = &pDataIn[curIndex*strlen];

      if (BINARY_LT(val1, val2, strlen) == 0) {
         curCount++;
         pGroupOut[curIndex] = curGroup + baseIndex;
      }
      else {
         curGroup++;
         pCountOut[curGroup] = curCount;
         pFirstOut[curGroup] = curIndex;
         pGroupOut[curIndex] = curGroup + baseIndex;
         val1 = val2;
         curCount = 1;
      }
   }
   curGroup++;
   pCountOut[curGroup] = curCount;
   return curGroup;
}

typedef int64_t(*GROUP_INDEX_FUNC)(
   void*       pDataIn1,
   int64_t       arraySize1V,
   void*       pDataIndexInV,
   void*       pGroupOutV,
   void*       pFirstOutV,
   void*       pCountOutV,
   bool*       pFilter,       // optional
   int64_t       base_index,
   int64_t       strlen);


//------------------------------------------------------------------------------------------
// Internal and can be called from groupby
// caller must allocate the pGroupOut as int32_t or int64_t with size arraySize1
// UINDEX = int32_t or int64_t
template <typename UINDEX>
static int64_t GroupIndex(
   void*       pDataIn1,
   int64_t       arraySize1V,
   void*       pDataIndexInV,
   void*       pGroupOutV,
   void*       pFirstOutV,
   void*       pCountOutV,
   bool*       pFilter,       // optional
   int64_t       base_index,
   int64_t       strlen) {

   int64_t       uniqueCount = 0;

   UINDEX*     pDataIndexIn = (UINDEX*)pDataIndexInV;
   UINDEX*     pGroupOut = (UINDEX*)pGroupOutV;
   UINDEX*     pFirstOut = (UINDEX*)pFirstOutV;
   UINDEX*     pCountOut = (UINDEX*)pCountOutV;
   UINDEX      arraySize1 = (UINDEX)arraySize1V;

   switch (strlen) {
   case 1:
      uniqueCount =
         GroupIndexStep2<int8_t, UINDEX>(pDataIn1, arraySize1, pDataIndexIn, pGroupOut, pFirstOut, pCountOut, pFilter, base_index, 0);
      break;
   case 2:
      uniqueCount =
         GroupIndexStep2<int16_t, UINDEX>(pDataIn1, arraySize1, pDataIndexIn, pGroupOut, pFirstOut, pCountOut, pFilter, base_index, 0);
      break;
   case 4:
      uniqueCount =
         GroupIndexStep2<int32_t, UINDEX>(pDataIn1, arraySize1, pDataIndexIn, pGroupOut, pFirstOut, pCountOut, pFilter, base_index, 0);
      break;
   case 8:
      uniqueCount =
         GroupIndexStep2<int64_t, UINDEX>(pDataIn1, arraySize1, pDataIndexIn, pGroupOut, pFirstOut, pCountOut, pFilter, base_index, 0);
      break;
   default:
      uniqueCount =
         GroupIndexStep2String<const char, UINDEX>(pDataIn1, arraySize1, pDataIndexIn, pGroupOut, pFirstOut, pCountOut, pFilter, base_index, strlen);
      break;
   }

   return uniqueCount;
}


//===============================================================================
// TODO: Need to add checks for some array allocations in this function (to see if they succeeded,
//       and if not, free any other allocated arrays before setting a PyExc_MemoryError and returning).
template<typename UINDEX>
static PyObject* GroupFromLexSortInternal(
   PyObject*   kwargs,
   UINDEX*     pIndex,
   npy_intp    indexLength, 
   npy_intp    indexLengthValues,
   void*       pValues,
   npy_intp    itemSizeValues) {

   int64_t    cutOffLength = 0;
   int64_t*   pCutOffs = GetCutOffs(kwargs, cutOffLength);

   int64_t    filterLength = 0;
   bool*    pFilter = GetFilter(kwargs, filterLength);

   int64_t    base_index = GetBaseIndex(kwargs);


   if (pCutOffs && pCutOffs[cutOffLength - 1] != indexLength) {
      return PyErr_Format(PyExc_ValueError, "GroupFromLexSort last cutoff length does not match array length %lld", indexLength);
   }
   if (cutOffLength == -1) {
      return PyErr_Format(PyExc_ValueError, "GroupFromLexSort 'cutoffs' must be an array of type int64_t");
   }
   if (pFilter && filterLength != indexLength) {
      return PyErr_Format(PyExc_ValueError, "GroupFromLexSort filter length does not match array length %lld", indexLength);
   }

   // The countout always reserves the zero bin (even for when base_index =0) for filtering out
   // TODO: Change this to use type npy_intp and check for overflow.
   int64_t worstCase = indexLength + 1 + cutOffLength;

   PyArrayObject* const keys = AllocateNumpyArray(1, (npy_intp*)&indexLengthValues, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);
   PyArrayObject* const first = AllocateNumpyArray(1, (npy_intp*)&indexLength, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);
   PyArrayObject* const count = AllocateNumpyArray(1, (npy_intp*)&worstCase, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);

   // Make sure allocations succeeded
   if (!keys || !first || !count)
   {
      // Release/recycle any of the arrays which _were_ successfully allocated so they're not leaked.
      if (keys) { RecycleNumpyInternal(keys); }
      if (first) { RecycleNumpyInternal(first); }

      return PyErr_Format(PyExc_MemoryError, "GroupFromLexSort out of memory length %lld", indexLength);
   }

   UINDEX* pKeyOut = (UINDEX*)PyArray_BYTES(keys);
   UINDEX* pFirstOut = (UINDEX*)PyArray_BYTES(first);
   UINDEX* pCountOut = (UINDEX*)PyArray_BYTES(count);

   int64_t uniqueCount = 0;
   GROUP_INDEX_FUNC  gpfunc = GroupIndex<UINDEX>;

   if (pCutOffs) {
      PyArrayObject* uniqueCounts = AllocateNumpyArray(1, (npy_intp*)&cutOffLength, NPY_INT64);
      int64_t*         pUniqueCounts = (int64_t*)PyArray_BYTES(uniqueCounts);

      // Turn off caching of large memory allocs
      g_cMathWorker->NoCaching = true;

      PLOGGING("partition version col: %lld  %p  %p  %p\n", cutOffLength, pToSort, pToSort + arrayLength, pValues);

      struct stPGROUP {
         GROUP_INDEX_FUNC  funcSingleGroup;
         int64_t*            pUniqueCounts;

         int64_t*            pCutOffs;   
         int64_t             cutOffLength;

         char*             pValues;
         char*             pIndex;
         char*             pKeyOut;
         char*             pFirstOut;
         char*             pCountOut;
         bool*             pFilter;
         int64_t             base_index;
         int64_t             strlen;
         int64_t             sizeofUINDEX;

      } pgroup;

      pgroup.funcSingleGroup = gpfunc;
      pgroup.pUniqueCounts = pUniqueCounts;

      pgroup.pCutOffs = pCutOffs;
      pgroup.cutOffLength = cutOffLength;
      pgroup.pValues = (char*)pValues;
      pgroup.pIndex = (char*)pIndex;

      pgroup.pKeyOut = (char*)pKeyOut;
      pgroup.pFirstOut = (char*)pFirstOut;
      pgroup.pCountOut = (char*)pCountOut;
      pgroup.pFilter = pFilter;
      pgroup.base_index = base_index;
      pgroup.strlen = itemSizeValues;
      const int64_t INDEX_SIZE = (int64_t)sizeof(UINDEX);

      pgroup.sizeofUINDEX = INDEX_SIZE;

      // Use threads per partition
      auto lambdaPSCallback = [](void* callbackArgT, int core, int64_t workIndex) -> bool {
         stPGROUP* callbackArg = (stPGROUP*)callbackArgT;
         int64_t t = workIndex;
         int64_t partLength;
         int64_t partStart;

         if (t == 0) {
            partStart = 0;
         }
         else {
            partStart = callbackArg->pCutOffs[t - 1];
         }

         partLength = callbackArg->pCutOffs[t] - partStart;

         PLOGGING("[%lld] start: %lld  length: %lld\n", t, partStart, partLength);

         int64_t shift = 
         // shift the data pointers to match the partition
         // call a single threaded merge
         callbackArg->pUniqueCounts[t]=
         callbackArg->funcSingleGroup(
            callbackArg->pValues + (partStart * callbackArg->strlen),
            partLength,
            callbackArg->pIndex + (partStart * callbackArg->sizeofUINDEX),
            callbackArg->pKeyOut + (partStart * callbackArg->sizeofUINDEX),
            callbackArg->pFirstOut + (partStart * callbackArg->sizeofUINDEX),
            callbackArg->pCountOut + (partStart * callbackArg->sizeofUINDEX),
            callbackArg->pFilter + partStart,
            0,       //callbackArg->base_index, fix for countout
            callbackArg->strlen);

         return true;
      };

      g_cMathWorker->DoMultiThreadedWork((int)cutOffLength, lambdaPSCallback, &pgroup);
      g_cMathWorker->NoCaching = false;

      // TODO: make global routine
      int64_t totalUniques = 0;
      for (int i = 0; i < cutOffLength; i++) {
         totalUniques += pUniqueCounts[i];
         pUniqueCounts[i] = totalUniques;
      }

      //printf("Total uniques %lld\n", totalUniques);

      // TODO: fix up keys
      //parallel add?
      PyArrayObject* firstReduced = AllocateNumpyArray(1, (npy_intp*)&totalUniques, INDEX_SIZE == 4 ? NPY_INT32 : NPY_INT64);

      totalUniques++;
      PyArrayObject* countReduced = AllocateNumpyArray(1, (npy_intp*)&totalUniques, INDEX_SIZE == 4 ? NPY_INT32 : NPY_INT64);

      // ANOTHER PARALEL ROUTINE to copy
      struct stPGROUPADD {
         int64_t*            pUniqueCounts;

         int64_t*            pCutOffs;       // May be NULL (if so no partitions)
         int64_t             cutOffLength;

         char*             pIndex;
         char*             pKeyOut;
         char*             pFirstOut;
         char*             pCountOut;

         char*             pFirstReduced;
         char*             pCountReduced;
         bool*             pFilter;

         int64_t             base_index;
         int64_t             sizeofUINDEX;

      } pgroupadd;

      pgroupadd.pUniqueCounts = pUniqueCounts;
      pgroupadd.pCutOffs = pCutOffs;
      pgroupadd.cutOffLength = cutOffLength;

      pgroupadd.pIndex = (char*)pIndex;

      pgroupadd.pKeyOut = (char*)pKeyOut;
      pgroupadd.pFirstOut = (char*)pFirstOut;
      pgroupadd.pCountOut = (char*)pCountOut;
      pgroupadd.pFilter = pFilter;

      pgroupadd.pFirstReduced = (char*)PyArray_BYTES(firstReduced);
      pgroupadd.pCountReduced = (char*)PyArray_BYTES(countReduced);

      // skip first value since reserved for zero bin (and assign it 0)
      for (int64_t c = 0; c < INDEX_SIZE; c++) {
         *pgroupadd.pCountReduced++ = 0;
      }

      pgroupadd.base_index = base_index;
      pgroupadd.sizeofUINDEX = sizeof(UINDEX);

      // Use threads per partition
      auto lambdaPGADDCallback = [](void* callbackArgT, int core, int64_t workIndex) -> bool {
         stPGROUPADD* callbackArg = (stPGROUPADD*)callbackArgT;
         int64_t t = workIndex;
         int64_t partLength;
         int64_t partStart;
         int64_t uniquesBefore;

         if (t == 0) {
            partStart = 0;
            uniquesBefore = 0;
         }
         else {
            partStart = callbackArg->pCutOffs[t - 1];
            uniquesBefore = callbackArg->pUniqueCounts[t - 1];
         }

         partLength = callbackArg->pCutOffs[t] - partStart;
         //printf("[%lld] start: %lld  length: %lld  ubefore: %lld\n", t, partStart, partLength, uniquesBefore);

         if (callbackArg->sizeofUINDEX == 4) {
            int32_t* pKey = (int32_t*)callbackArg->pKeyOut;

            // the iGroup is fixed up
            int32_t* pIndex = (int32_t*)callbackArg->pIndex;
            
            // pFirst is reduced to iFirstKey (only the uniques)
            int32_t* pFirst = (int32_t*)callbackArg->pFirstOut; 
            int32_t* pFirstReduced = (int32_t*)callbackArg->pFirstReduced;  

            // becomes nCount and the very first is reserved for zero bin
            // holds all the uniques + 1 for the zero bin.
            int32_t* pCount = (int32_t*)callbackArg->pCountOut;
            int32_t* pCountReduced = (int32_t*)callbackArg->pCountReduced;

            int32_t  ubefore = (int32_t)uniquesBefore;

            if (t != 0) {
               pKey += partStart;
               pIndex += partStart;

               for (int64_t i = 0; i < partLength; i++) {
                  pKey[i] += ((int32_t)ubefore + 1); // start at 1 (to reserve zero bin), becomes ikey
                  pIndex[i] += (int32_t)partStart;
               }
            }
            else {
               pKey += partStart;

               for (int64_t i = 0; i < partLength; i++) {
                  pKey[i] += ((int32_t)partStart + 1); // start at 1, becomes ikey
               }
            }

            int64_t uniqueLength = callbackArg->pUniqueCounts[t] - uniquesBefore;
            //printf("first reduced %d %lld\n", ubefore, uniqueLength);
            pFirst += partStart;
            pFirstReduced += ubefore;

            pCount += partStart;
            pCountReduced += ubefore;

            // very first [0] is for zero bin
            //pCount++;

            for (int64_t i = 0; i < uniqueLength; i++) {
               pFirstReduced[i] = pFirst[i] + (int32_t)partStart;
               //printf("setting %lld ", (int64_t)pCount[i]);
               pCountReduced[i] = pCount[i];
            }

         }
         else {

            int64_t* pKey = (int64_t*)callbackArg->pKeyOut;

            // the iGroup is fixed up
            int64_t* pIndex = (int64_t*)callbackArg->pIndex;

            // pFirst is reduced to iFirstKey (only the uniques)
            int64_t* pFirst = (int64_t*)callbackArg->pFirstOut;
            int64_t* pFirstReduced = (int64_t*)callbackArg->pFirstReduced;

            // becomes nCount and the very first is reserved for zero bin
            // holds all the uniques + 1 for the zero bin.
            int64_t* pCount = (int64_t*)callbackArg->pCountOut;
            int64_t* pCountReduced = (int64_t*)callbackArg->pCountReduced;

            int64_t  ubefore = (int64_t)uniquesBefore;

            if (t != 0) {
               pKey += partStart;
               pIndex += partStart;

               for (int64_t i = 0; i < partLength; i++) {
                  pKey[i] += ((int64_t)ubefore + 1); // start at 1 (to reserve zero bin), becomes ikey
                  pIndex[i] += (int64_t)partStart;
               }
            }
            else {
               pKey += partStart;

               for (int64_t i = 0; i < partLength; i++) {
                  pKey[i] += ((int64_t)partStart + 1); // start at 1, becomes ikey
               }
            }

            int64_t uniqueLength = callbackArg->pUniqueCounts[t] - uniquesBefore;
            //printf("first reduced %d %lld\n", ubefore, uniqueLength);
            pFirst += partStart;
            pFirstReduced += ubefore;

            pCount += partStart;
            pCountReduced += ubefore;

            // very first [0] is for zero bin
            //pCount++;

            for (int64_t i = 0; i < uniqueLength; i++) {
               pFirstReduced[i] = pFirst[i] + (int64_t)partStart;
               //printf("setting %lld ", (int64_t)pCount[i]);
               pCountReduced[i] = pCount[i];
            }

         }

         return true;
      };
      g_cMathWorker->DoMultiThreadedWork((int)cutOffLength, lambdaPGADDCallback, &pgroupadd);

      Py_DecRef((PyObject*)first);
      //Py_DecRef((PyObject*)count);

      PyObject* returnObject = PyList_New(4);
      PyList_SET_ITEM(returnObject, 0, (PyObject*)keys);
      PyList_SET_ITEM(returnObject, 1, (PyObject*)firstReduced); // iFirstKey
      PyList_SET_ITEM(returnObject, 2, (PyObject*)countReduced); // nCountGroup

      PyList_SET_ITEM(returnObject, 3, (PyObject*)uniqueCounts);
      return returnObject;
   }
   else {

      // When multiple arrays are passed, we sort in order of how it is passed
      // Thus, the last array is the last sort, and therefore determines the primary sort order
      uniqueCount =
         gpfunc(
            pValues,
            indexLength,
            pIndex,
            pKeyOut,
            pFirstOut,
            pCountOut,
            pFilter,
            base_index,  
            itemSizeValues);

      // prior we allocate based on worst case
      // now we know the actual unique counts
      // memcpy..
      // also count invalid bin
      PyArrayObject* firstReduced = AllocateNumpyArray(1, (npy_intp*)&uniqueCount, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);
      int64_t copySize = sizeof(UINDEX) * uniqueCount;
      memcpy(PyArray_BYTES(firstReduced), pFirstOut, copySize);

      uniqueCount++;
      PyArrayObject* countReduced = AllocateNumpyArray(1, (npy_intp*)&uniqueCount, sizeof(UINDEX) == 4 ? NPY_INT32 : NPY_INT64);
      copySize = sizeof(UINDEX) * uniqueCount;
      // reduced
      memcpy(PyArray_BYTES(countReduced), pCountOut, copySize);

      Py_DecRef((PyObject*)first);
      Py_DecRef((PyObject*)count);

      PyObject* returnObject = PyList_New(3);
      PyList_SET_ITEM(returnObject, 0, (PyObject*)keys);
      PyList_SET_ITEM(returnObject, 1, (PyObject*)firstReduced);
      PyList_SET_ITEM(returnObject, 2, (PyObject*)countReduced);
      return returnObject;
   }
}


//===============================================================================
// Args:
//     Checks for cutoffs
//     If no cutoffs
//     
//     Arg1: lex=int32_t/int64_t result from lexsort
//     Arg2: value array that was sorted -- if it came from a list, convert it to a void type
//
// Returns 3 arrays
// iGroup (from lexsort)
// iFirstKey
// nCount -- from which iFirst can be derived
//
// if filtering was used then arrLength != arrLengthValues
// when this happens, the iGroup will have unfilled values for filtered out locations
PyObject* GroupFromLexSort(PyObject *self, PyObject *args, PyObject *kwargs) {

   PyArrayObject *inArrSortIndex = NULL;
   PyArrayObject *inArrValues = NULL;

   if (!PyArg_ParseTuple(args, "O!O!", &PyArray_Type, &inArrSortIndex, &PyArray_Type, &inArrValues))
   {
      return PyErr_Format(PyExc_TypeError, "Invalid argument types and/or count for GroupFromLexSort.");
   }

   const auto arrLength = ArrayLength(inArrSortIndex);
   const auto arrLengthValues = ArrayLength(inArrValues);

   // Due to filtering, now allow a smaller array length which might index the entire
   // size of inArrValues
   if (arrLength > arrLengthValues) {
      return PyErr_Format(PyExc_ValueError, "GroupFromLexSort input array lengths do not match: %lld vs %lld", arrLength, arrLengthValues);
   }

   const auto itemSize = PyArray_ITEMSIZE(inArrValues);

   const auto dtype = PyArray_TYPE(inArrSortIndex);

   void* pIndex = PyArray_BYTES(inArrSortIndex);
   void* pValues = PyArray_BYTES(inArrValues);

   switch (dtype) {
   CASE_NPY_INT32:
      return GroupFromLexSortInternal<int32_t>(kwargs, (int32_t*)pIndex, arrLength, arrLengthValues,  pValues, itemSize);
      break;

   CASE_NPY_INT64:
      return GroupFromLexSortInternal<int64_t>(kwargs, (int64_t*)pIndex, arrLength, arrLengthValues, pValues, itemSize);
      break;

   default:
      return PyErr_Format(PyExc_ValueError, "GroupFromLexSort does not support index type of %d", dtype);
   }
}

