# -*- coding: utf-8 -*-
# cython: profile=False

from cpython cimport PyFloat_Check, PyComplex_Check

cimport cython
from cython cimport Py_ssize_t

import numpy as np
cimport numpy as cnp
from numpy cimport ndarray, int64_t, uint8_t
cnp.import_array()

cimport util

from tslibs.np_datetime cimport get_timedelta64_value, get_datetime64_value
from tslibs.nattype import NaT

cdef double INF = <double> np.inf
cdef double NEGINF = -INF

cdef int64_t NPY_NAT = util.get_nat()


cdef inline bint is_null_datetimelike(object val):
    # determine if we have a null for a timedelta/datetime (or integer
    # versions)
    if util._checknull(val):
        return True
    elif val is NaT:
        return True
    elif util.is_timedelta64_object(val):
        return val.view('int64') == NPY_NAT
    elif util.is_datetime64_object(val):
        return val.view('int64') == NPY_NAT
    elif util.is_integer_object(val):
        return val == NPY_NAT
    return False


cdef inline bint _check_all_nulls(object val):
    """ utility to check if a value is any type of null """
    cdef bint res
    if PyFloat_Check(val) or PyComplex_Check(val):
        res = val != val
    elif val is NaT:
        res = 1
    elif val is None:
        res = 1
    elif util.is_datetime64_object(val):
        res = get_datetime64_value(val) == NPY_NAT
    elif util.is_timedelta64_object(val):
        res = get_timedelta64_value(val) == NPY_NAT
    else:
        res = 0
    return res


cpdef bint checknull(object val):
    """
    Return boolean describing of the input is NA-like, defined here as any
    of:
     - None
     - nan
     - NaT
     - np.datetime64 representation of NaT
     - np.timedelta64 representation of NaT

    Parameters
    ----------
    val : object

    Returns
    -------
    result : bool

    Notes
    -----
    The difference between `checknull` and `checknull_old` is that `checknull`
    does *not* consider INF or NEGINF to be NA.
    """
    if util.is_float_object(val) or util.is_complex_object(val):
        return val != val  # and val != INF and val != NEGINF
    elif util.is_datetime64_object(val):
        return get_datetime64_value(val) == NPY_NAT
    elif val is NaT:
        return True
    elif util.is_timedelta64_object(val):
        return get_timedelta64_value(val) == NPY_NAT
    elif util.is_array(val):
        return False
    else:
        return util._checknull(val)


cpdef bint checknull_old(object val):
    """
    Return boolean describing of the input is NA-like, defined here as any
    of:
     - None
     - nan
     - INF
     - NEGINF
     - NaT
     - np.datetime64 representation of NaT
     - np.timedelta64 representation of NaT

    Parameters
    ----------
    val : object

    Returns
    -------
    result : bool

    Notes
    -----
    The difference between `checknull` and `checknull_old` is that `checknull`
    does *not* consider INF or NEGINF to be NA.
    """
    if util.is_float_object(val) or util.is_complex_object(val):
        return val != val or val == INF or val == NEGINF
    elif util.is_datetime64_object(val):
        return get_datetime64_value(val) == NPY_NAT
    elif val is NaT:
        return True
    elif util.is_timedelta64_object(val):
        return get_timedelta64_value(val) == NPY_NAT
    elif util.is_array(val):
        return False
    else:
        return util._checknull(val)


cdef inline bint _check_none_nan_inf_neginf(object val):
    try:
        return val is None or (PyFloat_Check(val) and
                               (val != val or val == INF or val == NEGINF))
    except ValueError:
        return False


@cython.wraparound(False)
@cython.boundscheck(False)
def isnaobj(ndarray arr):
    """
    Return boolean mask denoting which elements of a 1-D array are na-like,
    according to the criteria defined in `_check_all_nulls`:
     - None
     - nan
     - NaT
     - np.datetime64 representation of NaT
     - np.timedelta64 representation of NaT

    Parameters
    ----------
    arr : ndarray

    Returns
    -------
    result : ndarray (dtype=np.bool_)
    """
    cdef:
        Py_ssize_t i, n
        object val
        ndarray[uint8_t] result

    assert arr.ndim == 1, "'arr' must be 1-D."

    n = len(arr)
    result = np.empty(n, dtype=np.uint8)
    for i in range(n):
        val = arr[i]
        result[i] = _check_all_nulls(val)
    return result.view(np.bool_)


@cython.wraparound(False)
@cython.boundscheck(False)
def isnaobj_old(ndarray arr):
    """
    Return boolean mask denoting which elements of a 1-D array are na-like,
    defined as being any of:
     - None
     - nan
     - INF
     - NEGINF
     - NaT

    Parameters
    ----------
    arr : ndarray

    Returns
    -------
    result : ndarray (dtype=np.bool_)
    """
    cdef:
        Py_ssize_t i, n
        object val
        ndarray[uint8_t] result

    assert arr.ndim == 1, "'arr' must be 1-D."

    n = len(arr)
    result = np.zeros(n, dtype=np.uint8)
    for i in range(n):
        val = arr[i]
        result[i] = val is NaT or _check_none_nan_inf_neginf(val)
    return result.view(np.bool_)


@cython.wraparound(False)
@cython.boundscheck(False)
def isnaobj2d(ndarray arr):
    """
    Return boolean mask denoting which elements of a 2-D array are na-like,
    according to the criteria defined in `checknull`:
     - None
     - nan
     - NaT
     - np.datetime64 representation of NaT
     - np.timedelta64 representation of NaT

    Parameters
    ----------
    arr : ndarray

    Returns
    -------
    result : ndarray (dtype=np.bool_)

    Notes
    -----
    The difference between `isnaobj2d` and `isnaobj2d_old` is that `isnaobj2d`
    does *not* consider INF or NEGINF to be NA.
    """
    cdef:
        Py_ssize_t i, j, n, m
        object val
        ndarray[uint8_t, ndim=2] result

    assert arr.ndim == 2, "'arr' must be 2-D."

    n, m = (<object> arr).shape
    result = np.zeros((n, m), dtype=np.uint8)
    for i in range(n):
        for j in range(m):
            val = arr[i, j]
            if checknull(val):
                result[i, j] = 1
    return result.view(np.bool_)


@cython.wraparound(False)
@cython.boundscheck(False)
def isnaobj2d_old(ndarray arr):
    """
    Return boolean mask denoting which elements of a 2-D array are na-like,
    according to the criteria defined in `checknull_old`:
     - None
     - nan
     - INF
     - NEGINF
     - NaT
     - np.datetime64 representation of NaT
     - np.timedelta64 representation of NaT

    Parameters
    ----------
    arr : ndarray

    Returns
    -------
    result : ndarray (dtype=np.bool_)

    Notes
    -----
    The difference between `isnaobj2d` and `isnaobj2d_old` is that `isnaobj2d`
    does *not* consider INF or NEGINF to be NA.
    """
    cdef:
        Py_ssize_t i, j, n, m
        object val
        ndarray[uint8_t, ndim=2] result

    assert arr.ndim == 2, "'arr' must be 2-D."

    n, m = (<object> arr).shape
    result = np.zeros((n, m), dtype=np.uint8)
    for i in range(n):
        for j in range(m):
            val = arr[i, j]
            if checknull_old(val):
                result[i, j] = 1
    return result.view(np.bool_)


cpdef bint isposinf_scalar(object val):
    if util.is_float_object(val) and val == INF:
        return True
    else:
        return False


cpdef bint isneginf_scalar(object val):
    if util.is_float_object(val) and val == NEGINF:
        return True
    else:
        return False
