# period frequency constants corresponding to scikits timeseries
# originals
from enum import Enum

from pandas._libs.tslibs.np_datetime cimport (
    NPY_DATETIMEUNIT,
    get_conversion_factor,
)


cdef class PeriodDtypeBase:
    """
    Similar to an actual dtype, this contains all of the information
    describing a PeriodDtype in an integer code.
    """
    # cdef readonly:
    #    PeriodDtypeCode _dtype_code

    def __cinit__(self, PeriodDtypeCode code):
        self._dtype_code = code

    def __eq__(self, other):
        if not isinstance(other, PeriodDtypeBase):
            return False
        if not isinstance(self, PeriodDtypeBase):
            # cython semantics, this is a reversed op
            return False
        return self._dtype_code == other._dtype_code

    @property
    def _freq_group_code(self) -> int:
        # See also: libperiod.get_freq_group
        return (self._dtype_code // 1000) * 1000

    @property
    def _resolution_obj(self) -> "Resolution":
        fgc = self._freq_group_code
        freq_group = FreqGroup(fgc)
        abbrev = _reverse_period_code_map[freq_group.value].split("-")[0]
        if abbrev == "B":
            return Resolution.RESO_DAY
        attrname = _abbrev_to_attrnames[abbrev]
        return Resolution.from_attrname(attrname)

    @property
    def _freqstr(self) -> str:
        # Will be passed to to_offset in Period._maybe_convert_freq
        return _reverse_period_code_map.get(self._dtype_code)

    cpdef int _get_to_timestamp_base(self):
        """
        Return frequency code group used for base of to_timestamp against
        frequency code.

        Return day freq code against longer freq than day.
        Return second freq code against hour between second.

        Returns
        -------
        int
        """
        base = <c_FreqGroup>self._dtype_code
        if base < FR_BUS:
            return FR_DAY
        elif FR_HR <= base <= FR_SEC:
            return FR_SEC
        return base


_period_code_map = {
    # Annual freqs with various fiscal year ends.
    # eg, 2005 for A-FEB runs Mar 1, 2004 to Feb 28, 2005
    "A-DEC": PeriodDtypeCode.A_DEC,  # Annual - December year end
    "A-JAN": PeriodDtypeCode.A_JAN,  # Annual - January year end
    "A-FEB": PeriodDtypeCode.A_FEB,  # Annual - February year end
    "A-MAR": PeriodDtypeCode.A_MAR,  # Annual - March year end
    "A-APR": PeriodDtypeCode.A_APR,  # Annual - April year end
    "A-MAY": PeriodDtypeCode.A_MAY,  # Annual - May year end
    "A-JUN": PeriodDtypeCode.A_JUN,  # Annual - June year end
    "A-JUL": PeriodDtypeCode.A_JUL,  # Annual - July year end
    "A-AUG": PeriodDtypeCode.A_AUG,  # Annual - August year end
    "A-SEP": PeriodDtypeCode.A_SEP,  # Annual - September year end
    "A-OCT": PeriodDtypeCode.A_OCT,  # Annual - October year end
    "A-NOV": PeriodDtypeCode.A_NOV,  # Annual - November year end

    # Quarterly frequencies with various fiscal year ends.
    # eg, Q42005 for Q-OCT runs Aug 1, 2005 to Oct 31, 2005
    "Q-DEC": PeriodDtypeCode.Q_DEC,    # Quarterly - December year end
    "Q-JAN": PeriodDtypeCode.Q_JAN,    # Quarterly - January year end
    "Q-FEB": PeriodDtypeCode.Q_FEB,    # Quarterly - February year end
    "Q-MAR": PeriodDtypeCode.Q_MAR,    # Quarterly - March year end
    "Q-APR": PeriodDtypeCode.Q_APR,    # Quarterly - April year end
    "Q-MAY": PeriodDtypeCode.Q_MAY,    # Quarterly - May year end
    "Q-JUN": PeriodDtypeCode.Q_JUN,    # Quarterly - June year end
    "Q-JUL": PeriodDtypeCode.Q_JUL,    # Quarterly - July year end
    "Q-AUG": PeriodDtypeCode.Q_AUG,    # Quarterly - August year end
    "Q-SEP": PeriodDtypeCode.Q_SEP,    # Quarterly - September year end
    "Q-OCT": PeriodDtypeCode.Q_OCT,    # Quarterly - October year end
    "Q-NOV": PeriodDtypeCode.Q_NOV,    # Quarterly - November year end

    "M": PeriodDtypeCode.M,        # Monthly

    "W-SUN": PeriodDtypeCode.W_SUN,    # Weekly - Sunday end of week
    "W-MON": PeriodDtypeCode.W_MON,    # Weekly - Monday end of week
    "W-TUE": PeriodDtypeCode.W_TUE,    # Weekly - Tuesday end of week
    "W-WED": PeriodDtypeCode.W_WED,    # Weekly - Wednesday end of week
    "W-THU": PeriodDtypeCode.W_THU,    # Weekly - Thursday end of week
    "W-FRI": PeriodDtypeCode.W_FRI,    # Weekly - Friday end of week
    "W-SAT": PeriodDtypeCode.W_SAT,    # Weekly - Saturday end of week

    "B": PeriodDtypeCode.B,        # Business days
    "D": PeriodDtypeCode.D,        # Daily
    "H": PeriodDtypeCode.H,        # Hourly
    "T": PeriodDtypeCode.T,        # Minutely
    "S": PeriodDtypeCode.S,        # Secondly
    "L": PeriodDtypeCode.L,       # Millisecondly
    "U": PeriodDtypeCode.U,       # Microsecondly
    "N": PeriodDtypeCode.N,       # Nanosecondly
}

_reverse_period_code_map = {
    _period_code_map[key]: key for key in _period_code_map}

# Yearly aliases; careful not to put these in _reverse_period_code_map
_period_code_map.update({"Y" + key[1:]: _period_code_map[key]
                         for key in _period_code_map
                         if key.startswith("A-")})

_period_code_map.update({
    "Q": 2000,   # Quarterly - December year end (default quarterly)
    "A": PeriodDtypeCode.A,   # Annual
    "W": 4000,   # Weekly
    "C": 5000,   # Custom Business Day
})

cdef set _month_names = {
    x.split("-")[-1] for x in _period_code_map.keys() if x.startswith("A-")
}

# Map attribute-name resolutions to resolution abbreviations
_attrname_to_abbrevs = {
    "year": "A",
    "quarter": "Q",
    "month": "M",
    "day": "D",
    "hour": "H",
    "minute": "T",
    "second": "S",
    "millisecond": "L",
    "microsecond": "U",
    "nanosecond": "N",
}
cdef dict attrname_to_abbrevs = _attrname_to_abbrevs
cdef dict _abbrev_to_attrnames = {v: k for k, v in attrname_to_abbrevs.items()}


class FreqGroup(Enum):
    # Mirrors c_FreqGroup in the .pxd file
    FR_ANN = c_FreqGroup.FR_ANN
    FR_QTR = c_FreqGroup.FR_QTR
    FR_MTH = c_FreqGroup.FR_MTH
    FR_WK = c_FreqGroup.FR_WK
    FR_BUS = c_FreqGroup.FR_BUS
    FR_DAY = c_FreqGroup.FR_DAY
    FR_HR = c_FreqGroup.FR_HR
    FR_MIN = c_FreqGroup.FR_MIN
    FR_SEC = c_FreqGroup.FR_SEC
    FR_MS = c_FreqGroup.FR_MS
    FR_US = c_FreqGroup.FR_US
    FR_NS = c_FreqGroup.FR_NS
    FR_UND = c_FreqGroup.FR_UND  # undefined

    @staticmethod
    def from_period_dtype_code(code: int) -> "FreqGroup":
        # See also: PeriodDtypeBase._freq_group_code
        code = (code // 1000) * 1000
        return FreqGroup(code)


class Resolution(Enum):
    RESO_NS = c_Resolution.RESO_NS
    RESO_US = c_Resolution.RESO_US
    RESO_MS = c_Resolution.RESO_MS
    RESO_SEC = c_Resolution.RESO_SEC
    RESO_MIN = c_Resolution.RESO_MIN
    RESO_HR = c_Resolution.RESO_HR
    RESO_DAY = c_Resolution.RESO_DAY
    RESO_MTH = c_Resolution.RESO_MTH
    RESO_QTR = c_Resolution.RESO_QTR
    RESO_YR = c_Resolution.RESO_YR

    def __lt__(self, other):
        return self.value < other.value

    def __ge__(self, other):
        return self.value >= other.value

    @property
    def attr_abbrev(self) -> str:
        # string that we can pass to to_offset
        return _attrname_to_abbrevs[self.attrname]

    @property
    def attrname(self) -> str:
        """
        Return datetime attribute name corresponding to this Resolution.

        Examples
        --------
        >>> Resolution.RESO_SEC.attrname
        'second'
        """
        return _reso_str_map[self.value]

    @classmethod
    def from_attrname(cls, attrname: str) -> "Resolution":
        """
        Return resolution str against resolution code.

        Examples
        --------
        >>> Resolution.from_attrname('second')
        <Resolution.RESO_SEC: 3>

        >>> Resolution.from_attrname('second') == Resolution.RESO_SEC
        True
        """
        return cls(_str_reso_map[attrname])

    @classmethod
    def get_reso_from_freqstr(cls, freq: str) -> "Resolution":
        """
        Return resolution code against frequency str.

        `freq` is given by the `offset.freqstr` for some DateOffset object.

        Examples
        --------
        >>> Resolution.get_reso_from_freqstr('H')
        <Resolution.RESO_HR: 5>

        >>> Resolution.get_reso_from_freqstr('H') == Resolution.RESO_HR
        True
        """
        try:
            attr_name = _abbrev_to_attrnames[freq]
        except KeyError:
            # For quarterly and yearly resolutions, we need to chop off
            #  a month string.
            split_freq = freq.split("-")
            if len(split_freq) != 2:
                raise
            if split_freq[1] not in _month_names:
                # i.e. we want e.g. "Q-DEC", not "Q-INVALID"
                raise
            attr_name = _abbrev_to_attrnames[split_freq[0]]

        return cls.from_attrname(attr_name)


class NpyDatetimeUnit(Enum):
    """
    Python-space analogue to NPY_DATETIMEUNIT.
    """
    NPY_FR_Y = NPY_DATETIMEUNIT.NPY_FR_Y
    NPY_FR_M = NPY_DATETIMEUNIT.NPY_FR_M
    NPY_FR_W = NPY_DATETIMEUNIT.NPY_FR_W
    NPY_FR_D = NPY_DATETIMEUNIT.NPY_FR_D
    NPY_FR_h = NPY_DATETIMEUNIT.NPY_FR_h
    NPY_FR_m = NPY_DATETIMEUNIT.NPY_FR_m
    NPY_FR_s = NPY_DATETIMEUNIT.NPY_FR_s
    NPY_FR_ms = NPY_DATETIMEUNIT.NPY_FR_ms
    NPY_FR_us = NPY_DATETIMEUNIT.NPY_FR_us
    NPY_FR_ns = NPY_DATETIMEUNIT.NPY_FR_ns
    NPY_FR_ps = NPY_DATETIMEUNIT.NPY_FR_ps
    NPY_FR_fs = NPY_DATETIMEUNIT.NPY_FR_fs
    NPY_FR_as = NPY_DATETIMEUNIT.NPY_FR_as
    NPY_FR_GENERIC = NPY_DATETIMEUNIT.NPY_FR_GENERIC


cpdef NPY_DATETIMEUNIT get_supported_reso(NPY_DATETIMEUNIT reso):
    # If we have an unsupported reso, return the nearest supported reso.
    if reso == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
        # TODO: or raise ValueError? trying this gives unraisable errors, but
        #  "except? -1" breaks at compile-time for unknown reasons
        return NPY_DATETIMEUNIT.NPY_FR_ns
    if reso < NPY_DATETIMEUNIT.NPY_FR_s:
        return NPY_DATETIMEUNIT.NPY_FR_s
    elif reso > NPY_DATETIMEUNIT.NPY_FR_ns:
        return NPY_DATETIMEUNIT.NPY_FR_ns
    return reso


def is_supported_unit(NPY_DATETIMEUNIT reso):
    return (
        reso == NPY_DATETIMEUNIT.NPY_FR_ns
        or reso == NPY_DATETIMEUNIT.NPY_FR_us
        or reso == NPY_DATETIMEUNIT.NPY_FR_ms
        or reso == NPY_DATETIMEUNIT.NPY_FR_s
    )


cpdef str npy_unit_to_abbrev(NPY_DATETIMEUNIT unit):
    if unit == NPY_DATETIMEUNIT.NPY_FR_ns or unit == NPY_DATETIMEUNIT.NPY_FR_GENERIC:
        # generic -> default to nanoseconds
        return "ns"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_us:
        return "us"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_ms:
        return "ms"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_s:
        return "s"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_m:
        return "m"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_h:
        return "h"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_D:
        return "D"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_W:
        return "W"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_M:
        return "M"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_Y:
        return "Y"

    # Checks for not-really-supported units go at the end, as we don't expect
    #  to see these often
    elif unit == NPY_DATETIMEUNIT.NPY_FR_ps:
        return "ps"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_fs:
        return "fs"
    elif unit == NPY_DATETIMEUNIT.NPY_FR_as:
        return "as"

    else:
        raise NotImplementedError(unit)


cpdef NPY_DATETIMEUNIT abbrev_to_npy_unit(str abbrev):
    if abbrev == "Y":
        return NPY_DATETIMEUNIT.NPY_FR_Y
    elif abbrev == "M":
        return NPY_DATETIMEUNIT.NPY_FR_M
    elif abbrev == "W":
        return NPY_DATETIMEUNIT.NPY_FR_W
    elif abbrev == "D" or abbrev == "d":
        return NPY_DATETIMEUNIT.NPY_FR_D
    elif abbrev == "h":
        return NPY_DATETIMEUNIT.NPY_FR_h
    elif abbrev == "m":
        return NPY_DATETIMEUNIT.NPY_FR_m
    elif abbrev == "s":
        return NPY_DATETIMEUNIT.NPY_FR_s
    elif abbrev == "ms":
        return NPY_DATETIMEUNIT.NPY_FR_ms
    elif abbrev == "us":
        return NPY_DATETIMEUNIT.NPY_FR_us
    elif abbrev == "ns":
        return NPY_DATETIMEUNIT.NPY_FR_ns
    elif abbrev == "ps":
        return NPY_DATETIMEUNIT.NPY_FR_ps
    elif abbrev == "fs":
        return NPY_DATETIMEUNIT.NPY_FR_fs
    elif abbrev == "as":
        return NPY_DATETIMEUNIT.NPY_FR_as
    elif abbrev is None:
        return NPY_DATETIMEUNIT.NPY_FR_GENERIC
    else:
        raise ValueError(f"Unrecognized unit {abbrev}")


cdef NPY_DATETIMEUNIT freq_group_code_to_npy_unit(int freq) nogil:
    """
    Convert the freq to the corresponding NPY_DATETIMEUNIT to pass
    to npy_datetimestruct_to_datetime.
    """
    if freq == FR_MTH:
        return NPY_DATETIMEUNIT.NPY_FR_M
    elif freq == FR_DAY:
        return NPY_DATETIMEUNIT.NPY_FR_D
    elif freq == FR_HR:
        return NPY_DATETIMEUNIT.NPY_FR_h
    elif freq == FR_MIN:
        return NPY_DATETIMEUNIT.NPY_FR_m
    elif freq == FR_SEC:
        return NPY_DATETIMEUNIT.NPY_FR_s
    elif freq == FR_MS:
        return NPY_DATETIMEUNIT.NPY_FR_ms
    elif freq == FR_US:
        return NPY_DATETIMEUNIT.NPY_FR_us
    elif freq == FR_NS:
        return NPY_DATETIMEUNIT.NPY_FR_ns
    elif freq == FR_UND:
        # Default to Day
        return NPY_DATETIMEUNIT.NPY_FR_D


# TODO: use in _matplotlib.converter?
cpdef int64_t periods_per_day(
    NPY_DATETIMEUNIT reso=NPY_DATETIMEUNIT.NPY_FR_ns
) except? -1:
    """
    How many of the given time units fit into a single day?
    """
    return get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_D, reso)


cpdef int64_t periods_per_second(NPY_DATETIMEUNIT reso) except? -1:
    return get_conversion_factor(NPY_DATETIMEUNIT.NPY_FR_s, reso)


cdef dict _reso_str_map = {
    Resolution.RESO_NS.value: "nanosecond",
    Resolution.RESO_US.value: "microsecond",
    Resolution.RESO_MS.value: "millisecond",
    Resolution.RESO_SEC.value: "second",
    Resolution.RESO_MIN.value: "minute",
    Resolution.RESO_HR.value: "hour",
    Resolution.RESO_DAY.value: "day",
    Resolution.RESO_MTH.value: "month",
    Resolution.RESO_QTR.value: "quarter",
    Resolution.RESO_YR.value: "year",
}

cdef dict _str_reso_map = {v: k for k, v in _reso_str_map.items()}

cdef dict npy_unit_to_attrname = {
    NPY_DATETIMEUNIT.NPY_FR_Y: "year",
    NPY_DATETIMEUNIT.NPY_FR_M: "month",
    NPY_DATETIMEUNIT.NPY_FR_D: "day",
    NPY_DATETIMEUNIT.NPY_FR_h: "hour",
    NPY_DATETIMEUNIT.NPY_FR_m: "minute",
    NPY_DATETIMEUNIT.NPY_FR_s: "second",
    NPY_DATETIMEUNIT.NPY_FR_ms: "millisecond",
    NPY_DATETIMEUNIT.NPY_FR_us: "microsecond",
    NPY_DATETIMEUNIT.NPY_FR_ns: "nanosecond",
}
cdef dict attrname_to_npy_unit = {v: k for k, v in npy_unit_to_attrname.items()}
