from __future__ import annotations

import datetime

from datetime import time
from datetime import timedelta
from typing import TYPE_CHECKING
from typing import Optional
from typing import cast
from typing import overload

import pendulum

from pendulum.constants import SECS_PER_HOUR
from pendulum.constants import SECS_PER_MIN
from pendulum.constants import USECS_PER_SEC
from pendulum.duration import AbsoluteDuration
from pendulum.duration import Duration
from pendulum.mixins.default import FormattableMixin
from pendulum.tz.timezone import UTC


if TYPE_CHECKING:
    from typing_extensions import Literal
    from typing_extensions import Self
    from typing_extensions import SupportsIndex

    from pendulum.tz.timezone import FixedTimezone
    from pendulum.tz.timezone import Timezone


class Time(FormattableMixin, time):
    """
    Represents a time instance as hour, minute, second, microsecond.
    """

    @classmethod
    def instance(
        cls, t: time, tz: str | Timezone | FixedTimezone | datetime.tzinfo | None = UTC
    ) -> Self:
        tz = t.tzinfo or tz

        if tz is not None:
            tz = pendulum._safe_timezone(tz)

        return cls(t.hour, t.minute, t.second, t.microsecond, tzinfo=tz, fold=t.fold)

    # String formatting
    def __repr__(self) -> str:
        us = ""
        if self.microsecond:
            us = f", {self.microsecond}"

        tzinfo = ""
        if self.tzinfo:
            tzinfo = f", tzinfo={self.tzinfo!r}"

        return (
            f"{self.__class__.__name__}"
            f"({self.hour}, {self.minute}, {self.second}{us}{tzinfo})"
        )

    # Comparisons

    def closest(self, dt1: Time | time, dt2: Time | time) -> Self:
        """
        Get the closest time from the instance.
        """
        dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
        dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)

        if self.diff(dt1).in_seconds() < self.diff(dt2).in_seconds():
            return dt1

        return dt2

    def farthest(self, dt1: Time | time, dt2: Time | time) -> Self:
        """
        Get the farthest time from the instance.
        """
        dt1 = self.__class__(dt1.hour, dt1.minute, dt1.second, dt1.microsecond)
        dt2 = self.__class__(dt2.hour, dt2.minute, dt2.second, dt2.microsecond)

        if self.diff(dt1).in_seconds() > self.diff(dt2).in_seconds():
            return dt1

        return dt2

    # ADDITIONS AND SUBSTRACTIONS

    def add(
        self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
    ) -> Time:
        """
        Add duration to the instance.

        :param hours: The number of hours
        :param minutes: The number of minutes
        :param seconds: The number of seconds
        :param microseconds: The number of microseconds
        """
        from pendulum.datetime import DateTime

        return (
            DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
            .add(
                hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
            )
            .time()
        )

    def subtract(
        self, hours: int = 0, minutes: int = 0, seconds: int = 0, microseconds: int = 0
    ) -> Time:
        """
        Add duration to the instance.

        :param hours: The number of hours
        :type hours: int

        :param minutes: The number of minutes
        :type minutes: int

        :param seconds: The number of seconds
        :type seconds: int

        :param microseconds: The number of microseconds
        :type microseconds: int

        :rtype: Time
        """
        from pendulum.datetime import DateTime

        return (
            DateTime.EPOCH.at(self.hour, self.minute, self.second, self.microsecond)
            .subtract(
                hours=hours, minutes=minutes, seconds=seconds, microseconds=microseconds
            )
            .time()
        )

    def add_timedelta(self, delta: datetime.timedelta) -> Time:
        """
        Add timedelta duration to the instance.

        :param delta: The timedelta instance
        """
        if delta.days:
            raise TypeError("Cannot add timedelta with days to Time.")

        return self.add(seconds=delta.seconds, microseconds=delta.microseconds)

    def subtract_timedelta(self, delta: datetime.timedelta) -> Time:
        """
        Remove timedelta duration from the instance.

        :param delta: The timedelta instance
        """
        if delta.days:
            raise TypeError("Cannot subtract timedelta with days to Time.")

        return self.subtract(seconds=delta.seconds, microseconds=delta.microseconds)

    def __add__(self, other: datetime.timedelta) -> Time:
        if not isinstance(other, timedelta):
            return NotImplemented

        return self.add_timedelta(other)

    @overload
    def __sub__(self, other: time) -> pendulum.Duration: ...

    @overload
    def __sub__(self, other: datetime.timedelta) -> Time: ...

    def __sub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
        if not isinstance(other, (Time, time, timedelta)):
            return NotImplemented

        if isinstance(other, timedelta):
            return self.subtract_timedelta(other)

        if isinstance(other, time):
            if other.tzinfo is not None:
                raise TypeError("Cannot subtract aware times to or from Time.")

            other = self.__class__(
                other.hour, other.minute, other.second, other.microsecond
            )

        return other.diff(self, False)

    @overload
    def __rsub__(self, other: time) -> pendulum.Duration: ...

    @overload
    def __rsub__(self, other: datetime.timedelta) -> Time: ...

    def __rsub__(self, other: time | datetime.timedelta) -> pendulum.Duration | Time:
        if not isinstance(other, (Time, time)):
            return NotImplemented

        if isinstance(other, time):
            if other.tzinfo is not None:
                raise TypeError("Cannot subtract aware times to or from Time.")

            other = self.__class__(
                other.hour, other.minute, other.second, other.microsecond
            )

        return other.__sub__(self)

    # DIFFERENCES

    def diff(self, dt: time | None = None, abs: bool = True) -> Duration:
        """
        Returns the difference between two Time objects as an Duration.

        :param dt: The time to subtract from
        :param abs: Whether to return an absolute duration or not
        """
        if dt is None:
            dt = pendulum.now().time()
        else:
            dt = self.__class__(dt.hour, dt.minute, dt.second, dt.microsecond)

        us1 = (
            self.hour * SECS_PER_HOUR + self.minute * SECS_PER_MIN + self.second
        ) * USECS_PER_SEC

        us2 = (
            dt.hour * SECS_PER_HOUR + dt.minute * SECS_PER_MIN + dt.second
        ) * USECS_PER_SEC

        klass = Duration
        if abs:
            klass = AbsoluteDuration

        return klass(microseconds=us2 - us1)

    def diff_for_humans(
        self,
        other: time | None = None,
        absolute: bool = False,
        locale: str | None = None,
    ) -> str:
        """
        Get the difference in a human readable format in the current locale.

        :param dt: The time to subtract from
        :param absolute: removes time difference modifiers ago, after, etc
        :param locale: The locale to use for localization
        """
        is_now = other is None

        if is_now:
            other = pendulum.now().time()

        diff = self.diff(other)

        return pendulum.format_diff(diff, is_now, absolute, locale)

    # Compatibility methods

    def replace(
        self,
        hour: SupportsIndex | None = None,
        minute: SupportsIndex | None = None,
        second: SupportsIndex | None = None,
        microsecond: SupportsIndex | None = None,
        tzinfo: bool | datetime.tzinfo | Literal[True] | None = True,
        fold: int = 0,
    ) -> Self:
        if tzinfo is True:
            tzinfo = self.tzinfo

        hour = hour if hour is not None else self.hour
        minute = minute if minute is not None else self.minute
        second = second if second is not None else self.second
        microsecond = microsecond if microsecond is not None else self.microsecond

        t = super().replace(
            hour,
            minute,
            second,
            microsecond,
            tzinfo=cast("Optional[datetime.tzinfo]", tzinfo),
            fold=fold,
        )
        return self.__class__(
            t.hour, t.minute, t.second, t.microsecond, tzinfo=t.tzinfo
        )

    def __getnewargs__(self) -> tuple[Time]:
        return (self,)

    def _get_state(
        self, protocol: SupportsIndex = 3
    ) -> tuple[int, int, int, int, datetime.tzinfo | None]:
        tz = self.tzinfo

        return self.hour, self.minute, self.second, self.microsecond, tz

    def __reduce__(
        self,
    ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
        return self.__reduce_ex__(2)

    def __reduce_ex__(
        self, protocol: SupportsIndex
    ) -> tuple[type[Time], tuple[int, int, int, int, datetime.tzinfo | None]]:
        return self.__class__, self._get_state(protocol)


Time.min = Time(0, 0, 0)
Time.max = Time(23, 59, 59, 999999)
Time.resolution = Duration(microseconds=1)
