import cython

from cpython.datetime cimport date, datetime, time, tzinfo

import numpy as np

from numpy cimport int64_t, intp_t, ndarray

from .conversion cimport normalize_i8_stamp

from .dtypes import Resolution

from .nattype cimport NPY_NAT, c_NaT as NaT
from .np_datetime cimport dt64_to_dtstruct, npy_datetimestruct
from .offsets cimport to_offset
from .period cimport get_period_ordinal
from .timestamps cimport create_timestamp_from_ts
from .timezones cimport get_dst_info, is_tzlocal, is_utc
from .tzconversion cimport tz_convert_utc_to_tzlocal

# -------------------------------------------------------------------------

cdef inline object create_datetime_from_ts(
    int64_t value,
    npy_datetimestruct dts,
    tzinfo tz,
    object freq,
    bint fold,
):
    """
    Convenience routine to construct a datetime.datetime from its parts.
    """
    return datetime(
        dts.year, dts.month, dts.day, dts.hour, dts.min, dts.sec, dts.us,
        tz, fold=fold,
    )


cdef inline object create_date_from_ts(
    int64_t value,
    npy_datetimestruct dts,
    tzinfo tz,
    object freq,
    bint fold
):
    """
    Convenience routine to construct a datetime.date from its parts.
    """
    # GH#25057 add fold argument to match other func_create signatures
    return date(dts.year, dts.month, dts.day)


cdef inline object create_time_from_ts(
    int64_t value,
    npy_datetimestruct dts,
    tzinfo tz,
    object freq,
    bint fold
):
    """
    Convenience routine to construct a datetime.time from its parts.
    """
    return time(dts.hour, dts.min, dts.sec, dts.us, tz, fold=fold)


@cython.wraparound(False)
@cython.boundscheck(False)
def ints_to_pydatetime(
    const int64_t[:] arr,
    tzinfo tz=None,
    object freq=None,
    bint fold=False,
    str box="datetime"
):
    """
    Convert an i8 repr to an ndarray of datetimes, date, time or Timestamp.

    Parameters
    ----------
    arr : array of i8
    tz : str, optional
         convert to this timezone
    freq : str/Offset, optional
         freq to convert
    fold : bint, default is 0
        Due to daylight saving time, one wall clock time can occur twice
        when shifting from summer to winter time; fold describes whether the
        datetime-like corresponds  to the first (0) or the second time (1)
        the wall clock hits the ambiguous time

        .. versionadded:: 1.1.0
    box : {'datetime', 'timestamp', 'date', 'time'}, default 'datetime'
        * If datetime, convert to datetime.datetime
        * If date, convert to datetime.date
        * If time, convert to datetime.time
        * If Timestamp, convert to pandas.Timestamp

    Returns
    -------
    ndarray of dtype specified by box
    """
    cdef:
        Py_ssize_t i, n = len(arr)
        ndarray[int64_t] trans
        int64_t[:] deltas
        intp_t[:] pos
        npy_datetimestruct dts
        object dt, new_tz
        str typ
        int64_t value, local_value, delta = NPY_NAT  # dummy for delta
        ndarray[object] result = np.empty(n, dtype=object)
        object (*func_create)(int64_t, npy_datetimestruct, tzinfo, object, bint)
        bint use_utc = False, use_tzlocal = False, use_fixed = False
        bint use_pytz = False

    if box == "date":
        assert (tz is None), "tz should be None when converting to date"

        func_create = create_date_from_ts
    elif box == "timestamp":
        func_create = create_timestamp_from_ts

        if isinstance(freq, str):
            freq = to_offset(freq)
    elif box == "time":
        func_create = create_time_from_ts
    elif box == "datetime":
        func_create = create_datetime_from_ts
    else:
        raise ValueError(
            "box must be one of 'datetime', 'date', 'time' or 'timestamp'"
        )

    if is_utc(tz) or tz is None:
        use_utc = True
    elif is_tzlocal(tz):
        use_tzlocal = True
    else:
        trans, deltas, typ = get_dst_info(tz)
        if typ not in ["pytz", "dateutil"]:
            # static/fixed; in this case we know that len(delta) == 1
            use_fixed = True
            delta = deltas[0]
        else:
            pos = trans.searchsorted(arr, side="right") - 1
            use_pytz = typ == "pytz"

    for i in range(n):
        new_tz = tz
        value = arr[i]

        if value == NPY_NAT:
            result[i] = <object>NaT
        else:
            if use_utc:
                local_value = value
            elif use_tzlocal:
                local_value = tz_convert_utc_to_tzlocal(value, tz)
            elif use_fixed:
                local_value = value + delta
            elif not use_pytz:
                # i.e. dateutil
                # no zone-name change for dateutil tzs - dst etc
                # represented in single object.
                local_value = value + deltas[pos[i]]
            else:
                # pytz
                # find right representation of dst etc in pytz timezone
                new_tz = tz._tzinfos[tz._transition_info[pos[i]]]
                local_value = value + deltas[pos[i]]

            dt64_to_dtstruct(local_value, &dts)
            result[i] = func_create(value, dts, new_tz, freq, fold)

    return result


# -------------------------------------------------------------------------

cdef:
    int RESO_NS = 0
    int RESO_US = 1
    int RESO_MS = 2
    int RESO_SEC = 3
    int RESO_MIN = 4
    int RESO_HR = 5
    int RESO_DAY = 6
    int RESO_MTH = 7
    int RESO_QTR = 8
    int RESO_YR = 9


cdef inline int _reso_stamp(npy_datetimestruct *dts):
    if dts.us != 0:
        if dts.us % 1000 == 0:
            return RESO_MS
        return RESO_US
    elif dts.sec != 0:
        return RESO_SEC
    elif dts.min != 0:
        return RESO_MIN
    elif dts.hour != 0:
        return RESO_HR
    return RESO_DAY


def get_resolution(const int64_t[:] stamps, tzinfo tz=None):
    cdef:
        Py_ssize_t i, n = len(stamps)
        npy_datetimestruct dts
        int reso = RESO_DAY, curr_reso
        ndarray[int64_t] trans
        int64_t[:] deltas
        Py_ssize_t[:] pos
        int64_t local_val, delta

    if is_utc(tz) or tz is None:
        for i in range(n):
            if stamps[i] == NPY_NAT:
                continue
            dt64_to_dtstruct(stamps[i], &dts)
            curr_reso = _reso_stamp(&dts)
            if curr_reso < reso:
                reso = curr_reso
    elif is_tzlocal(tz):
        for i in range(n):
            if stamps[i] == NPY_NAT:
                continue
            local_val = tz_convert_utc_to_tzlocal(stamps[i], tz)
            dt64_to_dtstruct(local_val, &dts)
            curr_reso = _reso_stamp(&dts)
            if curr_reso < reso:
                reso = curr_reso
    else:
        # Adjust datetime64 timestamp, recompute datetimestruct
        trans, deltas, typ = get_dst_info(tz)

        if typ not in ["pytz", "dateutil"]:
            # static/fixed; in this case we know that len(delta) == 1
            delta = deltas[0]
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    continue
                dt64_to_dtstruct(stamps[i] + delta, &dts)
                curr_reso = _reso_stamp(&dts)
                if curr_reso < reso:
                    reso = curr_reso
        else:
            pos = trans.searchsorted(stamps, side="right") - 1
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    continue
                dt64_to_dtstruct(stamps[i] + deltas[pos[i]], &dts)
                curr_reso = _reso_stamp(&dts)
                if curr_reso < reso:
                    reso = curr_reso

    return Resolution(reso)


# -------------------------------------------------------------------------

@cython.wraparound(False)
@cython.boundscheck(False)
cpdef ndarray[int64_t] normalize_i8_timestamps(const int64_t[:] stamps, tzinfo tz):
    """
    Normalize each of the (nanosecond) timezone aware timestamps in the given
    array by rounding down to the beginning of the day (i.e. midnight).
    This is midnight for timezone, `tz`.

    Parameters
    ----------
    stamps : int64 ndarray
    tz : tzinfo or None

    Returns
    -------
    result : int64 ndarray of converted of normalized nanosecond timestamps
    """
    cdef:
        Py_ssize_t i, n = len(stamps)
        int64_t[:] result = np.empty(n, dtype=np.int64)
        ndarray[int64_t] trans
        int64_t[:] deltas
        str typ
        Py_ssize_t[:] pos
        int64_t delta, local_val

    if tz is None or is_utc(tz):
        with nogil:
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                local_val = stamps[i]
                result[i] = normalize_i8_stamp(local_val)
    elif is_tzlocal(tz):
        for i in range(n):
            if stamps[i] == NPY_NAT:
                result[i] = NPY_NAT
                continue
            local_val = tz_convert_utc_to_tzlocal(stamps[i], tz)
            result[i] = normalize_i8_stamp(local_val)
    else:
        # Adjust datetime64 timestamp, recompute datetimestruct
        trans, deltas, typ = get_dst_info(tz)

        if typ not in ["pytz", "dateutil"]:
            # static/fixed; in this case we know that len(delta) == 1
            delta = deltas[0]
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                local_val = stamps[i] + delta
                result[i] = normalize_i8_stamp(local_val)
        else:
            pos = trans.searchsorted(stamps, side="right") - 1
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                local_val = stamps[i] + deltas[pos[i]]
                result[i] = normalize_i8_stamp(local_val)

    return result.base  # `.base` to access underlying ndarray


@cython.wraparound(False)
@cython.boundscheck(False)
def is_date_array_normalized(const int64_t[:] stamps, tzinfo tz=None):
    """
    Check if all of the given (nanosecond) timestamps are normalized to
    midnight, i.e. hour == minute == second == 0.  If the optional timezone
    `tz` is not None, then this is midnight for this timezone.

    Parameters
    ----------
    stamps : int64 ndarray
    tz : tzinfo or None

    Returns
    -------
    is_normalized : bool True if all stamps are normalized
    """
    cdef:
        Py_ssize_t i, n = len(stamps)
        ndarray[int64_t] trans
        int64_t[:] deltas
        intp_t[:] pos
        int64_t local_val, delta
        str typ
        int64_t day_nanos = 24 * 3600 * 1_000_000_000

    if tz is None or is_utc(tz):
        for i in range(n):
            local_val = stamps[i]
            if local_val % day_nanos != 0:
                return False

    elif is_tzlocal(tz):
        for i in range(n):
            local_val = tz_convert_utc_to_tzlocal(stamps[i], tz)
            if local_val % day_nanos != 0:
                return False
    else:
        trans, deltas, typ = get_dst_info(tz)

        if typ not in ["pytz", "dateutil"]:
            # static/fixed; in this case we know that len(delta) == 1
            delta = deltas[0]
            for i in range(n):
                # Adjust datetime64 timestamp, recompute datetimestruct
                local_val = stamps[i] + delta
                if local_val % day_nanos != 0:
                    return False

        else:
            pos = trans.searchsorted(stamps) - 1
            for i in range(n):
                # Adjust datetime64 timestamp, recompute datetimestruct
                local_val = stamps[i] + deltas[pos[i]]
                if local_val % day_nanos != 0:
                    return False

    return True


# -------------------------------------------------------------------------


@cython.wraparound(False)
@cython.boundscheck(False)
def dt64arr_to_periodarr(const int64_t[:] stamps, int freq, tzinfo tz):
    cdef:
        Py_ssize_t n = len(stamps)
        int64_t[:] result = np.empty(n, dtype=np.int64)
        ndarray[int64_t] trans
        int64_t[:] deltas
        Py_ssize_t[:] pos
        npy_datetimestruct dts
        int64_t local_val

    if is_utc(tz) or tz is None:
        with nogil:
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                dt64_to_dtstruct(stamps[i], &dts)
                result[i] = get_period_ordinal(&dts, freq)

    elif is_tzlocal(tz):
        for i in range(n):
            if stamps[i] == NPY_NAT:
                result[i] = NPY_NAT
                continue
            local_val = tz_convert_utc_to_tzlocal(stamps[i], tz)
            dt64_to_dtstruct(local_val, &dts)
            result[i] = get_period_ordinal(&dts, freq)
    else:
        # Adjust datetime64 timestamp, recompute datetimestruct
        trans, deltas, typ = get_dst_info(tz)

        if typ not in ["pytz", "dateutil"]:
            # static/fixed; in this case we know that len(delta) == 1
            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                dt64_to_dtstruct(stamps[i] + deltas[0], &dts)
                result[i] = get_period_ordinal(&dts, freq)
        else:
            pos = trans.searchsorted(stamps, side="right") - 1

            for i in range(n):
                if stamps[i] == NPY_NAT:
                    result[i] = NPY_NAT
                    continue
                dt64_to_dtstruct(stamps[i] + deltas[pos[i]], &dts)
                result[i] = get_period_ordinal(&dts, freq)

    return result.base  # .base to get underlying ndarray
