#include "RipTide.h"
#include "ndarray.h"
#include "MathWorker.h"

#include "CommonInc.h"
#include "Compare.h"
#include "Convert.h"
#include "BasicMath.h"
#include "platform_detect.h"

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

// TODO: handle div by zero, etc.
//#pragma STDC FENV_ACCESS ON
// std::feclearexcept(FE_ALL_EXCEPT);
// TODO: add floatstatus
//#include <cfenv>
//#include <cmath>
// int npy_get_floatstatus(void)
//{
//   int fpstatus = fetestexcept(FE_DIVBYZERO | FE_OVERFLOW |
//      FE_UNDERFLOW | FE_INVALID);
//
//   return ((FE_DIVBYZERO  & fpstatus) ? NPY_FPE_DIVIDEBYZERO : 0) |
//      ((FE_OVERFLOW   & fpstatus) ? NPY_FPE_OVERFLOW : 0) |
//      ((FE_UNDERFLOW  & fpstatus) ? NPY_FPE_UNDERFLOW : 0) |
//      ((FE_INVALID    & fpstatus) ? NPY_FPE_INVALID : 0);
//}
//
// int npy_clear_floatstatus(void)
//{
//   /* testing float status is 50-100 times faster than clearing on x86 */
//   int fpstatus = npy_get_floatstatus();
//   if (fpstatus != 0) {
//      feclearexcept(FE_DIVBYZERO | FE_OVERFLOW |
//         FE_UNDERFLOW | FE_INVALID);
//   }
//
//   return fpstatus;
//}

static const inline void STOREU(__m256d * x, __m256d y)
{
    _mm256_storeu_pd((double *)x, y);
}
static const inline void STOREU(__m256 * x, __m256 y)
{
    _mm256_storeu_ps((float *)x, y);
}
static const inline void STOREU(__m256i * x, __m256i y)
{
    _mm256_storeu_si256((__m256i *)x, y);
}

// For aligned loads which must be on 32 byte boundary
static const inline __m256d LOADA(__m256d * x)
{
    return _mm256_load_pd((double const *)x);
};
static const inline __m256 LOADA(__m256 * x)
{
    return _mm256_load_ps((float const *)x);
};
static const inline __m256i LOADA(__m256i * x)
{
    return _mm256_load_si256((__m256i const *)x);
};

static const inline void STOREA(__m256d * x, __m256d y)
{
    _mm256_store_pd((double *)x, y);
}
static const inline void STOREA(__m256 * x, __m256 y)
{
    _mm256_store_ps((float *)x, y);
}
static const inline void STOREA(__m256i * x, __m256i y)
{
    _mm256_store_si256((__m256i *)x, y);
}

static const inline __m256d LOADU(__m256d * x)
{
    return _mm256_loadu_pd((double const *)x);
}
static const inline __m256 LOADU(__m256 * x)
{
    return _mm256_loadu_ps((float const *)x);
}
static const inline __m256i LOADU(__m256i * x)
{
    return _mm256_loadu_si256((__m256i const *)x);
}

static const inline __m256i MIN_OPi8(__m256i x, __m256i y)
{
    return _mm256_min_epi8(x, y);
}
static const inline __m256i MIN_OPu8(__m256i x, __m256i y)
{
    return _mm256_min_epu8(x, y);
}
static const inline __m256i MIN_OPu16(__m256i x, __m256i y)
{
    return _mm256_min_epu16(x, y);
}
static const inline __m256i MIN_OPi16(__m256i x, __m256i y)
{
    return _mm256_min_epi16(x, y);
}
static const inline __m256i MIN_OPi32(__m256i x, __m256i y)
{
    return _mm256_min_epi32(x, y);
}
static const inline __m256i MIN_OPu32(__m256i x, __m256i y)
{
    return _mm256_min_epu32(x, y);
}

// Below unused
// static const inline __m256  MIN_OPf32( __m256  x, __m256 y) { return
// _mm256_min_ps(x, y); } static const inline __m256d MIN_OPf64( __m256d x,
// __m256d y) { return _mm256_min_pd(x, y); }

static const inline __m256i MAX_OPi8(__m256i x, __m256i y)
{
    return _mm256_max_epi8(x, y);
}
static const inline __m256i MAX_OPu8(__m256i x, __m256i y)
{
    return _mm256_max_epu8(x, y);
}
static const inline __m256i MAX_OPu16(__m256i x, __m256i y)
{
    return _mm256_max_epu16(x, y);
}
static const inline __m256i MAX_OPi16(__m256i x, __m256i y)
{
    return _mm256_max_epi16(x, y);
}
static const inline __m256i MAX_OPi32(__m256i x, __m256i y)
{
    return _mm256_max_epi32(x, y);
}
static const inline __m256i MAX_OPu32(__m256i x, __m256i y)
{
    return _mm256_max_epu32(x, y);
}
// NOTE: Cannot use for np.maximum since it handles nans and intel intrinsic
// does not handle the same way NOTE: Could use if we want a maximum and we dont
// care about nan handling Below unused
// static const inline __m256  MAX_OPf32(__m256  x, __m256 y) { return
// _mm256_max_ps(x, y); } static const inline __m256d MAX_OPf64(__m256d x,
// __m256d y) { return _mm256_max_pd(x, y); }

template <typename T>
static const inline T AddOp(T x, T y)
{
    return x + y;
}
template <typename T>
static const inline bool AddOp(bool x, bool y)
{
    return x | y;
}
template <typename T>
static const inline T SubOp(T x, T y)
{
    return x - y;
}
template <typename T>
static const inline bool SubOp(bool x, bool y)
{
    return (x == true) && (y == false);
}
template <typename T>
static const inline T MulOp(T x, T y)
{
    return x * y;
}

// nan != nan --> True
// nan > anynumber --> False
// nan < anynumber --> False
template <typename T>
static const inline float MinOp(float x, float y)
{
    if (x > y || y != y)
        return y;
    else
        return x;
}
template <typename T>
static const inline double MinOp(double x, double y)
{
    if (x > y || y != y)
        return y;
    else
        return x;
}
template <typename T>
static const inline T MinOp(T x, T y)
{
    if (x > y)
        return y;
    else
        return x;
}

template <typename T>
static const inline float MaxOp(float x, float y)
{
    if (x < y || y != y)
        return y;
    else
        return x;
}
template <typename T>
static const inline double MaxOp(double x, double y)
{
    if (x < y || y != y)
        return y;
    else
        return x;
}
template <typename T>
static const inline T MaxOp(T x, T y)
{
    if (x < y)
        return y;
    else
        return x;
}
// template<typename T> static const inline bool MulOp(bool x, bool y) { return
// x & y; }

static const int64_t NAN_FOR_INT64 = (int64_t)0x8000000000000000LL;
static const int64_t NAN_FOR_INT32 = (int32_t)0x80000000;

template <typename T>
static const inline double DivOp(T x, T y)
{
    return (double)x / (double)y;
}
template <typename T>
static const inline float DivOp(float x, T y)
{
    return x / y;
}

// Subtraction only two DateTimeNano subtractions
template <typename T>
static const inline double SubDateTimeOp(int64_t x, int64_t y)
{
    if (x == 0 || y == 0 || x == NAN_FOR_INT64 || y == NAN_FOR_INT64)
    {
        return NAN;
    }
    return (double)(x - y);
}

// Subtraction only both sides checked
template <typename T>
static const inline double SubDateTimeOp(int32_t x, int32_t y)
{
    if (x == 0 || y == 0 || x == NAN_FOR_INT32 || y == NAN_FOR_INT32)
    {
        return NAN;
    }
    return (double)(x - y);
}

// Subtract two dates to produce a DateSpan
template <typename T>
static const inline int32_t SubDatesOp(int32_t x, int32_t y)
{
    if (x == 0 || y == 0 || x == NAN_FOR_INT32 || y == NAN_FOR_INT32)
    {
        return 0;
    }
    return (x - y);
}

template <typename T>
static const inline int64_t SubDatesOp(int64_t x, int64_t y)
{
    if (x == 0 || y == 0 || x == NAN_FOR_INT64 || y == NAN_FOR_INT64)
    {
        return 0;
    }
    return (x - y);
}

template <typename T>
static const inline T FloorDivOp(T x, T y)
{
    if (y != 0)
    {
        if ((x < 0) == (y < 0))
            return x / y;

        T q = x / y;
        T r = x % y;
        if (r != 0)
            --q;
        return q;
    }
    else
        return 0;
}
template <typename T>
static const inline long double FloorDivOp(long double x, long double y)
{
    if (y == y)
        return floorl(x / y);
    else
        return NAN;
}
template <typename T>
static const inline double FloorDivOp(double x, double y)
{
    if (y == y)
        return floor(x / y);
    else
        return NAN;
}
template <typename T>
static const inline float FloorDivOp(float x, float y)
{
    if (y == y)
        return floorf(x / y);
    else
        return NAN;
}

template <typename T>
static const inline T ModOp(T x, T y)
{
    return x % y;
}
template <typename T>
static const inline float ModOp(T x, T y)
{
    return fmodf(x, y);
}
template <typename T>
static const inline double ModOp(T x, T y)
{
    return fmod(x, y);
}
template <typename T>
static const inline bool ModOp(bool x, bool y)
{
    return (x ^ y);
}

template <typename T>
static const inline T PowerOp(T x, T y)
{
    return (T)pow(x, y);
}
template <typename T>
static const inline float PowerOp(float x, float y)
{
    return powf(x, y);
}
template <typename T>
static const inline double PowerOp(double x, double y)
{
    return pow(x, y);
}
template <typename T>
static const inline long double PowerOp(long double x, long double y)
{
    return powl(x, y);
}
template <typename T>
static const inline bool PowerOp(bool x, bool y)
{
    if (! x & y)
        return 0;
    else
        return 1;
}

// numpy np.remainder is not 'C' remainder but there is a new math function
// template<typename T> static const inline long double REMAINDER_OP(long double
// x, long double y) { return remainderl(x, y); } template<typename T> static
// const inline double REMAINDER_OP(double x, double y) { return remainder(x, y);
// } template<typename T> static const inline float REMAINDER_OP(float x, float
// y) { return remainderf(x, y); }

template <typename T>
static const inline long double REMAINDER_OP(long double x, long double y)
{
    return x - (y * floorl(x / y));
}
template <typename T>
static const inline double REMAINDER_OP(double x, double y)
{
    return x - (y * floor(x / y));
}
template <typename T>
static const inline float REMAINDER_OP(float x, float y)
{
    return x - (y * floorf(x / y));
}

template <typename T>
static const inline long double FMOD_OP(long double x, long double y)
{
    return fmodl(x, y);
}
template <typename T>
static const inline double FMOD_OP(double x, double y)
{
    return fmod(x, y);
}
template <typename T>
static const inline float FMOD_OP(float x, float y)
{
    return fmodf(x, y);
}

template <typename T>
static const inline long double LogOp(long double x)
{
    return logl(x);
}
template <typename T>
static const inline double LogOp(double x)
{
    return log(x);
}
template <typename T>
static const inline float LogOp(float x)
{
    return logf(x);
}

// bitwise operations
template <typename T>
static const inline T AndOp(T x, T y)
{
    return x & y;
}
template <typename T>
static const inline T XorOp(T x, T y)
{
    return x ^ y;
}
template <typename T>
static const inline T OrOp(T x, T y)
{
    return x | y;
}
// NOTE: mimics intel intrinsic
template <typename T>
static const inline T AndNotOp(T x, T y)
{
    return ~x & y;
}

//=========================================================================================
// TJD NOTE:inline does not work with some compilers and templates in some loops
static const inline __m256 ADD_OP_256f32(__m256 x, __m256 y)
{
    return _mm256_add_ps(x, y);
}
static const inline __m256d ADD_OP_256f64(__m256d x, __m256d y)
{
    return _mm256_add_pd(x, y);
}
static const inline __m256i ADD_OP_256i32(__m256i x, __m256i y)
{
    return _mm256_add_epi32(x, y);
}
static const inline __m256i ADD_OP_256i64(__m256i x, __m256i y)
{
    return _mm256_add_epi64(x, y);
}
static const inline __m256i ADD_OP_256i16(__m256i x, __m256i y)
{
    return _mm256_add_epi16(x, y);
}
static const inline __m256i ADD_OP_256i8(__m256i x, __m256i y)
{
    return _mm256_add_epi8(x, y);
}

static const inline __m256 SUB_OP_256f32(__m256 x, __m256 y)
{
    return _mm256_sub_ps(x, y);
}
static const inline __m256d SUB_OP_256f64(__m256d x, __m256d y)
{
    return _mm256_sub_pd(x, y);
}
static const inline __m256i SUB_OP_256i32(__m256i x, __m256i y)
{
    return _mm256_sub_epi32(x, y);
}
static const inline __m256i SUB_OP_256i64(__m256i x, __m256i y)
{
    return _mm256_sub_epi64(x, y);
}
static const inline __m256i SUB_OP_256i16(__m256i x, __m256i y)
{
    return _mm256_sub_epi16(x, y);
}
static const inline __m256i SUB_OP_256i8(__m256i x, __m256i y)
{
    return _mm256_sub_epi8(x, y);
}

static const inline __m256 MUL_OP_256f32(__m256 x, __m256 y)
{
    return _mm256_mul_ps(x, y);
}
static const inline __m256d MUL_OP_256f64(__m256d x, __m256d y)
{
    return _mm256_mul_pd(x, y);
}
static const inline __m256i MUL_OP_256i32(__m256i x, __m256i y)
{
    return _mm256_mullo_epi32(x, y);
}
static const inline __m256i MUL_OP_256i16(__m256i x, __m256i y)
{
    return _mm256_mullo_epi16(x, y);
}

// mask off low 32bits
static const __m256i masklo = _mm256_set1_epi64x(0xFFFFFFFFLL);
static const __m128i shifthigh = _mm_set1_epi64x(32);

// This routine only works for positive integers
static const inline __m256i MUL_OP_256u64(__m256i x, __m256i y)
{
    // Algo is lo1*lo2 + (lo1*hi2) << 32 + (lo2*hi1) << 32
    // To get to 128 bit int would have to add (hi1*hi2) << 64
    __m256i lo1 = _mm256_and_si256(x, masklo);
    __m256i lo2 = _mm256_and_si256(y, masklo);
    __m256i hi1 = _mm256_srl_epi64(x, shifthigh); // need to sign extend
    __m256i hi2 = _mm256_srl_epi64(y, shifthigh);
    __m256i add1 = _mm256_mul_epu32(lo1, lo2);
    __m256i add2 = _mm256_sll_epi64(_mm256_mul_epu32(lo1, hi2), shifthigh);
    __m256i add3 = _mm256_sll_epi64(_mm256_mul_epu32(lo2, hi1), shifthigh);
    // add all the results together
    return _mm256_add_epi64(add1, _mm256_add_epi64(add2, add3));
}

// eight 32bit --> produce four 64 bit
// below unused
// static const inline __m256i MULX_OP_256i32(__m256i x, __m256i y) { return
// _mm256_mul_epi32(x, y); } static const inline __m256i MULX_OP_256u32(__m256i
// x, __m256i y) { return _mm256_mul_epu32(x, y); }

static const inline __m256 DIV_OP_256f32(__m256 x, __m256 y)
{
    return _mm256_div_ps(x, y);
}
static const inline __m256d DIV_OP_256f64(__m256d x, __m256d y)
{
    return _mm256_div_pd(x, y);
}
static const inline __m256d CONV_INT32_DOUBLE(__m128i * x)
{
    return _mm256_cvtepi32_pd(*x);
}

// static const inline __m256d CONV_INT16_DOUBLE(__m128i x) { return
// _mm256_cvtepi16_pd(x); } static const inline __m256d DIV_OP_256(const int32_t
// z, __m128i x, __m128i y) { return _mm256_div_pd(_mm256_cvtepi32_pd(x),
// _mm256_cvtepi32_pd(y)); } static const inline __m256d DIV_OP_256(const int64_t
// z, __m128i x, __m128i y) { return _mm256_div_pd(_mm256_cvtepi64_pd(x),
// _mm256_cvtepi64_pd(y)); }

static const inline __m256i AND_OP_256(__m256i x, __m256i y)
{
    return _mm256_and_si256(x, y);
}
static const inline __m256i OR_OP_256(__m256i x, __m256i y)
{
    return _mm256_or_si256(x, y);
}
static const inline __m256i XOR_OP_256(__m256i x, __m256i y)
{
    return _mm256_xor_si256(x, y);
}
static const inline __m256i ANDNOT_OP_256(__m256i x, __m256i y)
{
    return _mm256_andnot_si256(x, y);
}

//_asm cvt2q
// CVTDQ2PD
// inline __m256d DIV_OP_256(const int64_t z, __m256i x, __m256i y) { return
// _mm256_div_pd(_mm256_cvtepi64_pd(x), _mm256_cvtepi64_pd(y)); }

template <typename T, typename U256>
static const inline __m256 AddOp256(const float z, U256 x, U256 y)
{
    return _mm256_add_ps(x, y);
}
template <typename T, typename U256>
static const inline __m256d AddOp256(const double z, U256 x, U256 y)
{
    return _mm256_add_pd(x, y);
}
template <typename T, typename U256>
static const inline __m256i AddOp256(const int32_t z, U256 x, U256 y)
{
    return _mm256_add_epi32(x, y);
}

template <typename T, typename MathFunctionPtr>
FORCEINLINE void SimpleMathOpSlow(MathFunctionPtr MATH_OP, void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t len,
                                  int32_t scalarMode)
{
    T * pDataOut = (T *)pDataOutX;
    T * pDataIn1 = (T *)pDataIn1X;
    T * pDataIn2 = (T *)pDataIn2X;

    LOGGING("slow  In:%p  In2:%p  Out:%p\n", pDataIn1, pDataIn2, pDataOut);

    switch (scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            T arg1 = *pDataIn1;
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(arg1, pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            T arg2 = *pDataIn2;
            // printf("arg2 = %lld  %lld\n", (int64_t)arg2, (int64_t)pDataIn1[0]);
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], arg2);
            }
            break;
        }
    default:
        T arg1 = *pDataIn1;
        T arg2 = *pDataIn2;
        for (int64_t i = 0; i < len; i++)
        {
            pDataOut[i] = MATH_OP(arg1, arg2);
        }
        break;
    }
}

//=====================================================================================================
// Used in division where the output is a double
template <typename T, typename MathFunctionPtr>
FORCEINLINE void SimpleMathOpSlowDouble(MathFunctionPtr MATH_OP, void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t len,
                                        int32_t scalarMode)
{
    double * pDataOut = (double *)pDataOutX;
    T * pDataIn1 = (T *)pDataIn1X;
    T * pDataIn2 = (T *)pDataIn2X;

    LOGGING("slow div or dates double %lld  %d  %p  %p  %p\n", len, scalarMode, pDataIn1X, pDataIn2X, pDataOutX);

    switch (scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            T arg1 = *pDataIn1;
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(arg1, pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            T arg2 = *pDataIn2;
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], arg2);
            }
            break;
        }
    default:
        T arg1 = *pDataIn1;
        T arg2 = *pDataIn2;
        for (int64_t i = 0; i < len; i++)
        {
            pDataOut[i] = MATH_OP(arg1, arg2);
        }
        break;
        // printf("**error - impossible scalar mode\n");
    }
}

//=====================================================================================================
// Not symmetric -- arg1 must be first, arg2 must be second
template <typename T, typename U256, const T MATH_OP(T, T), const U256 MATH_OP256(U256, U256)>
inline void SimpleMathOpFast(void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t datalen, int32_t scalarMode)
{
    T * pDataOut = (T *)pDataOutX;
    T * pDataIn1 = (T *)pDataIn1X;
    T * pDataIn2 = (T *)pDataIn2X;

    const int64_t NUM_LOOPS_UNROLLED = 1;
    const int64_t chunkSize = NUM_LOOPS_UNROLLED * (sizeof(U256) / sizeof(T));
    int64_t perReg = sizeof(U256) / sizeof(T);

    LOGGING("mathopfast datalen %llu  chunkSize %llu  perReg %llu\n", datalen, chunkSize, perReg);

    switch (scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            if (datalen >= chunkSize)
            {
                T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                U256 * pEnd_256 = (U256 *)pEnd;
                U256 * pIn1_256 = (U256 *)pDataIn1;
                U256 * pIn2_256 = (U256 *)pDataIn2;
                U256 * pOut_256 = (U256 *)pDataOut;

                do
                {
                    // clang requires LOADU on last operand
#ifdef RT_COMPILER_MSVC
                    // Microsoft will create the opcode where the second argument is an
                    // address
                    STOREU(pOut_256, MATH_OP256(LOADU(pIn1_256), *pIn2_256));
#else
                    STOREU(pOut_256, MATH_OP256(LOADU(pIn1_256), LOADU(pIn2_256)));
#endif
                    pOut_256 += NUM_LOOPS_UNROLLED;
                    pIn1_256 += NUM_LOOPS_UNROLLED;
                    pIn2_256 += NUM_LOOPS_UNROLLED;
                }
                while (pOut_256 < pEnd_256);

                // update pointers to last location of wide pointers
                pDataIn1 = (T *)pIn1_256;
                pDataIn2 = (T *)pIn2_256;
                pDataOut = (T *)pOut_256;
            }

            datalen = datalen & (chunkSize - 1);
            for (int64_t i = 0; i < datalen; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], pDataIn2[i]);
            }

            break;
        }
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            // NOTE: the unrolled loop is faster
            T arg1 = *pDataIn1;

            if (datalen >= chunkSize)
            {
                T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                U256 * pEnd_256 = (U256 *)pEnd;
                U256 * pIn1_256 = (U256 *)pDataIn1;
                U256 * pIn2_256 = (U256 *)pDataIn2;
                U256 * pOut_256 = (U256 *)pDataOut;

                const U256 m0 = LOADU(pIn1_256);

                do
                {
#ifdef RT_COMPILER_MSVC
                    STOREU(pOut_256, MATH_OP256(m0, *pIn2_256));
#else
                    STOREU(pOut_256, MATH_OP256(m0, LOADU(pIn2_256)));
#endif

                    pOut_256 += NUM_LOOPS_UNROLLED;
                    pIn2_256 += NUM_LOOPS_UNROLLED;
                }
                while (pOut_256 < pEnd_256);

                // update pointers to last location of wide pointers
                pDataIn2 = (T *)pIn2_256;
                pDataOut = (T *)pOut_256;
            }
            datalen = datalen & (chunkSize - 1);
            for (int64_t i = 0; i < datalen; i++)
            {
                pDataOut[i] = MATH_OP(arg1, pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            T arg2 = *pDataIn2;

            // Check if the output is the same as the input
            if (pDataOut == pDataIn1)
            {
                // align the load to 32 byte boundary
                int64_t babylen = (int64_t)pDataIn1 & 31;
                if (babylen != 0)
                {
                    // calc how much to align data
                    babylen = (32 - babylen) / sizeof(T);
                    if (babylen <= datalen)
                    {
                        for (int64_t i = 0; i < babylen; i++)
                        {
                            pDataIn1[i] = MATH_OP(pDataIn1[i], arg2);
                        }
                        pDataIn1 += babylen;
                        datalen -= babylen;
                    }
                }

                // inplace operation
                if (datalen >= chunkSize)
                {
                    T * pEnd = &pDataIn1[chunkSize * (datalen / chunkSize)];
                    U256 * pEnd_256 = (U256 *)pEnd;
                    U256 * pIn1_256 = (U256 *)pDataIn1;
                    U256 * pIn2_256 = (U256 *)pDataIn2;

                    const U256 m1 = LOADU(pIn2_256);

                    // apply 256bit aligned operations
                    while (pIn1_256 < pEnd_256)
                    {
                        STOREA(pIn1_256, MATH_OP256(LOADA(pIn1_256), m1));
                        pIn1_256++;
                    }

                    // update pointers to last location of wide pointers
                    pDataIn1 = (T *)pIn1_256;
                }
                datalen = datalen & (chunkSize - 1);
                for (int64_t i = 0; i < datalen; i++)
                {
                    pDataIn1[i] = MATH_OP(pDataIn1[i], arg2);
                }
            }
            else
            {
                if (datalen >= chunkSize)
                {
                    T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                    U256 * pEnd_256 = (U256 *)pEnd;
                    U256 * pIn1_256 = (U256 *)pDataIn1;
                    U256 * pIn2_256 = (U256 *)pDataIn2;
                    U256 * pOut_256 = (U256 *)pDataOut;

                    const U256 m1 = LOADU((U256 *)pIn2_256);

                    // apply 256bit unaligned operations
                    do
                    {
                        STOREU(pOut_256, MATH_OP256(LOADU(pIn1_256), m1));

                        pOut_256 += NUM_LOOPS_UNROLLED;
                        pIn1_256 += NUM_LOOPS_UNROLLED;
                    }
                    while (pOut_256 < pEnd_256);

                    // update pointers to last location of wide pointers
                    pDataIn1 = (T *)pIn1_256;
                    pDataOut = (T *)pOut_256;
                }
                datalen = datalen & (chunkSize - 1);
                for (int64_t i = 0; i < datalen; i++)
                {
                    pDataOut[i] = MATH_OP(pDataIn1[i], arg2);
                }
            }
            break;
        }
    default:
        printf("**error - impossible scalar mode\n");
    }
}

//=====================================================================================================
// Flips arg1 and arg2 around for BITWISE_NOTAND
template <typename T, typename U256, const T MATH_OP(T, T), const U256 MATH_OP256(U256, U256)>
inline void SimpleMathOpFastReverse(void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t datalen, int32_t scalarMode)
{
    return SimpleMathOpFast<T, U256, MATH_OP, MATH_OP256>(pDataIn2X, pDataIn1X, pDataOutX, datalen, scalarMode);
}

//=====================================================================================================
// Not symmetric -- arg1 must be first, arg2 must be second
template <typename T, typename U256, const T MATH_OP(T, T), const U256 MATH_OP256(U256, U256)>
inline void SimpleMathOpFastSymmetric(void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t datalen, int32_t scalarMode)
{
    T * pDataOut = (T *)pDataOutX;
    T * pDataIn1 = (T *)pDataIn1X;
    T * pDataIn2 = (T *)pDataIn2X;

    const int64_t NUM_LOOPS_UNROLLED = 1;
    const int64_t chunkSize = NUM_LOOPS_UNROLLED * (sizeof(U256) / sizeof(T));
    int64_t perReg = sizeof(U256) / sizeof(T);

    LOGGING("mathopfast datalen %llu  chunkSize %llu  perReg %llu\n", datalen, chunkSize, perReg);

    switch (scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            if (datalen >= chunkSize)
            {
                T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                U256 * pEnd_256 = (U256 *)pEnd;
                U256 * pIn1_256 = (U256 *)pDataIn1;
                U256 * pIn2_256 = (U256 *)pDataIn2;
                U256 * pOut_256 = (U256 *)pDataOut;

                do
                {
                    // clang requires LOADU on last operand
#ifdef RT_COMPILER_MSVC
                    STOREU(pOut_256, MATH_OP256(LOADU(pIn1_256), *pIn2_256));
#else
                    STOREU(pOut_256, MATH_OP256(LOADU(pIn1_256), LOADU(pIn2_256)));
#endif
                    pOut_256 += NUM_LOOPS_UNROLLED;
                    pIn1_256 += NUM_LOOPS_UNROLLED;
                    pIn2_256 += NUM_LOOPS_UNROLLED;
                }
                while (pOut_256 < pEnd_256);

                // update pointers to last location of wide pointers
                pDataIn1 = (T *)pIn1_256;
                pDataIn2 = (T *)pIn2_256;
                pDataOut = (T *)pOut_256;
            }

            datalen = datalen & (chunkSize - 1);
            for (int64_t i = 0; i < datalen; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], pDataIn2[i]);
            }

            break;
        }
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            // NOTE: the unrolled loop is faster
            T arg1 = *pDataIn1;

            if (datalen >= chunkSize)
            {
                T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                U256 * pEnd_256 = (U256 *)pEnd;
                U256 * pIn1_256 = (U256 *)pDataIn1;
                U256 * pIn2_256 = (U256 *)pDataIn2;
                U256 * pOut_256 = (U256 *)pDataOut;

                const U256 m0 = LOADU(pIn1_256);

                do
                {
#ifdef RT_COMPILER_MSVC
                    STOREU(pOut_256, MATH_OP256(m0, *pIn2_256));
#else
                    STOREU(pOut_256, MATH_OP256(m0, LOADU(pIn2_256)));
#endif

                    pOut_256 += NUM_LOOPS_UNROLLED;
                    pIn2_256 += NUM_LOOPS_UNROLLED;
                }
                while (pOut_256 < pEnd_256);

                // update pointers to last location of wide pointers
                pDataIn2 = (T *)pIn2_256;
                pDataOut = (T *)pOut_256;
            }
            datalen = datalen & (chunkSize - 1);
            for (int64_t i = 0; i < datalen; i++)
            {
                pDataOut[i] = MATH_OP(arg1, pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            T arg2 = *pDataIn2;

            // Check if the output is the same as the input
            if (pDataOut == pDataIn1)
            {
                // align the load to 32 byte boundary
                int64_t babylen = (int64_t)pDataIn1 & 31;
                if (babylen != 0)
                {
                    // calc how much to align data
                    babylen = (32 - babylen) / sizeof(T);
                    if (babylen <= datalen)
                    {
                        for (int64_t i = 0; i < babylen; i++)
                        {
                            pDataIn1[i] = MATH_OP(pDataIn1[i], arg2);
                        }
                        pDataIn1 += babylen;
                        datalen -= babylen;
                    }
                }

                // inplace operation
                if (datalen >= chunkSize)
                {
                    T * pEnd = &pDataIn1[chunkSize * (datalen / chunkSize)];
                    U256 * pEnd_256 = (U256 *)pEnd;
                    U256 * pIn1_256 = (U256 *)pDataIn1;
                    U256 * pIn2_256 = (U256 *)pDataIn2;

                    const U256 m1 = LOADU(pIn2_256);

                    // apply 256bit aligned operations
                    while (pIn1_256 < pEnd_256)
                    {
                        // pin1_256 is aligned
                        STOREA(pIn1_256, MATH_OP256(m1, *pIn1_256));
                        pIn1_256++;
                    }

                    // update pointers to last location of wide pointers
                    pDataIn1 = (T *)pIn1_256;
                }
                datalen = datalen & (chunkSize - 1);
                for (int64_t i = 0; i < datalen; i++)
                {
                    pDataIn1[i] = MATH_OP(pDataIn1[i], arg2);
                }
            }
            else
            {
                if (datalen >= chunkSize)
                {
                    T * pEnd = &pDataOut[chunkSize * (datalen / chunkSize)];
                    U256 * pEnd_256 = (U256 *)pEnd;
                    U256 * pIn1_256 = (U256 *)pDataIn1;
                    U256 * pIn2_256 = (U256 *)pDataIn2;
                    U256 * pOut_256 = (U256 *)pDataOut;

                    const U256 m1 = LOADU((U256 *)pIn2_256);

                    // apply 256bit unaligned operations
                    do
                    {
#ifdef RT_COMPILER_MSVC
                        STOREU(pOut_256, MATH_OP256(m1, *pIn1_256));
#else
                        STOREU(pOut_256, MATH_OP256(m1, LOADU(pIn1_256)));
#endif
                        pOut_256 += NUM_LOOPS_UNROLLED;
                        pIn1_256 += NUM_LOOPS_UNROLLED;
                    }
                    while (pOut_256 < pEnd_256);

                    // update pointers to last location of wide pointers
                    pDataIn1 = (T *)pIn1_256;
                    pDataOut = (T *)pOut_256;
                }
                datalen = datalen & (chunkSize - 1);
                for (int64_t i = 0; i < datalen; i++)
                {
                    pDataOut[i] = MATH_OP(pDataIn1[i], arg2);
                }
            }
            break;
        }
    default:
        printf("**error - impossible scalar mode\n");
    }
}

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

template <typename T, typename U128, typename U256, typename MathFunctionPtr, typename MathFunctionConvert,
          typename MathFunctionPtr256>
inline void SimpleMathOpFastDouble(MathFunctionPtr MATH_OP, MathFunctionConvert MATH_CONV, MathFunctionPtr256 MATH_OP256,
                                   void * pDataIn1X, void * pDataIn2X, void * pDataOutX, int64_t len, int32_t scalarMode)
{
    double * pDataOut = (double *)pDataOutX;
    T * pDataIn1 = (T *)pDataIn1X;
    T * pDataIn2 = (T *)pDataIn2X;
    const double dummy = 0;

    int64_t chunkSize = (sizeof(U256)) / sizeof(double);
    int64_t perReg = sizeof(U256) / sizeof(double);

    LOGGING("mathopfastDouble len %llu  chunkSize %llu  perReg %llu\n", len, chunkSize, perReg);

    switch (scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            if (len >= chunkSize)
            {
                double * pEnd = &pDataOut[chunkSize * (len / chunkSize)];
                U256 * pEnd_256 = (U256 *)pEnd;

                U128 * pIn1_256 = (U128 *)pDataIn1;
                U128 * pIn2_256 = (U128 *)pDataIn2;
                U256 * pOut_256 = (U256 *)pDataOut;

                while (pOut_256 < pEnd_256)
                {
                    STOREU(pOut_256, MATH_OP256(MATH_CONV(pIn1_256), MATH_CONV(pIn2_256)));
                    pIn1_256 += 1;
                    pIn2_256 += 1;
                    pOut_256 += 1;
                }

                // update pointers to last location of wide pointers
                pDataIn1 = (T *)pIn1_256;
                pDataIn2 = (T *)pIn2_256;
                pDataOut = (double *)pOut_256;
            }

            len = len & (chunkSize - 1);
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], pDataIn2[i]);
            }

            break;
        }
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            T arg1 = *pDataIn1;
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(arg1, pDataIn2[i]);
            }
            break;
        }
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            T arg2 = *pDataIn2;
            for (int64_t i = 0; i < len; i++)
            {
                pDataOut[i] = MATH_OP(pDataIn1[i], arg2);
            }
            break;
        }
    default:
        printf("**error - impossible scalar mode\n");
    }
}

template <typename T, typename U128, typename U256>
static void SimpleMathOpFastDivDouble(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpFastDouble<T, U128, U256, const double (*)(T, T), const U256 (*)(U128 *), const U256 (*)(U256, U256)>(
        DivOp<T>, CONV_INT32_DOUBLE, DIV_OP_256f64, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

//--------------------------------------------------------------------------------------

template <typename T>
static void SimpleMathOpSlowAdd(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(AddOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowSub(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(SubOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowMul(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(MulOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowDivFloat(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const float (*)(float, T)>(DivOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowDiv(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlowDouble<T, const double (*)(T, T)>(DivOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSubDateTime(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlowDouble<T, const double (*)(T, T)>(SubDateTimeOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSubDates(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(SubDatesOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowFloorDiv(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(FloorDivOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowMod(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(ModOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowLog(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(LogOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowPower(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(PowerOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowRemainder(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(REMAINDER_OP<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowFmod(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(FMOD_OP<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowMin(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(MinOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

template <typename T>
static void SimpleMathOpSlowMax(void * pDataIn1, void * pDataIn2, void * pDataOut, int64_t len, int32_t scalarMode)
{
    return SimpleMathOpSlow<T, const T (*)(T, T)>(MaxOp<T>, pDataIn1, pDataIn2, pDataOut, len, scalarMode);
}

static ANY_TWO_FUNC GetSimpleMathOpFast(int func, int scalarMode, int numpyInType1, int numpyInType2, int numpyOutType,
                                        int * wantedOutType)
{
    LOGGING("GetSimpleMathOpFastFunc %d %d\n", numpyInType1, func);

    switch (func)
    {
    case MATH_OPERATION::ADD:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, OrOp<int8_t>, OR_OP_256>;
        case NPY_FLOAT:
            return SimpleMathOpFastSymmetric<float, __m256, AddOp<float>, ADD_OP_256f32>;
        case NPY_DOUBLE:
            return SimpleMathOpFastSymmetric<double, __m256d, AddOp<double>, ADD_OP_256f64>;
            // proof of concept for i32 addition loop
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, AddOp<int32_t>, ADD_OP_256i32>;
        CASE_NPY_INT64:

            return SimpleMathOpFastSymmetric<int64_t, __m256i, AddOp<int64_t>, ADD_OP_256i64>;
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, AddOp<int16_t>, ADD_OP_256i16>;
        case NPY_INT8:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, AddOp<int8_t>, ADD_OP_256i8>;
        }
        return nullptr;

    case MATH_OPERATION::MUL:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, AndOp<int8_t>, AND_OP_256>;
        case NPY_FLOAT:
            return SimpleMathOpFastSymmetric<float, __m256, MulOp<float>, MUL_OP_256f32>;
        case NPY_DOUBLE:
            return SimpleMathOpFastSymmetric<double, __m256d, MulOp<double>, MUL_OP_256f64>;
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, MulOp<int32_t>, MUL_OP_256i32>;

        // CASE_NPY_INT64:   return SimpleMathOpFast<int64_t, __m256i,
        // MulOp<int64_t>, MUL_OP_256i64>;
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, MulOp<int16_t>, MUL_OP_256i16>;

        // Below the intrinsic to multiply is slower so we disabled it (really wants
        // 32bit -> 64bit)
        // CASE_NPY_UINT32:  return SimpleMathOpFastMul<uint32_t, __m256i>;
        // TODO: 64bit multiply can be done with algo..
        // lo1 * lo2 + (lo1 * hi2) << 32 + (hi1 *lo2) << 32)
        CASE_NPY_UINT64:

            return SimpleMathOpFastSymmetric<uint64_t, __m256i, MulOp<uint64_t>, MUL_OP_256u64>;
        }
        return nullptr;

    case MATH_OPERATION::SUB:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_FLOAT:
            return SimpleMathOpFast<float, __m256, SubOp<float>, SUB_OP_256f32>;
        case NPY_DOUBLE:
            return SimpleMathOpFast<double, __m256d, SubOp<double>, SUB_OP_256f64>;
        CASE_NPY_INT32:
            return SimpleMathOpFast<int32_t, __m256i, SubOp<int32_t>, SUB_OP_256i32>;
        CASE_NPY_INT64:

            return SimpleMathOpFast<int64_t, __m256i, SubOp<int64_t>, SUB_OP_256i64>;
        case NPY_INT16:
            return SimpleMathOpFast<int16_t, __m256i, SubOp<int16_t>, SUB_OP_256i16>;
        case NPY_INT8:
            return SimpleMathOpFast<int8_t, __m256i, SubOp<int8_t>, SUB_OP_256i8>;
        }
        return nullptr;

    case MATH_OPERATION::MIN:
        // This is min for two arrays
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        // TODO: Vector routine needs to be written
        case NPY_FLOAT:
            return SimpleMathOpSlowMin<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowMin<double>;
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, MinOp<int32_t>, MIN_OPi32>;
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, MinOp<int16_t>, MIN_OPi16>;
        case NPY_INT8:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, MinOp<int8_t>, MIN_OPi8>;
        CASE_NPY_UINT32:
            return SimpleMathOpFastSymmetric<uint32_t, __m256i, MinOp<uint32_t>, MIN_OPu32>;
        case NPY_UINT16:
            return SimpleMathOpFastSymmetric<uint16_t, __m256i, MinOp<uint16_t>, MIN_OPu16>;
        case NPY_UINT8:
            return SimpleMathOpFastSymmetric<uint8_t, __m256i, MinOp<uint8_t>, MIN_OPu8>;
        }
        return nullptr;

    case MATH_OPERATION::MAX:
        // This is max for two arrays
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        // TODO: Vector routine needs to be written
        case NPY_FLOAT:
            return SimpleMathOpSlowMax<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowMax<double>;
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, MaxOp<int32_t>, MAX_OPi32>;
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, MaxOp<int16_t>, MAX_OPi16>;
        case NPY_INT8:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, MaxOp<int8_t>, MAX_OPi8>;
        CASE_NPY_UINT32:
            return SimpleMathOpFastSymmetric<uint32_t, __m256i, MaxOp<uint32_t>, MAX_OPu32>;
        case NPY_UINT16:
            return SimpleMathOpFastSymmetric<uint16_t, __m256i, MaxOp<uint16_t>, MAX_OPu16>;
        case NPY_UINT8:
            return SimpleMathOpFastSymmetric<uint8_t, __m256i, MaxOp<uint8_t>, MAX_OPu8>;
        }
        return nullptr;

    case MATH_OPERATION::DIV:
        *wantedOutType = NPY_DOUBLE;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
        {
            if (numpyInType2 == NPY_FLOAT)
                *wantedOutType = NPY_FLOAT;
        }
        else
        {
            if (numpyInType1 == NPY_FLOAT)
                *wantedOutType = NPY_FLOAT;
        }
        switch (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR ? numpyInType2 : numpyInType1)
        {
        case NPY_FLOAT:
            return SimpleMathOpFast<float, __m256, DivOp<float>, DIV_OP_256f32>;
        case NPY_DOUBLE:
            return SimpleMathOpFast<double, __m256d, DivOp<double>, DIV_OP_256f64>;
        CASE_NPY_INT32:
            return SimpleMathOpFastDivDouble<int32_t, __m128i, __m256d>;
        }
        return nullptr;

    case MATH_OPERATION::LOGICAL_AND:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, AndOp<int8_t>, AND_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_AND:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, AndOp<int8_t>, AND_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, AndOp<int16_t>, AND_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, AndOp<int32_t>, AND_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFastSymmetric<int64_t, __m256i, AndOp<int64_t>, AND_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::LOGICAL_OR:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, OrOp<int8_t>, OR_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_OR:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, OrOp<int8_t>, OR_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, OrOp<int16_t>, OR_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, OrOp<int32_t>, OR_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFastSymmetric<int64_t, __m256i, OrOp<int64_t>, OR_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_XOR:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, XorOp<int8_t>, XOR_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, XorOp<int16_t>, XOR_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, XorOp<int32_t>, XOR_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFastSymmetric<int64_t, __m256i, XorOp<int64_t>, XOR_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_XOR_SPECIAL:
        *wantedOutType = numpyInType1;
        // SPECIAL does not change output type
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFastSymmetric<int8_t, __m256i, XorOp<int8_t>, XOR_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFastSymmetric<int16_t, __m256i, XorOp<int16_t>, XOR_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFastSymmetric<int32_t, __m256i, XorOp<int32_t>, XOR_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFastSymmetric<int64_t, __m256i, XorOp<int64_t>, XOR_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_ANDNOT:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFastReverse<int8_t, __m256i, AndNotOp<int8_t>, ANDNOT_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFastReverse<int16_t, __m256i, AndNotOp<int16_t>, ANDNOT_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFastReverse<int32_t, __m256i, AndNotOp<int32_t>, ANDNOT_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFastReverse<int64_t, __m256i, AndNotOp<int64_t>, ANDNOT_OP_256>;
        }
        return nullptr;

    case MATH_OPERATION::BITWISE_NOTAND:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_INT8:
        case NPY_UINT8:
        case NPY_BOOL:
            return SimpleMathOpFast<int8_t, __m256i, AndNotOp<int8_t>, ANDNOT_OP_256>;
        case NPY_UINT16:
        case NPY_INT16:
            return SimpleMathOpFast<int16_t, __m256i, AndNotOp<int16_t>, ANDNOT_OP_256>;
        CASE_NPY_UINT32:
        CASE_NPY_INT32:
            return SimpleMathOpFast<int32_t, __m256i, AndNotOp<int32_t>, ANDNOT_OP_256>;
        CASE_NPY_UINT64:

        CASE_NPY_INT64:

            return SimpleMathOpFast<int64_t, __m256i, AndNotOp<int64_t>, ANDNOT_OP_256>;
        }
        return nullptr;
    }

    return nullptr;
}

//==========================================================
// May return nullptr if it cannot handle type or function
static ANY_TWO_FUNC GetSimpleMathOpSlow(int func, int scalarMode, int numpyInType1, int numpyInType2, int numpyOutType,
                                        int * wantedOutType)
{
    switch (func)
    {
    case MATH_OPERATION::ADD:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpSlowAdd<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowAdd<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowAdd<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowAdd<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowAdd<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowAdd<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowAdd<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowAdd<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowAdd<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowAdd<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowAdd<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowAdd<uint16_t>;
        case NPY_STRING:
            if (numpyInType1 == numpyInType2 && scalarMode == SCALAR_MODE::NO_SCALARS)
            {
            }
            break;

        case NPY_UNICODE:
            if (numpyInType1 == numpyInType2 && scalarMode == SCALAR_MODE::NO_SCALARS)
            {
            }
            break;
        }
        return nullptr;

    case MATH_OPERATION::SUB:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpSlowSub<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowSub<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowSub<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowSub<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowSub<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowSub<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowSub<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowSub<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowSub<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowSub<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowSub<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowSub<uint16_t>;
        }
        return nullptr;

    case MATH_OPERATION::MUL:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        // case NPY_BOOL:   return SimpleMathOpSlowMul<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowMul<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowMul<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowMul<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowMul<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowMul<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowMul<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowMul<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowMul<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowMul<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowMul<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowMul<uint16_t>;
        }
        return nullptr;

    case MATH_OPERATION::DIV:
        *wantedOutType = NPY_DOUBLE;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
        {
            if (numpyInType2 == NPY_FLOAT)
                *wantedOutType = NPY_FLOAT;
        }
        else
        {
            if (numpyInType1 == NPY_FLOAT)
                *wantedOutType = NPY_FLOAT;
        }

        switch (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR ? numpyInType2 : numpyInType1)
        {
        case NPY_BOOL:
            return SimpleMathOpSlowDiv<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowDivFloat<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowDiv<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowDiv<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowDiv<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowDiv<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowDiv<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowDiv<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowDiv<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowDiv<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowDiv<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowDiv<uint16_t>;
        }
        return nullptr;

    case MATH_OPERATION::SUBDATETIMES:
        *wantedOutType = NPY_DOUBLE;
        switch (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR ? numpyInType2 : numpyInType1)
        {
        CASE_NPY_INT32:
            return SimpleMathOpSubDateTime<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSubDateTime<int64_t>;
        }
        printf("bad call to subdatetimes %d %d\n", numpyInType2, numpyInType1);
        return nullptr;

    case MATH_OPERATION::SUBDATES:
        *wantedOutType = NPY_INT32;
        switch (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR ? numpyInType2 : numpyInType1)
        {
        CASE_NPY_INT32:
            return SimpleMathOpSubDates<int32_t>;
        CASE_NPY_INT64:

            *wantedOutType = NPY_INT64;
            return SimpleMathOpSubDates<int64_t>;
        }
        printf("bad call to subdates %d %d\n", numpyInType2, numpyInType1);
        return nullptr;

    case MATH_OPERATION::FLOORDIV:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        // printf("checking floordiv type %d\n", *wantedOutType);
        switch (*wantedOutType)
        {
            // case NPY_BOOL:   return SimpleMathOpSlowMul<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowFloorDiv<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowFloorDiv<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowFloorDiv<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowFloorDiv<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowFloorDiv<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowFloorDiv<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowFloorDiv<uint64_t>;

#ifndef RT_COMPILER_CLANG // possible error with int8 array and
                          // np.floor_divide() with vextractps instruction
        case NPY_INT8:
            return SimpleMathOpSlowFloorDiv<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowFloorDiv<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowFloorDiv<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowFloorDiv<uint16_t>;
#endif
        }
        return nullptr;

    case MATH_OPERATION::MOD:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_BOOL:
            return SimpleMathOpSlowMod<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowRemainder<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowRemainder<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowRemainder<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowMod<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowMod<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowMod<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowMod<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowMod<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowMod<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowMod<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowMod<uint16_t>;
        }
        return nullptr;

    case MATH_OPERATION::POWER:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        // case NPY_BOOL:   return SimpleMathOpSlowPower<bool>;
        case NPY_FLOAT:
            return SimpleMathOpSlowPower<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowPower<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowPower<long double>;
        CASE_NPY_INT32:
            return SimpleMathOpSlowPower<int32_t>;
        CASE_NPY_INT64:

            return SimpleMathOpSlowPower<int64_t>;
        CASE_NPY_UINT32:
            return SimpleMathOpSlowPower<uint32_t>;
        CASE_NPY_UINT64:

            return SimpleMathOpSlowPower<uint64_t>;
        case NPY_INT8:
            return SimpleMathOpSlowPower<int8_t>;
        case NPY_INT16:
            return SimpleMathOpSlowPower<int16_t>;
        case NPY_UINT8:
            return SimpleMathOpSlowPower<uint8_t>;
        case NPY_UINT16:
            return SimpleMathOpSlowPower<uint16_t>;
        }
        return nullptr;

    case MATH_OPERATION::REMAINDER:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_FLOAT:
            return SimpleMathOpSlowRemainder<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowRemainder<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowRemainder<long double>;
        }
        return nullptr;

    case MATH_OPERATION::FMOD:
        *wantedOutType = numpyInType1;
        if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            *wantedOutType = numpyInType2;
        switch (*wantedOutType)
        {
        case NPY_FLOAT:
            return SimpleMathOpSlowFmod<float>;
        case NPY_DOUBLE:
            return SimpleMathOpSlowFmod<double>;
        case NPY_LONGDOUBLE:
            return SimpleMathOpSlowFmod<long double>;
        }
        return nullptr;
    }

    return nullptr;
}

//---------------------------------
// Removes confusing dtype nums in linux vs windows
// Converts dtype 7 to either 5 or 9
// Converts dtype 8 to either 6 or 10
// TODO: Replace with call to our FixupDtype() function?
static int GetNonAmbiguousDtype(int dtype)
{
    switch (dtype)
    {
    CASE_NPY_INT32:
        dtype = 5;
        break;
    CASE_NPY_UINT32:
        dtype = 6;
        break;
    CASE_NPY_INT64:

        dtype = 9;
        break;
    CASE_NPY_UINT64:

        dtype = 10;
        break;
    }
    return dtype;
}

//--------------------------------------------------------------------
// Check to see if we can compute this
static ANY_TWO_FUNC CheckMathOpTwoInputs(int func, int scalarMode, int numpyInType1, int numpyInType2, int numpyOutType,
                                         int * wantedOutType)
{
    ANY_TWO_FUNC pTwoFunc = nullptr;

    LOGGING("CheckMathTwo %d  scalar:%d  type1:%d  type2:%d\n", func, scalarMode, numpyInType1, numpyInType2);

    // Not handling complex, 128bit, voids, objects, or strings here
    if (numpyInType1 <= NPY_LONGDOUBLE && numpyInType2 <= NPY_LONGDOUBLE)
    {
        switch (func)
        {
        case MATH_OPERATION::ADD:
        case MATH_OPERATION::SUB:
        case MATH_OPERATION::MUL:
        case MATH_OPERATION::MOD:
        case MATH_OPERATION::MIN:
        case MATH_OPERATION::MAX:
        case MATH_OPERATION::DIV:
        case MATH_OPERATION::POWER:
        case MATH_OPERATION::REMAINDER: // comes in as mod???
        case MATH_OPERATION::FLOORDIV:
        case MATH_OPERATION::SUBDATETIMES:
        case MATH_OPERATION::SUBDATES:
            if (scalarMode == SCALAR_MODE::NO_SCALARS && numpyInType1 != numpyInType2)
            {
                // Additional check to see if dtypes really are different (this path is
                // rare)
                if (GetNonAmbiguousDtype(numpyInType1) != GetNonAmbiguousDtype(numpyInType2))
                {
                    LOGGING("basicmath types do not match %d %d\n", numpyInType1, numpyInType2);
                    break;
                }
            }

            // Try to get fast one first
            pTwoFunc = GetSimpleMathOpFast(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            if (pTwoFunc == nullptr)
            {
                pTwoFunc = GetSimpleMathOpSlow(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            }
            break;

        case MATH_OPERATION::CMP_EQ:
        case MATH_OPERATION::CMP_NE:
        case MATH_OPERATION::CMP_GT:
        case MATH_OPERATION::CMP_GTE:
        case MATH_OPERATION::CMP_LT:
        case MATH_OPERATION::CMP_LTE:
            pTwoFunc = GetComparisonOpFast(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            if (pTwoFunc == nullptr)
            {
                pTwoFunc = GetComparisonOpSlow(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            }
            break;

        case MATH_OPERATION::LOGICAL_AND:
        case MATH_OPERATION::LOGICAL_OR:
        case MATH_OPERATION::BITWISE_AND:
        case MATH_OPERATION::BITWISE_OR:
        case MATH_OPERATION::BITWISE_XOR:
        case MATH_OPERATION::BITWISE_ANDNOT:
        case MATH_OPERATION::BITWISE_NOTAND:
        case MATH_OPERATION::BITWISE_XOR_SPECIAL:
            if (scalarMode == SCALAR_MODE::NO_SCALARS && numpyInType1 != numpyInType2)
            {
                // Additional check to see if dtypes really are different (this path is
                // rare)
                if (GetNonAmbiguousDtype(numpyInType1) != GetNonAmbiguousDtype(numpyInType2))
                {
                    LOGGING("basicmath bitwise types do not match %d %d\n", numpyInType1, numpyInType2);
                    break;
                }
            }

            // Try to get fast one first
            pTwoFunc = GetSimpleMathOpFast(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            if (pTwoFunc == nullptr)
            {
                pTwoFunc = GetSimpleMathOpSlow(func, scalarMode, numpyInType1, numpyInType2, numpyOutType, wantedOutType);
            }
            break;
        }
    }
    else
    {
        LOGGING("input types are out of range %d %d\n", numpyInType1, numpyInType2);
    }

    return pTwoFunc;
}

static int g_signed_table[65] = {
    NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT16, NPY_INT16, NPY_INT16,
    NPY_INT16, NPY_INT16, NPY_INT16, NPY_INT16, NPY_INT16, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32,
    NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT64,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64
};

// the dtype is signed but the scalar has no sign
static int g_mixedsign_table[65] = {
    NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,  NPY_INT8,   NPY_INT8,  NPY_INT8,  NPY_UINT8, NPY_INT16, NPY_INT16,
    NPY_INT16, NPY_INT16, NPY_INT16, NPY_INT16, NPY_INT16, NPY_UINT16, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32,
    NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32,  NPY_INT32, NPY_INT32, NPY_INT32, NPY_INT32, NPY_UINT32,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,  NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,  NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,
    NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64, NPY_INT64,  NPY_INT64, NPY_INT64, NPY_INT64, NPY_UINT64
};
static int g_bothunsigned_table[65] = {
    NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT8,  NPY_UINT16,
    NPY_UINT16, NPY_UINT16, NPY_UINT16, NPY_UINT16, NPY_UINT16, NPY_UINT16, NPY_UINT16, NPY_UINT32, NPY_UINT32, NPY_UINT32,
    NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT32,
    NPY_UINT32, NPY_UINT32, NPY_UINT32, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64,
    NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64,
    NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64,
    NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64, NPY_UINT64
};

//--------------------------------------------------------------------
class CTwoInputs
{
public:
    CTwoInputs(int numpyOutType1, int funcNumber1)
    {
        numpyOutType = numpyOutType1;
        funcNumber = funcNumber1;
    }

    ~CTwoInputs()
    {
        if (toDelete1)
        {
            Py_DecRef(toDelete1);
        }
        if (toDelete2)
        {
            Py_DecRef(toDelete2);
        }
        if (toDelete3)
        {
            Py_DecRef(toDelete3);
        }
        if (toDelete4)
        {
            Py_DecRef(toDelete4);
        }
    }

    //-------------------------------------------------------------------
    // Called when there is scalar which might be too large for the array
    // For instance a large number with an array of int32s
    // returns a recommended dtype which the array can then be upcasted to
    //
    // TOOO: What about string to unicode or numpy scalar string
    int PossiblyUpcast(PyObject * scalarObject, int dtype)
    {
        if (PyLong_Check(scalarObject))
        {
            // If both bools, we are ok
            if (dtype == NPY_BOOL && PyBool_Check(scalarObject))
            {
                return dtype;
            }

            // check if the array is also integer type
            if (dtype <= NPY_ULONGLONG)
            {
                // this path includes bools
                int overflow = 0;
                int64_t val = PyLong_AsLongLongAndOverflow(scalarObject, &overflow);
                int64_t log2 = 64;
                if (! overflow)
                {
                    int issigned = 0;
                    if (val < 0)
                    {
                        issigned = 1;
                        // Handle case of -128 which can exist in an int8_t
                        val = -(val + 1);
                    }
                    // Check for 0 due to builtin_clz issue
                    log2 = (val != 0) ? 64 - lzcnt_64(val) : 0;

                    // Find the smallest dtype that can be used to represent the number
                    int scalar_dtype;
                    if (issigned)
                    {
                        if (dtype > NPY_BOOL && (dtype & 1) == 0)
                        {
                            // dtype is unsigned and scalar is signed
                            scalar_dtype = g_mixedsign_table[log2];
                        }
                        else
                        {
                            // both are signed
                            scalar_dtype = g_signed_table[log2];
                        }
                    }
                    else
                    {
                        if (dtype > NPY_BOOL && (dtype & 1) == 0)
                        {
                            // if both are unsigned
                            scalar_dtype = g_bothunsigned_table[log2];
                        }
                        else
                        {
                            // dtype is signed, scalar is positive
                            scalar_dtype = g_signed_table[log2];
                        }
                    }
                    int convertType1 = dtype;
                    int convertType2 = scalar_dtype;
                    bool result = GetUpcastType(dtype, scalar_dtype, convertType1, convertType2, funcNumber);
                    LOGGING("Getting upcast %d %d, result %d  convert:%d\n", dtype, scalar_dtype, result, convertType1);
                    if (result)
                    {
                        return convertType1;
                    }
                }
                else
                {
                    // if we have overflow then only ulonglong can handle, else float64
                    LOGGING("overflow dtype %d\n", dtype);
                    switch (dtype)
                    {
                    CASE_NPY_UINT64:
                        break;
                    default:
                        return NPY_FLOAT64;
                    }
                }
            }
        }
        else if (PyFloat_Check(scalarObject))
        {
            if (dtype <= NPY_ULONGLONG)
            {
                return NPY_FLOAT64;
            }
        }
        else if (PyUnicode_Check(scalarObject))
        {
            return NPY_UNICODE;
        }
        else if (PyBytes_Check(scalarObject))
        {
            return NPY_STRING;
        }
        // Check if they passed a numpy scalar
        else if (PyArray_IsScalar(scalarObject, Generic))
        {
            PyArray_Descr * pDescr = PyArray_DescrFromScalar(scalarObject);
            int scalarType = pDescr->type_num;
            Py_DECREF(pDescr);

            if (scalarType == NPY_OBJECT)
            {
                // TODO: extract int from object?
                LOGGING("Got object in possibly upcast\n");
            }
            if (scalarType != dtype && scalarType <= NPY_LONGDOUBLE)
            {
                LOGGING("scalars dont match %d %d\n", scalarType, dtype);
                int convertType1 = dtype;
                int convertType2 = scalarType;
                bool result = GetUpcastType(dtype, scalarType, convertType1, convertType2, funcNumber);
                if (result)
                {
                    return convertType1;
                }
            }
        }
        // return same type
        return dtype;
    }

    //-------------------------------------------------------------------
    //
    // returns nullptr on failure
    PyObject * PossiblyConvertString(int newType, int oldType, PyObject * inObject)
    {
        if (newType == NPY_UNICODE && oldType == NPY_STRING)
        {
            if (PyUnicode_Check(inObject))
            {
                inObject = PyUnicode_AsUTF8String(inObject);
                if (inObject)
                {
                    return inObject;
                }
            }
        }
        else if (newType == NPY_STRING && oldType == NPY_UNICODE)
        {
            if (PyBytes_Check(inObject))
            {
                inObject = PyUnicode_FromObject(inObject);
                if (inObject)
                {
                    return inObject;
                }
            }
        }
        return nullptr;
    }

    //-------------------------------------------------------------------
    // TODO: create struct
    void inline FillArray1(PyObject * array1)
    {
        inArr = (PyArrayObject *)array1;
        itemSize1 = PyArray_ITEMSIZE(inArr);
        numpyInType1 = PyArray_TYPE(inArr);
        ndim1 = PyArray_NDIM(inArr);
        dims1 = PyArray_DIMS(inArr);
        pDataIn1 = PyArray_BYTES(inArr);
        len1 = CALC_ARRAY_LENGTH(ndim1, dims1);
        flags1 = PyArray_FLAGS(inArr);
    }

    void inline FillArray2(PyObject * array2)
    {
        inArr2 = (PyArrayObject *)array2;
        itemSize2 = PyArray_ITEMSIZE(inArr2);
        numpyInType2 = PyArray_TYPE(inArr2);
        ndim2 = PyArray_NDIM(inArr2);
        dims2 = PyArray_DIMS(inArr2);
        pDataIn2 = PyArray_BYTES(inArr2);
        len2 = CALC_ARRAY_LENGTH(ndim2, dims2);
        flags2 = PyArray_FLAGS(inArr2);
    }

    //-------------------------------------------------------------------
    // Arg1: defaultScalarType currently not used
    // returns false on failure
    bool CheckInputs(PyObject * args, int defaultScalarType, int64_t funcNumber)
    {
        if (! PyTuple_CheckExact(args))
        {
            PyErr_Format(PyExc_ValueError, "BasicMath arguments needs to be a tuple");
            return false;
        }

        tupleSize = Py_SIZE(args);

        if (tupleSize != (expectedTupleSize - 1) && tupleSize != expectedTupleSize)
        {
            PyErr_Format(PyExc_ValueError, "BasicMath only takes two or three arguments instead of %llu args", tupleSize);
            return false;
        }

        // PyTuple_GetItem will not increment the ref count
        inObject1 = PyTuple_GET_ITEM(args, 0);
        inObject2 = PyTuple_GET_ITEM(args, 1);

        //// For three inputs, the third inputs cannot be a scalar and must be same
        /// array length and type
        // if (expectedTupleSize == 4) {
        //   // Possibly check array type here
        //   pDataIn3 = PyArray_BYTES((PyArrayObject*)PyTuple_GET_ITEM(args, 2), 0);
        //}

        if (tupleSize == expectedTupleSize)
        {
            // TODO: Get the third item, check if numpy array, if so use as output
            outputObject = PyTuple_GetItem(args, (expectedTupleSize - 1));
        }
        return CheckInputsInternal(defaultScalarType, funcNumber);
    }

    // Called from low level __add__ hook
    // If output is set, it came from an inplace operation and the ref count does
    // not need to be incremented
    bool CheckInputs(PyObject * input1, PyObject * input2, PyObject * output, int64_t funcNumber)
    {
        tupleSize = 2;
        inObject1 = input1;
        inObject2 = input2;
        if (output != nullptr)
        {
            outputObject = output;
            // This will force the output type
            numpyOutType = PyArray_TYPE((PyArrayObject *)outputObject);
        }

        return CheckInputsInternal(0, funcNumber);
    }

    //----------------------------------------
    // Input: numpyInType1 and numpyInType2 must be set
    // Returns false on error
    //
    bool CheckUpcast()
    {
        // Check if we need to upcast one or both of the arrays
        // bool, NPY_INT8, NPY_UINT8
        if (numpyInType1 != numpyInType2)
        {
            // Remove the ambiguous type
            if (numpyInType1 == 7 || numpyInType1 == 8)
            {
                if (sizeof(long) == 4)
                {
                    numpyInType1 -= 2;
                }
                else
                {
                    numpyInType1 += 2;
                }
            }
            if (numpyInType2 == 7 || numpyInType2 == 8)
            {
                if (sizeof(long) == 4)
                {
                    numpyInType2 -= 2;
                }
                else
                {
                    numpyInType2 += 2;
                }
            }
            if (numpyInType1 != numpyInType2)
            {
                int convertType1 = numpyInType1;
                int convertType2 = numpyInType2;
                bool result = GetUpcastType(numpyInType1, numpyInType2, convertType1, convertType2, funcNumber);
                if (result)
                {
                    if (numpyInType1 != convertType1 && convertType1 <= NPY_LONGDOUBLE)
                    {
                        // Convert
                        LOGGING("converting array1 to %d  from  %d and %d\n", convertType1, numpyInType1, numpyInType2);
                        PyObject * newarray1 = ConvertSafeInternal(inArr, convertType1);
                        if (! newarray1)
                        {
                            // Failed to convert
                            return false;
                        }
                        toDelete3 = newarray1;
                        FillArray1(newarray1);
                    }
                    if (numpyInType2 != convertType2 && convertType2 <= NPY_LONGDOUBLE)
                    {
                        // Convert
                        LOGGING("converting array2 to %d  from  %d and %d\n", convertType2, numpyInType2, numpyInType1);
                        PyObject * newarray2 = ConvertSafeInternal(inArr2, convertType2);
                        if (! newarray2)
                        {
                            // Failed to convert
                            return false;
                        }
                        toDelete4 = newarray2;
                        FillArray2(newarray2);
                    }
                }
                else
                {
                    LOGGING("BasicMath cannot upcast %d vs %d\n", numpyInType1, numpyInType2);
                    return false;
                }
            }
        }
        return true;
    }

    // Common routine from array_ufunc or lower level slot hook
    // inObject1 is set and will get analyzed
    // inObject2 is set and will get analyzed
    // funcNumber is passed because certain comparisons with int64_t <-> uint64_t
    // dont need to get upcast to float64
    bool CheckInputsInternal(int defaultScalarType, int64_t funcNumber)
    {
        bool isArray1 = IsFastArrayOrNumpy((PyArrayObject *)inObject1);
        bool isArray2 = IsFastArrayOrNumpy((PyArrayObject *)inObject2);
        isScalar1 = ! isArray1;
        isScalar2 = ! isArray2;

        if (! isArray1)
        {
            isScalar1 = PyArray_IsAnyScalar(inObject1);
            if (! isScalar1)
            {
                LOGGING("Converting object1 to array %s\n", inObject1->ob_type->tp_name);
                // Convert to temp array that must be deleted
                toDelete1 = inObject1 = PyArray_FromAny(inObject1, nullptr, 0, 0, NPY_ARRAY_ENSUREARRAY, nullptr);
                if (inObject1 == nullptr)
                {
                    return false;
                }
                isArray1 = true;
                isScalar1 = false;
            }
            else if (isArray2 && ! outputObject)
            {
                //
                int oldType = PyArray_TYPE((PyArrayObject *)inObject2);
                int newType = PossiblyUpcast(inObject1, oldType);

                // Cannot convert string to unicode yet
                if (newType != oldType)
                {
                    if (newType <= NPY_LONGDOUBLE)
                    {
                        // Convert second array - upcast
                        LOGGING("Converting because of scalar1 to %d from %d   in1:%s\n", newType, oldType,
                                inObject1->ob_type->tp_name);
                        PyObject * newarray2 = ConvertSafeInternal((PyArrayObject *)inObject2, newType);
                        if (! newarray2)
                        {
                            // Failed to convert
                            return false;
                        }
                        // remember to delete temp array
                        toDelete1 = inObject2 = newarray2;
                    }
                    else
                    {
                        // Handle conversion from string to unicode here
                        toDelete1 = PossiblyConvertString(newType, oldType, inObject2);
                        if (! toDelete1)
                            return false;
                        inObject2 = toDelete1;
                        return false; // until we handle string compares
                    }
                }
            }
        }

        if (! isArray2)
        {
            isScalar2 = PyArray_IsAnyScalar(inObject2);
            if (! isScalar2)
            {
                LOGGING("Converting object2 to array %s\n", inObject2->ob_type->tp_name);
                toDelete2 = inObject2 = PyArray_FromAny(inObject2, nullptr, 0, 0, NPY_ARRAY_ENSUREARRAY, nullptr);
                if (inObject2 == nullptr)
                {
                    return false;
                }
                isArray2 = true;
                isScalar2 = false;
            }
            else if (isArray1 && ! outputObject)
            {
                //
                int oldType = PyArray_TYPE((PyArrayObject *)inObject1);
                int newType = PossiblyUpcast(inObject2, oldType);
                if (newType != oldType)
                {
                    if (newType <= NPY_LONGDOUBLE)
                    {
                        // Convert first array - upcast
                        LOGGING("Converting because of scalar2 to %d from %d\n", newType, oldType);
                        PyObject * newarray1 = ConvertSafeInternal((PyArrayObject *)inObject1, newType);
                        if (! newarray1)
                        {
                            // Failed to convert
                            return false;
                        }
                        // remember to delete temp array
                        toDelete2 = inObject1 = newarray1;
                    }
                    else
                    {
                        // Handle conversion from string to unicode here
                        toDelete2 = PossiblyConvertString(newType, oldType, inObject1);
                        if (! toDelete2)
                            return false;
                        inObject1 = toDelete2;
                        return false;
                    }
                }
            }
        }

        LOGGING("scalar: %d %d\n", isScalar1, isScalar2);

        if (isScalar1)
        {
            scalarMode = SCALAR_MODE::FIRST_ARG_SCALAR;
            if (! isScalar2)
            {
                // for most basic math, the two inputs must be same dtype, so check
                // other dtype
                inArr2 = (PyArrayObject *)inObject2;
                numpyInType2 = PyArray_TYPE(inArr2);
            }
            else
            {
                // case when both are scalars, so use output type
                // this case should not occur
                numpyInType2 = numpyOutType;
            }

            // Check the scalar object and consider upcasting the other array
            // For example, the scalar is a float and the second array is integer
            // Or the scalar is a larger integer and the second array is a small
            // integer, or unisgned

            // pDataIn1 will be set pointing to 256 bit value
            bool bResult = ConvertScalarObject(inObject1, &m256Scalar1, numpyInType2, &pDataIn1, &itemSize1);

            LOGGING(
                "Converting first scalar type to %d, bresult: %d,  value: %lld  "
                "inObject1: %s\n",
                numpyInType2, bResult, *(int64_t *)&m256Scalar1, inObject1->ob_type->tp_name);

            // TJD added this Dec 11, 2018
            numpyInType1 = numpyInType2;

            if (! bResult)
                return false;
        }
        else
        {
            // We know the first object is an array
            FillArray1(inObject1);
            // Check strides
            if (ndim1 > 0 && PyArray_DIM(inArr, 0) > 1 && itemSize1 != PyArray_STRIDE(inArr, 0))
            {
                return false;
            }

            //// check for a scalar passed as array of 1 item
            // if (len1 == 1) {
            //   if (!isScalar2) {
            //      LOGGING("setting to first arg scalar\n");
            //      scalarMode = SCALAR_MODE::FIRST_ARG_SCALAR;

            //      // NOTE: does not handle single array of bytes/unicode
            //      bool bResult =
            //         ConvertSingleItemArray(pDataIn1, GetArrDType(inArr),
            //         &m256Scalar1, numpyInType2);

            //      if (!bResult) return false;
            //      pDataIn1 = &m256Scalar1;
            //   }
            //}
        }

        if (isScalar2)
        {
            if (scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            {
                // TJD: new Dec 2018, allow both scalar
                scalarMode = SCALAR_MODE::BOTH_SCALAR;
            }
            else
            {
                LOGGING("setting to second arg scalar\n");
                scalarMode = SCALAR_MODE::SECOND_ARG_SCALAR;
            }

            numpyInType2 = numpyInType1;

            // pDataIn2 will be set pointing to 256 bit value
            bool bResult = ConvertScalarObject(inObject2, &m256Scalar2, numpyInType1, &pDataIn2, &itemSize2);

            LOGGING(
                "Converting second scalar type to %d, bresult: %d,  value: %lld  "
                "type: %s\n",
                numpyInType1, bResult, *(int64_t *)&m256Scalar2, inObject2->ob_type->tp_name);

            if (! bResult)
                return false;
        }
        else
        {
            // We know the second object is an array
            FillArray2(inObject2);

            // Check strides
            if (ndim2 > 0 && PyArray_DIM(inArr2, 0) > 1 && itemSize2 != PyArray_STRIDE((PyArrayObject *)inArr2, 0))
            {
                return false;
            }

            //// check for a scalar passed as array of 1 item
            // if (len2 == 1) {
            //   if (scalarMode != SCALAR_MODE::FIRST_ARG_SCALAR) {
            //      scalarMode = SCALAR_MODE::SECOND_ARG_SCALAR;

            //      // broadcastable check  -- we always checkd for 1?
            //      bool bResult =
            //         ConvertSingleItemArray(pDataIn1, GetArrDType(inArr),
            //         &m256Scalar1, numpyInType2);

            //      if (!bResult) return false;
            //      pDataIn1 = &m256Scalar1;
            //   }
            //}
        }

        // If both are arrays, need to be same size
        if (scalarMode == SCALAR_MODE::NO_SCALARS)
        {
            // Check for single length array
            // Check for np.where condition, the length is known

            if (len1 != len2)
            {
                // if ndim ==0, then first argument was likely a scalar numpy array
                if (len1 == 1 && len2 >= 1)
                {
                    // First object is a scalar
                    scalarMode = SCALAR_MODE::FIRST_ARG_SCALAR;

                    // Check if we can upcast the scalar or if the scalar causes the array
                    // to upcast
                    numpyInType1 = GetArrDType(inArr);
                    int convertType1 = numpyInType1;
                    int convertType2 = numpyInType2;
                    bool result = GetUpcastType(numpyInType1, numpyInType2, convertType1, convertType2, funcNumber);
                    if (result)
                    {
                        LOGGING("setting BM to first arg scalar from %d to %d.  ndim1: %d\n", numpyInType1, convertType1, ndim1);
                        // Convert
                        bool bResult = ConvertSingleItemArray(pDataIn1, numpyInType1, &m256Scalar1, convertType1);

                        if (! bResult)
                            return false;
                        pDataIn1 = &m256Scalar1;
                        numpyInType1 = convertType1;

                        if (numpyInType2 != convertType2 && convertType2 <= NPY_LONGDOUBLE)
                        {
                            // Convert
                            LOGGING("converting array2 to %d  from  %d and %d\n", convertType2, numpyInType2, numpyInType1);
                            PyObject * newarray2 = ConvertSafeInternal(inArr2, convertType2);
                            if (! newarray2)
                            {
                                // Failed to convert
                                return false;
                            }
                            toDelete4 = newarray2;
                            FillArray2(newarray2);
                        }
                    }
                    else
                    {
                        return false;
                    }
                }
                else if (len2 == 1 && len1 >= 1)
                {
                    // Second object is a scalar
                    scalarMode = SCALAR_MODE::SECOND_ARG_SCALAR;

                    // Check if we can upcast the scalar or if the scalar causes the array
                    // to upcast
                    numpyInType2 = GetArrDType(inArr2);
                    int convertType1 = numpyInType1;
                    int convertType2 = numpyInType2;
                    bool result = GetUpcastType(numpyInType1, numpyInType2, convertType1, convertType2, funcNumber);
                    if (result)
                    {
                        LOGGING("setting BM to second arg scalar from %d to %d.  ndim2: %d\n", numpyInType2, convertType2, ndim2);

                        // Convert
                        bool bResult = ConvertSingleItemArray(pDataIn2, numpyInType2, &m256Scalar2, convertType2);

                        if (! bResult)
                            return false;
                        pDataIn2 = &m256Scalar2;
                        numpyInType2 = convertType2;

                        if (numpyInType1 != convertType1 && convertType1 <= NPY_LONGDOUBLE)
                        {
                            // Convert
                            LOGGING("converting array1 to %d  from  %d and %d\n", convertType1, numpyInType1, numpyInType2);
                            PyObject * newarray1 = ConvertSafeInternal(inArr, convertType1);
                            if (! newarray1)
                            {
                                // Failed to convert
                                return false;
                            }
                            toDelete2 = newarray1;
                            FillArray1(newarray1);
                        }
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    LOGGING(
                        "BasicMath needs matching 1 dim x 1 dim array   %llu x %llu. "
                        " Will punt to numpy.\n",
                        len1, len2);
                    // PyErr_Format(PyExc_ValueError, "GenericAnyTwoFunc needs 1 dim x 1
                    // dim array   %d x %d", len1, len2);
                    return false;
                }
            }
            else
            {
                // Check if we need to upcast one or both of the arrays
                // bool, NPY_INT8, NPY_UINT8
                if (! CheckUpcast())
                    return false;
            }
        }

        return true;
    }

    //-----------------------------------------------------------------------
    // returns nullptr on failure
    // will ALLOCATE or use an existing numpy array
    PyArrayObject * CheckOutputs(int wantOutType)
    {
        LOGGING("Checking output type %d   %lld %lld\n", wantOutType, tupleSize, expectedTupleSize);

        // TJD: Possibly make this dynamic?
        // If the number of args is 3, the third argument can be considered the
        // output argument outputObject will be set if came from inplace opeation
        // like x += 3

        if (outputObject)
        {
            if (! PyArray_Check(outputObject))
            {
                LOGGING("BasicMath needs third argument to be an array\n");
                PyErr_Format(PyExc_ValueError, "BasicMath needs third argument to be an array");
                return nullptr;
            }

            if (PyArray_TYPE((PyArrayObject *)outputObject) != wantOutType)
            {
                LOGGING("BasicMath does not match output type %d vs %d\n", PyArray_TYPE((PyArrayObject *)outputObject),
                        wantOutType);
                // PyErr_Format(PyExc_ValueError, "BasicMath does not match output type
                // %d vs %d", PyArray_TYPE((PyArrayObject*)outputObject), wantOutType);
                // NOTE: Now handled in FastArray where we will copy result back into
                // array
                outputObject = nullptr;
            }
            else
            {
                int64_t len3 = ArrayLength((PyArrayObject *)outputObject);
                LOGGING("oref %llu  len1:%lld  len2:%lld  len3: %lld\n", outputObject->ob_refcnt, len1, len2, len3);
                if (len3 < len2 || len3 < len1)
                {
                    // something wrong with output size (gonna crash)
                    return nullptr;
                }

                if (PyArray_DIM(reinterpret_cast<PyArrayObject const *>(outputObject), 0) > 1 &&
                    PyArray_ITEMSIZE((PyArrayObject *)outputObject) != PyArray_STRIDE((PyArrayObject *)outputObject, 0))
                {
                    // output array is strided
                    LOGGING("Punting because output array is strided\n");
                    return nullptr;
                }

                // TJD: since code like this is possible
                // z=rc.addi32(a,b,c)
                // Now z and c point to the same object, thus we must increment the ref
                // count
                Py_INCREF(outputObject);
            }
        }

        // Check if they specified their own output array
        // NOTE: the output array is specified for += operations (in place
        // operations)
        if (outputObject)
        {
            LOGGING("Going to use existing output array %lld %lld\n", len1, len2);
            returnObject = (PyArrayObject *)outputObject;

            // Check to punt on when shifted data (overlapped)
            // a[1:] -= a[:-1]
            char * pOutput = PyArray_BYTES(returnObject);
            char * pOutputEnd = pOutput + ArrayLength(returnObject) * PyArray_ITEMSIZE(returnObject);
            bool bOverlapProblem = false;

            if (len1)
            {
                if (pDataIn1 > pOutput && pDataIn1 < pOutputEnd)
                {
                    printf(
                        "!!!warning: inplace operation has memory overlap in "
                        "beginning for input1\n");
                    bOverlapProblem = true;
                }
                char * pTempEnd = (char *)pDataIn1 + (len1 * itemSize1);
                if (pTempEnd > pOutput && pTempEnd < pOutputEnd)
                {
                    printf(
                        "!!!warning: inplace operation has memory overlap at end for "
                        "input1\n");
                    bOverlapProblem = true;
                }
            }
            if (len2)
            {
                if (pDataIn2 > pOutput && pDataIn2 < pOutputEnd)
                {
                    printf(
                        "!!!warning: inplace operation has memory overlap in "
                        "beginning for input2\n");
                    bOverlapProblem = true;
                }
                char * pTempEnd = (char *)pDataIn2 + (len2 * itemSize2);
                if (pTempEnd > pOutput && pTempEnd < pOutputEnd)
                {
                    printf(
                        "!!!warning: inplace operation has memory overlap at end for "
                        "input2\n");
                    bOverlapProblem = true;
                }
            }

            if (bOverlapProblem)
            {
                return nullptr;
            }
        }
        else
        {
            LOGGING("Going to allocate type %d  %lld  %lld  %d %d  %p  %p\n", wantOutType, len1, len2, ndim1, ndim2, dims1, dims2);

            // TODO: when handling 2 dim arrays that are contiguous and same ORDER,
            // allocate an array with the flags for order 'F' or 'C'
            if (len1 <= 0 && len2 <= 0)
            {
                // Handle empty
                npy_intp dimensions[1] = { 0 };
                returnObject = AllocateNumpyArray(1, dimensions, wantOutType);
            }
            else
                // Check for functions that always output a boolean
                if (len1 >= len2)
                {
                    LOGGING("Going to allocate for len1 type %d  %lld  %lld  %d %lld\n", wantOutType, len1, len2, ndim1,
                            dims1 ? (int64_t)dims1[0] : 0);
                    if (ndim1 > 1)
                    {
                        returnObject = AllocateNumpyArray(ndim1, dims1, wantOutType, 0,
                                                          (flags1 & flags2 & NPY_ARRAY_F_CONTIGUOUS) == NPY_ARRAY_F_CONTIGUOUS);
                    }
                    else
                    {
                        returnObject = AllocateNumpyArray(ndim1, dims1, wantOutType);
                    }
                }
                else
                {
                    LOGGING("Going to allocate for len2 type %d  %lld  %lld  %d %lld\n", wantOutType, len1, len2, ndim2,
                            dims2 ? (int64_t)dims2[0] : 0);
                    if (ndim2 > 1)
                    {
                        returnObject = AllocateNumpyArray(ndim2, dims2, wantOutType, 0,
                                                          (flags1 & flags2 & NPY_ARRAY_F_CONTIGUOUS) == NPY_ARRAY_F_CONTIGUOUS);
                    }
                    else
                    {
                        returnObject = AllocateNumpyArray(ndim2, dims2, wantOutType);
                    }
                }
            CHECK_MEMORY_ERROR(returnObject);
        }

        if (returnObject == nullptr)
        {
            LOGGING("!!! BasicMath has no output\n");
            PyErr_Format(PyExc_ValueError, "BasicMath has no output (possibly out of memory)");
            return nullptr;
        }

        LOGGING("Len1 %llu   len2  %llu  scalarmode:%d\n", len1, len2, scalarMode);
        return returnObject;
    }

    //---------------------------------------------------------------------
    // Data members
    int numpyOutType; // input as first parameter
    int funcNumber;   // input as second parameter

    _m256all m256Scalar1; // possibly store scalar as 256bit
    _m256all m256Scalar2;

    // Default to 0 dimensions, which is a scalar
    int ndim1 = 0;
    int ndim2 = 0;
    int numpyInType1 = 0;
    int numpyInType2 = 0;
    npy_intp * dims1 = 0;
    npy_intp * dims2 = 0;
    int64_t itemSize1 = 0;
    int64_t itemSize2 = 0;
    // For 2 or 3 dim arrays, should be able to calculate length
    int64_t len1 = 0;
    int64_t len2 = 0;
    // flags are ANDED together to see if still F contiguous for > 1 dim output
    // arrays
    int flags1 = NPY_ARRAY_F_CONTIGUOUS;
    int flags2 = NPY_ARRAY_F_CONTIGUOUS;

    void * pDataIn1 = nullptr;
    void * pDataIn2 = nullptr;

    // PyTuple_GetItem will not increment the ref count
    PyObject * inObject1 = nullptr;
    PyObject * inObject2 = nullptr;

    PyObject * toDelete1 = nullptr;
    PyObject * toDelete2 = nullptr;
    PyObject * toDelete3 = nullptr;
    PyObject * toDelete4 = nullptr;

    // often the same as inObject1/2
    PyArrayObject * inArr = nullptr;
    PyArrayObject * inArr2 = nullptr;

    PyArrayObject * returnObject = nullptr;
    PyObject * outputObject = nullptr;

    int32_t scalarMode = SCALAR_MODE::NO_SCALARS;
    bool isScalar1 = false;
    bool isScalar2 = false;

    Py_ssize_t tupleSize = 0;

    // Change this value to 4 for three inputs
    Py_ssize_t expectedTupleSize = 3;

    // For "where" the array size is already known
    // Set the expected length if known ahead of time
    int64_t expectedLength = 0;
};

OLD_CALLBACK bmOldCallback;

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

    int64_t typeSizeIn = OldCallback->FunctionList->InputItemSize;
    int64_t typeSizeOut = OldCallback->FunctionList->OutputItemSize;

    char * pDataInX = (char *)OldCallback->pDataInBase1;
    char * pDataInX2 = (char *)OldCallback->pDataInBase2;
    char * pDataOutX = (char *)OldCallback->pDataOutBase1;
    int64_t lenX;
    int64_t workBlock;

    // As long as there is work to do
    while ((lenX = pstWorkerItem->GetNextWorkBlock(&workBlock)) > 0)
    {
        // Calculate how much to adjust the pointers to get to the data for this
        // work block
        int64_t offsetAdj = pstWorkerItem->BlockSize * workBlock * typeSizeIn;
        int64_t outputAdj = pstWorkerItem->BlockSize * workBlock * typeSizeOut;
        // int64_t outputAdj = offsetAdj;

        THREADLOGGING("workblock %llu   len=%llu  offset=%llu \n", workBlock, lenX, offsetAdj);
        switch (OldCallback->ScalarMode)
        {
        case NO_SCALARS:
            // Process this block of work
            OldCallback->FunctionList->AnyTwoStubCall(pDataInX + offsetAdj, pDataInX2 + offsetAdj, pDataOutX + outputAdj, lenX,
                                                      OldCallback->ScalarMode);
            break;

        case FIRST_ARG_SCALAR:
            // Process this block of work
            OldCallback->FunctionList->AnyTwoStubCall(pDataInX, pDataInX2 + offsetAdj, pDataOutX + outputAdj, lenX,
                                                      OldCallback->ScalarMode);
            break;

        case SECOND_ARG_SCALAR:
            // Process this block of work
            OldCallback->FunctionList->AnyTwoStubCall(pDataInX + offsetAdj, pDataInX2, pDataOutX + outputAdj, lenX,
                                                      OldCallback->ScalarMode);
            break;

        case BOTH_SCALAR:
            printf("** bug both are scalar!\n");
            // Process this block of work
            // FunctionList->AnyTwoStubCall(pDataInX, pDataInX2, pDataOutX +
            // outputAdj, lenX, ScalarMode);
            break;
        }

        // Indicate we completed a block
        didSomeWork = true;

        // tell others we completed this work block
        pstWorkerItem->CompleteWorkBlock();
        // printf("|%d %d", core, (int)workBlock);
    }

    return didSomeWork;
}

//------------------------------------------------------------------------------
//
void WorkTwoStubCall(FUNCTION_LIST * anyTwoStubCall, void * pDataIn, void * pDataIn2, void * pDataOut, int64_t len,
                     int32_t scalarMode)
{
    // printf("worktwostub %d\n", scalarMode);

    // Check if we can use worker threads
    stMATH_WORKER_ITEM * pWorkItem = g_cMathWorker->GetWorkItem(len);

    if (pWorkItem == nullptr)
    {
        // Threading not allowed for this work item, call it directly from main
        // thread
        anyTwoStubCall->AnyTwoStubCall(pDataIn, pDataIn2, pDataOut, len, scalarMode);
        return;
    }

    // Each thread will call this routine with the callbackArg
    pWorkItem->DoWorkCallback = BasicMathThreadCallback;
    pWorkItem->WorkCallbackArg = &bmOldCallback;

    // Store the base values use in a work item
    bmOldCallback.FunctionList = anyTwoStubCall;
    bmOldCallback.pDataInBase1 = pDataIn;
    bmOldCallback.pDataInBase2 = pDataIn2;
    bmOldCallback.pDataOutBase1 = pDataOut;
    bmOldCallback.ScalarMode = scalarMode;

    // This will notify the worker threads of a new work item
    g_cMathWorker->WorkMain(pWorkItem, len, 0);
}

//-----------------------------------------------------------------------------------
// Called when two strings are added
//
PyObject * ConcatTwoStrings(int32_t scalarMode, char * pStr1, char * pStr2, int64_t itemSize1, int64_t itemSize2, int64_t length)
{
    int64_t itemSizeOut = itemSize1 + itemSize2;

    // printf("concat two strings %lld %lld\n", itemSize1, itemSize2);

    PyArrayObject * pNewString = AllocateNumpyArray(1, (npy_intp *)&length, NPY_STRING, itemSizeOut);
    CHECK_MEMORY_ERROR(pNewString);

    if (pNewString)
    {
        char * pOut = (char *)PyArray_BYTES(pNewString);

        switch (scalarMode)
        {
        case SCALAR_MODE::NO_SCALARS:

            for (int64_t i = 0; i < length; i++)
            {
                char * pOut1 = pOut + (itemSizeOut * i);
                char * pEndOut1 = pOut1 + itemSizeOut;
                char * pIn1 = pStr1 + (itemSize1 * i);
                char * pEnd1 = pIn1 + itemSize1;
                char * pIn2 = pStr2 + (itemSize2 * i);
                char * pEnd2 = pIn2 + itemSize2;

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }
                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;

        case SCALAR_MODE::FIRST_ARG_SCALAR:
            for (int64_t i = 0; i < length; i++)
            {
                char * pOut1 = pOut + (itemSizeOut * i);
                char * pEndOut1 = pOut1 + itemSizeOut;
                char * pIn1 = pStr1;
                char * pEnd1 = pIn1 + itemSize1;
                char * pIn2 = pStr2 + (itemSize2 * i);
                char * pEnd2 = pIn2 + itemSize2;

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }
                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;

        case SCALAR_MODE::SECOND_ARG_SCALAR:
            for (int64_t i = 0; i < length; i++)
            {
                char * pOut1 = pOut + (itemSizeOut * i);
                char * pEndOut1 = pOut1 + itemSizeOut;
                char * pIn1 = pStr1 + (itemSize1 * i);
                char * pEnd1 = pIn1 + itemSize1;
                char * pIn2 = pStr2;
                char * pEnd2 = pIn2 + itemSize2;

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }
                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;
        }
    }
    return (PyObject *)pNewString;
}

//-----------------------------------------------------------------------------------
// Called when two unicode are added
//
PyObject * ConcatTwoUnicodes(int32_t scalarMode, uint32_t * pStr1, uint32_t * pStr2, int64_t itemSize1, int64_t itemSize2,
                             int64_t length)
{
    itemSize1 = itemSize1 / 4;
    itemSize2 = itemSize2 / 4;

    int64_t itemSizeOut = itemSize1 + itemSize2;

    // printf("concat two unicode %lld %lld\n", itemSize1, itemSize2);

    PyArrayObject * pNewString = AllocateNumpyArray(1, (npy_intp *)&length, NPY_UNICODE, itemSizeOut * 4);
    CHECK_MEMORY_ERROR(pNewString);

    if (pNewString)
    {
        uint32_t * pOut = (uint32_t *)PyArray_BYTES(pNewString);
        switch (scalarMode)
        {
        case SCALAR_MODE::NO_SCALARS:
            for (int64_t i = 0; i < length; i++)
            {
                uint32_t * pOut1 = pOut + (itemSizeOut * i);
                uint32_t * pEndOut1 = pOut1 + itemSizeOut;
                uint32_t * pIn1 = pStr1 + (itemSize1 * i);
                uint32_t * pEnd1 = pIn1 + itemSize1;
                uint32_t * pIn2 = pStr2 + (itemSize2 * i);
                uint32_t * pEnd2 = pIn2 + itemSize2;

                // printf("%lld %lld ", i, pEndOut1 - pOut1);

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }

                // printf(" > %lld %lld < ", i, pEndOut1 - pOut1);

                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;
        case SCALAR_MODE::FIRST_ARG_SCALAR:
            for (int64_t i = 0; i < length; i++)
            {
                uint32_t * pOut1 = pOut + (itemSizeOut * i);
                uint32_t * pEndOut1 = pOut1 + itemSizeOut;
                uint32_t * pIn1 = pStr1;
                uint32_t * pEnd1 = pIn1 + itemSize1;
                uint32_t * pIn2 = pStr2 + (itemSize2 * i);
                uint32_t * pEnd2 = pIn2 + itemSize2;

                // printf("%lld %lld ", i, pEndOut1 - pOut1);

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }

                // printf(" > %lld %lld < ", i, pEndOut1 - pOut1);

                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;
        case SCALAR_MODE::SECOND_ARG_SCALAR:
            for (int64_t i = 0; i < length; i++)
            {
                uint32_t * pOut1 = pOut + (itemSizeOut * i);
                uint32_t * pEndOut1 = pOut1 + itemSizeOut;
                uint32_t * pIn1 = pStr1 + (itemSize1 * i);
                uint32_t * pEnd1 = pIn1 + itemSize1;
                uint32_t * pIn2 = pStr2;
                uint32_t * pEnd2 = pIn2 + itemSize2;

                // printf("%lld %lld ", i, pEndOut1 - pOut1);

                while (pIn1 < pEnd1 && *pIn1)
                {
                    *pOut1++ = *pIn1++;
                }
                while (pIn2 < pEnd2 && *pIn2)
                {
                    *pOut1++ = *pIn2++;
                }

                // printf(" > %lld %lld < ", i, pEndOut1 - pOut1);

                // add 0s at end
                while (pOut1 < pEndOut1)
                {
                    *pOut1++ = 0;
                }
            }
            break;
        }
    }

    return (PyObject *)pNewString;
}

//-------------------------------------------------------------
// Called when adding two strings
// May return nullptr on failure, else new string array
static PyObject * PossiblyAddUnicode(CTwoInputs & twoInputs)
{
    int32_t inType = -1;

    if (twoInputs.scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR &&
        (twoInputs.numpyInType2 == NPY_STRING || twoInputs.numpyInType2 == NPY_UNICODE))
    {
        inType = twoInputs.numpyInType2;
        LOGGING("First arg scalar %d %lld %lld\n", inType, twoInputs.itemSize1, twoInputs.itemSize2);
    }
    if (twoInputs.scalarMode == SCALAR_MODE::SECOND_ARG_SCALAR &&
        (twoInputs.numpyInType1 == NPY_STRING || twoInputs.numpyInType1 == NPY_UNICODE))
    {
        inType = twoInputs.numpyInType1;
        LOGGING("Second arg scalar %d %lld %lld\n", inType, twoInputs.itemSize1, twoInputs.itemSize2);
    }

    if (twoInputs.scalarMode == SCALAR_MODE::NO_SCALARS && twoInputs.numpyInType1 == twoInputs.numpyInType2)
    {
        inType = twoInputs.numpyInType1;
        LOGGING("Both arg scalar %d %lld %lld\n", inType, twoInputs.itemSize1, twoInputs.itemSize2);
    }
    if (inType != -1)
    {
        if (inType == NPY_STRING)
        {
            // printf("before call\n");
            // NOTE: handle out of memory condition
            return ConcatTwoStrings(twoInputs.scalarMode, (char *)twoInputs.pDataIn1, (char *)twoInputs.pDataIn2,
                                    twoInputs.itemSize1, twoInputs.itemSize2,
                                    twoInputs.len1 >= twoInputs.len2 ? twoInputs.len1 : twoInputs.len2);
        }
        if (inType == NPY_UNICODE)
        {
            // printf("before call\n");
            return ConcatTwoUnicodes(twoInputs.scalarMode, (uint32_t *)twoInputs.pDataIn1, (uint32_t *)twoInputs.pDataIn2,
                                     twoInputs.itemSize1, twoInputs.itemSize2,
                                     twoInputs.len1 >= twoInputs.len2 ? twoInputs.len1 : twoInputs.len2);
        }
    }
    return nullptr;
}

//-----------------------------------------------------------------------------------
// Called when two arrays are used as input params
// Such as min/max functions
// GreaterThan/LessThan
// Call with (tuple(arr1,arr2), funcNumber, numpyOutputType)  or
// Call with (tuple(arr1,arr2,out), funcNumber, numpyOutputType)
//
// New: Bitwise many arr1 |= arr2 | arr3
//
// If NONE is returned, punt to numpy
PyObject * TwoInputsInternal(CTwoInputs & twoInputs, int64_t funcNumber)
{
    // SPECIAL HOOK FOR ADDING TWO STRINGS -------------------------
    if (funcNumber == MATH_OPERATION::ADD)
    {
        PyObject * result = PossiblyAddUnicode(twoInputs);
        if (result)
            return result;
    }

    // Now check if we can perform the function requested
    ANY_TWO_FUNC pTwoFunc = nullptr;
    int wantedOutputType = -1;

    pTwoFunc = CheckMathOpTwoInputs((int)funcNumber, twoInputs.scalarMode, twoInputs.numpyInType1, twoInputs.numpyInType2,
                                    twoInputs.numpyOutType, &wantedOutputType);

    // Place holder for output array
    PyArrayObject * outputArray = nullptr;

    // if (pTwoFunc && wantedOutputType != -1 && twoInputs.numpyOutType ==
    // wantedOutputType) {
    //   printf("Wanted output type %d does not match %d\n", wantedOutputType,
    //   numpyOutputType);
    //} else
    // Aborting - punt this to numpy since we cannot do it/understand it
    if (pTwoFunc && wantedOutputType != -1)
    {
        // This will allocate the output type
        outputArray = twoInputs.CheckOutputs(wantedOutputType);
    }
    LOGGING(" inner %p %p %d  lens: %lld %lld\n", outputArray, pTwoFunc, wantedOutputType, twoInputs.len1, twoInputs.len2);

    // If everything worked so far, perform the math operation
    if (outputArray && pTwoFunc && wantedOutputType != -1)
    {
        void * pDataOut = PyArray_BYTES(outputArray);

        FUNCTION_LIST fl;
        fl.AnyTwoStubCall = pTwoFunc;
        fl.FunctionName = "BMath";

        if (twoInputs.len1 > 0 || twoInputs.len2 > 0)
        {
            // if there is a scalar
            fl.Input1Strides = 0;
            fl.Input2Strides = 0;

            // TODO: handle C vs F two dim arrays (if not C contiguous punt)
            if (twoInputs.len1 > 1 && PyArray_DIM(twoInputs.inArr, 0) > 1)
                fl.Input1Strides = PyArray_STRIDE(twoInputs.inArr, 0);
            if (twoInputs.len2 > 1 && PyArray_DIM(twoInputs.inArr2, 0) > 1)
                fl.Input2Strides = PyArray_STRIDE(twoInputs.inArr2, 0);

            fl.InputItemSize =
                twoInputs.len1 >= twoInputs.len2 ? PyArray_ITEMSIZE(twoInputs.inArr) : PyArray_ITEMSIZE(twoInputs.inArr2);
            fl.OutputItemSize = PyArray_ITEMSIZE(outputArray);

            fl.NumpyOutputType = wantedOutputType;
            fl.NumpyType = twoInputs.len1 >= twoInputs.len2 ? twoInputs.numpyInType1 : twoInputs.numpyInType2;
            fl.TypeOfFunctionCall = TYPE_OF_FUNCTION_CALL::ANY_TWO;

            LOGGING("Calling func %p %p %p %llu %llu %d\n", twoInputs.pDataIn1, twoInputs.pDataIn2, pDataOut, twoInputs.len1,
                    twoInputs.len2, twoInputs.scalarMode);
            // pTwoFunc(twoInputs.pDataIn1, twoInputs.pDataIn2, pDataOut,
            // twoInputs.len1 >= twoInputs.len2 ? twoInputs.len1 : twoInputs.len2,
            // twoInputs.scalarMode);
            WorkTwoStubCall(&fl, twoInputs.pDataIn1, twoInputs.pDataIn2, pDataOut,
                            twoInputs.len1 >= twoInputs.len2 ? twoInputs.len1 : twoInputs.len2, twoInputs.scalarMode);

            // Not sure why this final step is required to call fastarrayview
            // otherwise, the view is not set properly from a new array (recycled
            // arrays do work though)
            LOGGING("Setting view\n");
            return SetFastArrayView(outputArray);
        }

        // RETURN EMPTY ARRAY?
        LOGGING("Setting empty array view\n");
        return SetFastArrayView(outputArray);
    }
    LOGGING("BasicMath punting inner loop\n");
    // punt to numpy
    Py_INCREF(Py_None);
    return Py_None;
}

//----------------------------------------------------------------------------------
// Called when two arrays are used as input params
// Such as min/max functions
// GreaterThan/LessThan
// Call with (tuple(arr1,arr2), funcNumber, numpyOutputType)  or
// Call with (tuple(arr1,arr2,out), funcNumber, numpyOutputType)
//
// New: Bitwise many arr1 |= arr2 | arr3
//
// If NONE is returned, punt to numpy
PyObject * BasicMathTwoInputs(PyObject * self, PyObject * args)
{
    PyObject * tuple = nullptr;
    int64_t funcNumber;
    int64_t numpyOutputType;

    if (Py_SIZE(args) != 3)
    {
        PyErr_Format(PyExc_ValueError, "BasicMathTwoInputs requires three inputs: tuple, long, long");
        return nullptr;
    }

    tuple = PyTuple_GET_ITEM(args, 0);
    funcNumber = PyLong_AsLongLong(PyTuple_GET_ITEM(args, 1));
    numpyOutputType = PyLong_AsLongLong(PyTuple_GET_ITEM(args, 2));

    if (! PyTuple_CheckExact(tuple))
    {
        PyErr_Format(PyExc_ValueError, "BasicMathTwoInputs arguments needs to be a tuple");
        return nullptr;
    }

    // Examine data
    CTwoInputs twoInputs((int)numpyOutputType, (int)funcNumber);

    // Check the inputs to see if the user request makes sense
    bool result = twoInputs.CheckInputs(tuple, 0, funcNumber);

    if (result)
    {
        return TwoInputsInternal(twoInputs, funcNumber);
    }

    // punt to numpy
    Py_INCREF(Py_None);
    return Py_None;
}

//-----------------------------------------------------------------------------------
// Called internally when type class numbermethod called
// inputOut can be nullptr if there is no output array
PyObject * BasicMathTwoInputsFromNumber(PyObject * input1, PyObject * input2, PyObject * output, int64_t funcNumber)
{
    CTwoInputs twoInputs((int)0, (int)funcNumber);

    // Check the inputs to see if the user request makes sense
    bool result = false;
    result = twoInputs.CheckInputs(input1, input2, output, funcNumber);

    if (result)
    {
        return TwoInputsInternal(twoInputs, funcNumber);
    }

    // punt to numpy
    Py_INCREF(Py_None);
    return Py_None;
}

// MT callback
struct WhereCallbackStruct
{
    int8_t * pBooleanMask; // condition
    void * pValuesOut;
    void * pChoice1;
    void * pChoice2;
    int64_t itemSize; // of output array, used for strings
    int64_t strideSize1;
    int64_t strideSize2;
    int64_t strideSize3;
    int32_t scalarMode;
};

template <typename T>
bool WhereCallback(void * callbackArgT, int core, int64_t start, int64_t length)
{
    WhereCallbackStruct * pCallback = (WhereCallbackStruct *)callbackArgT;
    bool * pCondition = (bool *)pCallback->pBooleanMask;
    T * pOutput = (T *)pCallback->pValuesOut;
    T * pInput1 = (T *)pCallback->pChoice1;
    T * pInput2 = (T *)pCallback->pChoice2;
    int64_t strides1 = pCallback->strideSize1;
    int64_t strides2 = pCallback->strideSize2;
    int64_t strides3 = pCallback->strideSize3;

    int64_t itemSize = pCallback->itemSize;
    pCondition = &pCondition[start];
    pOutput = &pOutput[start];

    if (pCallback->scalarMode == SCALAR_MODE::NO_SCALARS || pCallback->scalarMode == SCALAR_MODE::SECOND_ARG_SCALAR)
    {
        pInput1 = &pInput1[start * strides1];
    }
    if (pCallback->scalarMode == SCALAR_MODE::NO_SCALARS || pCallback->scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
    {
        pInput2 = &pInput2[start * strides2];
    }

    LOGGING(
        "[%d] Where start: %lld  length: %lld   scalar: %d  strides1: %lld  "
        "strides2: %lld\n",
        core, start, length, pCallback->scalarMode, strides1, strides2);
    // printf("Where strides:%lld %lld\n", strides1, strides2);

    // BUG BUG what if... itemsize is small?
    // What if condition is strided?
    if (strides1 == 1 && strides2 == 1)
    {
        switch (pCallback->scalarMode)
        {
        case SCALAR_MODE::NO_SCALARS:
            {
                // TODO: future optimization to load 16 bools at once
                // Create 16 ones in a row
                //__m128i allones = _mm_set1_epi8(1);
                //__m128i* pCondition128 = (__m128i*)pCondition;
                //__m128i condition = _mm_min_epu8(*pCondition128, allones);

                // calculate the address delta since we know input1 and input2 are the
                // same itemsize assumes 64bit addressing
                uint64_t delta = pInput2 - pInput1;
                uint64_t ulength = (uint64_t)length;

                for (uint64_t i = 0; i < ulength; i++)
                {
                    // The condition is either 1 or 0.  if 1 we use the address delta to get
                    // from input2 to input1
                    uint64_t test = pCondition[i] == 0;
                    pOutput[i] = pInput1[i + delta * test];
                }

                // This is slower on msft windows compiler
                // does not produce cmove (conditional move instruction)
                // for (uint64_t i = 0; i < ulength; i++) {
                //   // The condition is either 1 or 0.  if 1 we use the address delta to
                //   get from input2 to input1 T* address = pCondition[i] ? pInput1 :
                //   pInput2; pOutput[i] = address[i];
                //}
            }
            break;
        case SCALAR_MODE::FIRST_ARG_SCALAR:
            {
                T input1 = pInput1[0];
                for (int64_t i = 0; i < length; i++)
                {
                    pOutput[i] = pCondition[i] ? input1 : pInput2[i];
                }
            }
            break;
        case SCALAR_MODE::SECOND_ARG_SCALAR:
            {
                T input2 = pInput2[0];
                for (int64_t i = 0; i < length; i++)
                {
                    pOutput[i] = pCondition[i] ? pInput1[i] : input2;
                }
            }
            break;
        case SCALAR_MODE::BOTH_SCALAR:
            {
                T input1 = pInput1[0];
                T input2 = pInput2[0];
                for (int64_t i = 0; i < length; i++)
                {
                    pOutput[i] = pCondition[i] ? input1 : input2;
                }
            }
            break;
        }
    }
    else
    {
        switch (pCallback->scalarMode)
        {
        case SCALAR_MODE::NO_SCALARS:
            {
                for (int64_t i = 0; i < length; i++)
                {
                    if (pCondition[i])
                    {
                        pOutput[i] = pInput1[i * strides1];
                    }
                    else
                    {
                        pOutput[i] = pInput2[i * strides2];
                    }
                }
            }
            break;
        case SCALAR_MODE::FIRST_ARG_SCALAR:
            {
                for (int64_t i = 0; i < length; i++)
                {
                    if (pCondition[i])
                    {
                        pOutput[i] = pInput1[0];
                    }
                    else
                    {
                        pOutput[i] = pInput2[i];
                    }
                }
            }
            break;
        case SCALAR_MODE::SECOND_ARG_SCALAR:
            {
                for (int64_t i = 0; i < length; i++)
                {
                    if (pCondition[i])
                    {
                        pOutput[i] = pInput1[i * strides1];
                    }
                    else
                    {
                        pOutput[i] = pInput2[0];
                    }
                }
            }
            break;
        case SCALAR_MODE::BOTH_SCALAR:
            {
                for (int64_t i = 0; i < length; i++)
                {
                    if (pCondition[i])
                    {
                        pOutput[i] = pInput1[0];
                    }
                    else
                    {
                        pOutput[i] = pInput2[0];
                    }
                }
            }
            break;
        }
    }

    return true;
}

//========================================================================
// callback return for strings or odd itemsizes
bool WhereCallbackString(void * callbackArgT, int core, int64_t start, int64_t length)
{
    WhereCallbackStruct * pCallback = (WhereCallbackStruct *)callbackArgT;
    int8_t * pCondition = pCallback->pBooleanMask;
    char * pOutput = (char *)pCallback->pValuesOut;
    char * pInput1 = (char *)pCallback->pChoice1;
    char * pInput2 = (char *)pCallback->pChoice2;
    int64_t itemSize = pCallback->itemSize;

    // BUG BUG what if... itemsize is small?
    switch (pCallback->scalarMode)
    {
    case SCALAR_MODE::NO_SCALARS:
        {
            for (int64_t i = start; i < (start + length); i++)
            {
                if (pCondition[i])
                {
                    memcpy(&pOutput[i * itemSize], &pInput1[i * itemSize], itemSize);
                }
                else
                {
                    memcpy(&pOutput[i * itemSize], &pInput2[i * itemSize], itemSize);
                }
            }
        }
        break;
    case SCALAR_MODE::FIRST_ARG_SCALAR:
        {
            for (int64_t i = start; i < (start + length); i++)
            {
                if (pCondition[i])
                {
                    memcpy(&pOutput[i * itemSize], pInput1, itemSize);
                }
                else
                {
                    memcpy(&pOutput[i * itemSize], &pInput2[i * itemSize], itemSize);
                }
            }
        }
        break;
    case SCALAR_MODE::SECOND_ARG_SCALAR:
        {
            for (int64_t i = start; i < (start + length); i++)
            {
                if (pCondition[i])
                {
                    memcpy(&pOutput[i * itemSize], &pInput1[i * itemSize], itemSize);
                }
                else
                {
                    memcpy(&pOutput[i * itemSize], pInput2, itemSize);
                }
            }
        }
        break;
    case SCALAR_MODE::BOTH_SCALAR:
        {
            for (int64_t i = start; i < (start + length); i++)
            {
                if (pCondition[i])
                {
                    memcpy(&pOutput[i * itemSize], pInput1, itemSize);
                }
                else
                {
                    memcpy(&pOutput[i * itemSize], pInput2, itemSize);
                }
            }
        }
        break;
    }
    return true;
}

//-----------------------------------------------------------------------------------
// Called when three arrays are used as input params, and one is boolean array
//
// see: np.where
// where(condition, x, y)   condition must be boolean array
//
// rc.where(condition, (x,y), 0, dtype.num, dtype.itemsize)
//
PyObject * Where(PyObject * self, PyObject * args)
{
    PyArrayObject * inBool1 = nullptr;
    PyObject * tuple = nullptr;
    int64_t numpyOutputType;
    int64_t numpyItemSize;

    if (! PyArg_ParseTuple(args, "O!OLL", &PyArray_Type, &inBool1, &tuple, &numpyOutputType, &numpyItemSize))
    {
        return nullptr;
    }

    if (! PyTuple_CheckExact(tuple))
    {
        PyErr_Format(PyExc_ValueError, "Where: second argument needs to be a tuple");
        return nullptr;
    }

    if (PyArray_TYPE(inBool1) != 0)
    {
        PyErr_Format(PyExc_ValueError, "Where: first argument must be a boolean array");
        return nullptr;
    }

    // Examine data
    CTwoInputs twoInputs((int)numpyOutputType, 0);

    // get length of boolean array
    int64_t length = ArrayLength(inBool1);
    twoInputs.expectedLength = length;

    LOGGING("Where: Examining data for func types: %d %d\n", twoInputs.numpyInType1, twoInputs.numpyInType2);

    // Check the inputs to see if the user request makes sense
    bool result = false;

    result = twoInputs.CheckInputs(tuple, 0, MATH_OPERATION::WHERE);

    if (result)
    {
        LOGGING(
            "Where input: scalarmode: %d  in1:%d in2:%d  out:%d  length: %lld  "
            "itemsize: %lld\n",
            twoInputs.scalarMode, twoInputs.numpyInType1, twoInputs.numpyInType2, twoInputs.numpyOutType, length, numpyItemSize);

        // Place holder for output array
        PyArrayObject * outputArray = nullptr;

        outputArray = AllocateNumpyArray(1, (npy_intp *)&length, (int)numpyOutputType, numpyItemSize);

        CHECK_MEMORY_ERROR(outputArray);

        if (outputArray)
        {
            int8_t * pCondition = (int8_t *)PyArray_BYTES(inBool1);
            void * pOutput = PyArray_BYTES(outputArray);

            // for scalar strings, the item size is twoInputs.itemSize1
            int64_t itemSize1 = twoInputs.itemSize1;
            int64_t itemSize2 = twoInputs.itemSize2;
            char * pInput1 = (char *)twoInputs.pDataIn1;
            char * pInput2 = (char *)twoInputs.pDataIn2;

            char * extraAlloc1 = nullptr;
            char * extraAlloc2 = nullptr;

            // If we have scalar strings, we must make sure the length is the same
            if (numpyOutputType == NPY_STRING || numpyOutputType == NPY_UNICODE)
            {
                if (twoInputs.scalarMode == SCALAR_MODE::BOTH_SCALAR || twoInputs.scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
                {
                    // These are small allocs that should never fail
                    extraAlloc1 = (char *)malloc(numpyItemSize);
                    memset(extraAlloc1, 0, numpyItemSize);
                    memcpy(extraAlloc1, pInput1, itemSize1);

                    // swap out for the new one
                    pInput1 = extraAlloc1;
                }
                if (twoInputs.scalarMode == SCALAR_MODE::BOTH_SCALAR || twoInputs.scalarMode == SCALAR_MODE::SECOND_ARG_SCALAR)
                {
                    extraAlloc2 = (char *)malloc(numpyItemSize);
                    memset(extraAlloc2, 0, numpyItemSize);
                    memcpy(extraAlloc2, pInput2, itemSize2);
                    pInput2 = extraAlloc2;
                }
            }

            WhereCallbackStruct WCBS;
            WCBS.itemSize = numpyItemSize;
            WCBS.pBooleanMask = pCondition;
            WCBS.pValuesOut = pOutput;
            WCBS.scalarMode = twoInputs.scalarMode;
            WCBS.pChoice1 = pInput1;
            WCBS.pChoice2 = pInput2;
            WCBS.strideSize1 = 1;
            WCBS.strideSize2 = 1;
            WCBS.strideSize3 = 1;

            // handle strided arrays only when one dimension
            if (twoInputs.scalarMode == SCALAR_MODE::NO_SCALARS || twoInputs.scalarMode == SCALAR_MODE::SECOND_ARG_SCALAR)
            {
                if (twoInputs.ndim1 == 1 && PyArray_DIM(twoInputs.inArr, 0) > 1)
                    WCBS.strideSize1 = PyArray_STRIDE(twoInputs.inArr, 0) / twoInputs.itemSize1;
            }
            if (twoInputs.scalarMode == SCALAR_MODE::NO_SCALARS || twoInputs.scalarMode == SCALAR_MODE::FIRST_ARG_SCALAR)
            {
                if (twoInputs.ndim2 == 1 && PyArray_DIM(twoInputs.inArr2, 0) > 1)
                    WCBS.strideSize2 = PyArray_STRIDE(twoInputs.inArr2, 0) / twoInputs.itemSize2;
            }

            switch (numpyItemSize)
            {
            case 1:
                g_cMathWorker->DoMultiThreadedChunkWork(length, WhereCallback<int8_t>, &WCBS);
                break;
            case 2:
                g_cMathWorker->DoMultiThreadedChunkWork(length, WhereCallback<int16_t>, &WCBS);
                break;
            case 4:
                g_cMathWorker->DoMultiThreadedChunkWork(length, WhereCallback<int32_t>, &WCBS);
                break;
            case 8:
                g_cMathWorker->DoMultiThreadedChunkWork(length, WhereCallback<int64_t>, &WCBS);
                break;
            default:
                g_cMathWorker->DoMultiThreadedChunkWork(length, WhereCallbackString, &WCBS);
                break;
            }

            //==========================================================================================
            if (extraAlloc1)
            {
                free(extraAlloc1);
            }
            if (extraAlloc2 == nullptr)
            {
                free(extraAlloc2);
            }
            return (PyObject *)outputArray;
        }

        PyErr_Format(PyExc_ValueError, "Where could not allocate a numpy array");
        return nullptr;
    }

    // punt to numpy
    Py_INCREF(Py_None);
    return Py_None;
}
