from __future__ import absolute_import

import datetime as _datetime

from typing import Optional
from typing import Union

from .__version__ import __version__
from .constants import DAYS_PER_WEEK
from .constants import FRIDAY
from .constants import HOURS_PER_DAY
from .constants import MINUTES_PER_HOUR
from .constants import MONDAY
from .constants import MONTHS_PER_YEAR
from .constants import SATURDAY
from .constants import SECONDS_PER_DAY
from .constants import SECONDS_PER_HOUR
from .constants import SECONDS_PER_MINUTE
from .constants import SUNDAY
from .constants import THURSDAY
from .constants import TUESDAY
from .constants import WEDNESDAY
from .constants import WEEKS_PER_YEAR
from .constants import YEARS_PER_CENTURY
from .constants import YEARS_PER_DECADE
from .date import Date
from .datetime import DateTime
from .duration import Duration
from .formatting import Formatter
from .helpers import format_diff
from .helpers import get_locale
from .helpers import get_test_now
from .helpers import has_test_now
from .helpers import locale
from .helpers import set_locale
from .helpers import set_test_now
from .helpers import test
from .helpers import week_ends_at
from .helpers import week_starts_at
from .parser import parse
from .period import Period
from .time import Time
from .tz import POST_TRANSITION
from .tz import PRE_TRANSITION
from .tz import TRANSITION_ERROR
from .tz import UTC
from .tz import local_timezone
from .tz import set_local_timezone
from .tz import test_local_timezone
from .tz import timezone
from .tz import timezones
from .tz.timezone import Timezone as _Timezone
from .utils._compat import _HAS_FOLD


_TEST_NOW = None  # type: Optional[DateTime]
_LOCALE = "en"
_WEEK_STARTS_AT = MONDAY
_WEEK_ENDS_AT = SUNDAY

_formatter = Formatter()


def _safe_timezone(obj):
    # type: (Optional[Union[str, float, _datetime.tzinfo, _Timezone]]) -> _Timezone
    """
    Creates a timezone instance
    from a string, Timezone, TimezoneInfo or integer offset.
    """
    if isinstance(obj, _Timezone):
        return obj

    if obj is None or obj == "local":
        return local_timezone()

    if isinstance(obj, (int, float)):
        obj = int(obj * 60 * 60)
    elif isinstance(obj, _datetime.tzinfo):
        # pytz
        if hasattr(obj, "localize"):
            obj = obj.zone
        elif obj.tzname(None) == "UTC":
            return UTC
        else:
            offset = obj.utcoffset(None)

            if offset is None:
                offset = _datetime.timedelta(0)

            obj = int(offset.total_seconds())

    return timezone(obj)


# Public API
def datetime(
    year,  # type: int
    month,  # type: int
    day,  # type: int
    hour=0,  # type: int
    minute=0,  # type: int
    second=0,  # type: int
    microsecond=0,  # type: int
    tz=UTC,  # type: Optional[Union[str, float, _Timezone]]
    dst_rule=POST_TRANSITION,  # type: str
):  # type: (...) -> DateTime
    """
    Creates a new DateTime instance from a specific date and time.
    """
    if tz is not None:
        tz = _safe_timezone(tz)

    if not _HAS_FOLD:
        dt = naive(year, month, day, hour, minute, second, microsecond)
    else:
        dt = _datetime.datetime(year, month, day, hour, minute, second, microsecond)
    if tz is not None:
        dt = tz.convert(dt, dst_rule=dst_rule)

    return DateTime(
        dt.year,
        dt.month,
        dt.day,
        dt.hour,
        dt.minute,
        dt.second,
        dt.microsecond,
        tzinfo=dt.tzinfo,
        fold=dt.fold,
    )


def local(
    year, month, day, hour=0, minute=0, second=0, microsecond=0
):  # type: (int, int, int, int, int, int, int) -> DateTime
    """
    Return a DateTime in the local timezone.
    """
    return datetime(
        year, month, day, hour, minute, second, microsecond, tz=local_timezone()
    )


def naive(
    year, month, day, hour=0, minute=0, second=0, microsecond=0
):  # type: (int, int, int, int, int, int, int) -> DateTime
    """
    Return a naive DateTime.
    """
    return DateTime(year, month, day, hour, minute, second, microsecond)


def date(year, month, day):  # type: (int, int, int) -> Date
    """
    Create a new Date instance.
    """
    return Date(year, month, day)


def time(hour, minute=0, second=0, microsecond=0):  # type: (int, int, int, int) -> Time
    """
    Create a new Time instance.
    """
    return Time(hour, minute, second, microsecond)


def instance(
    dt, tz=UTC
):  # type: (_datetime.datetime, Optional[Union[str, _Timezone]]) -> DateTime
    """
    Create a DateTime instance from a datetime one.
    """
    if not isinstance(dt, _datetime.datetime):
        raise ValueError("instance() only accepts datetime objects.")

    if isinstance(dt, DateTime):
        return dt

    tz = dt.tzinfo or tz

    # Checking for pytz/tzinfo
    if isinstance(tz, _datetime.tzinfo) and not isinstance(tz, _Timezone):
        # pytz
        if hasattr(tz, "localize") and tz.zone:
            tz = tz.zone
        else:
            # We have no sure way to figure out
            # the timezone name, we fallback
            # on a fixed offset
            tz = tz.utcoffset(dt).total_seconds() / 3600

    return datetime(
        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, tz=tz
    )


def now(tz=None):  # type: (Optional[Union[str, _Timezone]]) -> DateTime
    """
    Get a DateTime instance for the current date and time.
    """
    if has_test_now():
        test_instance = get_test_now()
        _tz = _safe_timezone(tz)

        if tz is not None and _tz != test_instance.timezone:
            test_instance = test_instance.in_tz(_tz)

        return test_instance

    if tz is None or tz == "local":
        dt = _datetime.datetime.now(local_timezone())
    elif tz is UTC or tz == "UTC":
        dt = _datetime.datetime.now(UTC)
    else:
        dt = _datetime.datetime.now(UTC)
        tz = _safe_timezone(tz)
        dt = tz.convert(dt)

    return DateTime(
        dt.year,
        dt.month,
        dt.day,
        dt.hour,
        dt.minute,
        dt.second,
        dt.microsecond,
        tzinfo=dt.tzinfo,
        fold=dt.fold if _HAS_FOLD else 0,
    )


def today(tz="local"):  # type: (Union[str, _Timezone]) -> DateTime
    """
    Create a DateTime instance for today.
    """
    return now(tz).start_of("day")


def tomorrow(tz="local"):  # type: (Union[str, _Timezone]) -> DateTime
    """
    Create a DateTime instance for today.
    """
    return today(tz).add(days=1)


def yesterday(tz="local"):  # type: (Union[str, _Timezone]) -> DateTime
    """
    Create a DateTime instance for today.
    """
    return today(tz).subtract(days=1)


def from_format(
    string, fmt, tz=UTC, locale=None,  # noqa
):  # type: (str, str, Union[str, _Timezone], Optional[str]) -> DateTime
    """
    Creates a DateTime instance from a specific format.
    """
    parts = _formatter.parse(string, fmt, now(), locale=locale)
    if parts["tz"] is None:
        parts["tz"] = tz

    return datetime(**parts)


def from_timestamp(
    timestamp, tz=UTC
):  # type: (Union[int, float], Union[str, _Timezone]) -> DateTime
    """
    Create a DateTime instance from a timestamp.
    """
    dt = _datetime.datetime.utcfromtimestamp(timestamp)

    dt = datetime(
        dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond
    )

    if tz is not UTC or tz != "UTC":
        dt = dt.in_timezone(tz)

    return dt


def duration(
    days=0,  # type: float
    seconds=0,  # type: float
    microseconds=0,  # type: float
    milliseconds=0,  # type: float
    minutes=0,  # type: float
    hours=0,  # type: float
    weeks=0,  # type: float
    years=0,  # type: float
    months=0,  # type: float
):  # type: (...) -> Duration
    """
    Create a Duration instance.
    """
    return Duration(
        days=days,
        seconds=seconds,
        microseconds=microseconds,
        milliseconds=milliseconds,
        minutes=minutes,
        hours=hours,
        weeks=weeks,
        years=years,
        months=months,
    )


def period(start, end, absolute=False):  # type: (DateTime, DateTime, bool) -> Period
    """
    Create a Period instance.
    """
    return Period(start, end, absolute=absolute)
