# -*- coding: utf-8 -*-

# Copyright (c) 2021, Brandon Nielsen
# All rights reserved.
#
# This software may be modified and distributed under the terms
# of the BSD license.  See the LICENSE file for details.

from __future__ import absolute_import

import datetime
import math
from decimal import Decimal, getcontext

from attotime import constants, util


class attotimedelta(object):
    def __init__(
        self,
        days=0,
        seconds=0,
        microseconds=0,
        milliseconds=0,
        minutes=0,
        hours=0,
        weeks=0,
        nanoseconds=0,
    ):
        if type(days) not in (int, float, Decimal):
            raise TypeError("days must be int, float, or Decimal")

        if type(seconds) not in (int, float, Decimal):
            raise TypeError("seconds must be int, float, or Decimal")

        if type(microseconds) not in (int, float, Decimal):
            raise TypeError("microseconds must be int, float, or Decimal")

        if type(milliseconds) not in (int, float, Decimal):
            raise TypeError("milliseconds must be int, float, or Decimal")

        if type(minutes) not in (int, float, Decimal):
            raise TypeError("minutes must be int, float, or Decimal")

        if type(hours) not in (int, float, Decimal):
            raise TypeError("hours must be int, float, or Decimal")

        if type(weeks) not in (int, float, Decimal):
            raise TypeError("weeks must be int, float, or Decimal")

        if type(nanoseconds) not in (int, float, Decimal):
            raise TypeError("nanoseconds must be int, float, or Decimal")

        # Convert everything to nanoseconds
        total_nanoseconds = self._as_nanoseconds(
            days=days,
            seconds=seconds,
            microseconds=microseconds,
            milliseconds=milliseconds,
            minutes=minutes,
            hours=hours,
            weeks=weeks,
            nanoseconds=nanoseconds,
        )

        results_tuple = self._reduce_nanoseconds(total_nanoseconds)

        days = results_tuple[0]
        seconds = results_tuple[1]
        microseconds = results_tuple[2]
        milliseconds = results_tuple[3]
        minutes = results_tuple[4]
        hours = results_tuple[5]
        weeks = results_tuple[6]
        nanosesconds = results_tuple[7]

        # Create the native timedelta
        self._native_timedelta = datetime.timedelta(
            days=int(days),
            seconds=int(seconds),
            microseconds=int(microseconds),
            milliseconds=int(milliseconds),
            minutes=int(minutes),
            hours=int(hours),
            weeks=int(weeks),
        )

        # Store the nanoseconds
        self._nanoseconds = Decimal(nanosesconds)

    @property
    def days(self):
        return self._native_timedelta.days

    @property
    def seconds(self):
        return self._native_timedelta.seconds

    @property
    def microseconds(self):
        return self._native_timedelta.microseconds

    @property
    def nanoseconds(self):
        return self._nanoseconds

    def __add__(self, y):
        # Return the result of x + y, where self is x
        x_nanoseconds = self.total_nanoseconds()
        y_nanoseconds = y.total_nanoseconds()

        result_tuple = self._reduce_nanoseconds(x_nanoseconds + y_nanoseconds)

        return attotimedelta(
            days=result_tuple[0],
            seconds=result_tuple[1],
            microseconds=result_tuple[2],
            milliseconds=result_tuple[3],
            minutes=result_tuple[4],
            hours=result_tuple[5],
            weeks=result_tuple[6],
            nanoseconds=result_tuple[7],
        )

    def __sub__(self, y):
        # Return the result of x - y, where self is x
        return self + -y

    def __mul__(self, y):
        # Return the result of x * y, where self is x and y is an integer, float, or Decimal
        x_nanoseconds = self.total_nanoseconds()

        result_tuple = self._reduce_nanoseconds(x_nanoseconds * Decimal(y))

        return attotimedelta(
            days=result_tuple[0],
            seconds=result_tuple[1],
            microseconds=result_tuple[2],
            milliseconds=result_tuple[3],
            minutes=result_tuple[4],
            hours=result_tuple[5],
            weeks=result_tuple[6],
            nanoseconds=result_tuple[7],
        )

    def __rmul__(self, y):
        # Return the result of y * x, where self ix x and y is an integer, float, or Decimal
        return self.__mul__(y)

    def __div__(self, y):
        # Return the result of x / y, where self is x and y is an integer, float, or Decimal
        x_nanoseconds = self.total_nanoseconds()

        result_tuple = self._reduce_nanoseconds(x_nanoseconds / Decimal(y))

        return attotimedelta(
            days=result_tuple[0],
            seconds=result_tuple[1],
            microseconds=result_tuple[2],
            milliseconds=result_tuple[3],
            minutes=result_tuple[4],
            hours=result_tuple[5],
            weeks=result_tuple[6],
            nanoseconds=result_tuple[7],
        )

    def __truediv__(self, y):
        # Python3 division
        return self.__div__(y)

    def __floordiv__(self, y):
        # Return the result of x // y, where self is x and y is an integer, float, or Decimal
        x_nanoseconds = self.total_nanoseconds()

        result_tuple = self._reduce_nanoseconds(x_nanoseconds // Decimal(y))

        return attotimedelta(
            days=result_tuple[0],
            seconds=result_tuple[1],
            microseconds=result_tuple[2],
            milliseconds=result_tuple[3],
            minutes=result_tuple[4],
            hours=result_tuple[5],
            weeks=result_tuple[6],
            nanoseconds=result_tuple[7],
        )

    def __eq__(self, y):
        # Return the result of x == y, where self is x
        if isinstance(y, self.__class__):
            return self.total_nanoseconds() == y.total_nanoseconds()

        # Other class doesn't match
        return False

    def __ne__(self, y):
        # Return the result of x != y, where self is x
        return not self.__eq__(y)

    def __gt__(self, y):
        # Return the result of x > y, where self is x
        if isinstance(y, self.__class__):
            return self.total_nanoseconds() > y.total_nanoseconds()
        else:
            raise TypeError(
                "can't compare {0} to {1}".format(type(self).__name__, type(y).__name__)
            )

    def __ge__(self, y):
        # Return the result of x >= y, where self is x
        if isinstance(y, self.__class__):
            return self.total_nanoseconds() >= y.total_nanoseconds()
        else:
            raise TypeError(
                "can't compare {0} to {1}".format(type(self).__name__, type(y).__name__)
            )

    def __lt__(self, y):
        # Return the result of x < y, where self is x
        if isinstance(y, self.__class__):
            return self.total_nanoseconds() < y.total_nanoseconds()
        else:
            raise TypeError(
                "can't compare {0} to {1}".format(type(self).__name__, type(y).__name__)
            )

    def __le__(self, y):
        # Return the result of x <= y, where self is x
        if isinstance(y, self.__class__):
            return self.total_nanoseconds() <= y.total_nanoseconds()
        else:
            raise TypeError(
                "can't compare {0} to {1}".format(type(self).__name__, type(y).__name__)
            )

    def __pos__(self):
        # Return the result of +x, where self is x
        #
        # https://stackoverflow.com/questions/16819023/whats-the-purpose-of-the-pos-unary-operator-in-python
        return attotimedelta(nanoseconds=+self.total_nanoseconds())

    def __neg__(self):
        # Return the result of -x, where self is x
        return attotimedelta(nanoseconds=-self.total_nanoseconds())

    def __abs__(self):
        # Return the result of abs(x), where self is x
        return attotimedelta(nanoseconds=abs(self.total_nanoseconds()))

    def __str__(self):
        # Returns a string in the form [D day[s], ][H]H:MM:SS[.UUUUUU], where D is negative for negative t.
        nanoseconds = self.total_nanoseconds()

        reduced_tuple = self._reduce_nanoseconds(nanoseconds)

        if nanoseconds < 0:
            if (
                reduced_tuple[1] != 0
                or reduced_tuple[2] != 0
                or reduced_tuple[3] != 0
                or reduced_tuple[4] != 0
                or reduced_tuple[5] != 0
                or reduced_tuple[7] != 0
            ):
                # We go to a negative day, then count forward
                days = -(
                    abs(reduced_tuple[0])
                    + abs(reduced_tuple[6]) * constants.DAYS_PER_WEEK
                    + 1
                )
            else:
                days = -(
                    abs(reduced_tuple[0])
                    + abs(reduced_tuple[6]) * constants.DAYS_PER_WEEK
                )

            seconds = abs(reduced_tuple[1])
            microseconds = abs(reduced_tuple[2])
            milliseconds = abs(reduced_tuple[3])
            minutes = abs(reduced_tuple[4])
            hours = abs(reduced_tuple[5])
            nanoseconds = abs(reduced_tuple[7])

            # Now count forward
            remaining_nanoseconds = constants.NANOSECONDS_PER_DAY - (
                seconds * constants.NANOSECONDS_PER_SECOND
                + microseconds * constants.NANOSECONDS_PER_MICROSECOND
                + milliseconds * constants.NANOSECONDS_PER_MILLISECOND
                + minutes * constants.NANOSECONDS_PER_MINUTE
                + hours * constants.NANOSECONDS_PER_HOUR
                + nanoseconds
            )

            reduced_tuple = self._reduce_nanoseconds(remaining_nanoseconds)

            seconds = reduced_tuple[1]
            microseconds = reduced_tuple[2]
            milliseconds = reduced_tuple[3]
            minutes = reduced_tuple[4]
            hours = reduced_tuple[5]
            nanoseconds = reduced_tuple[7]
        else:
            days = reduced_tuple[0] + reduced_tuple[6] * constants.DAYS_PER_WEEK
            seconds = reduced_tuple[1]
            microseconds = reduced_tuple[2]
            milliseconds = reduced_tuple[3]
            minutes = reduced_tuple[4]
            hours = reduced_tuple[5]
            nanoseconds = reduced_tuple[7]

        # Combine the seconds
        combined_seconds = (
            seconds
            + (microseconds / constants.MICROSECONDS_PER_SECOND)
            + (milliseconds / constants.MILLISECONDS_PER_SECOND)
            + (nanoseconds / constants.NANOSECONDS_PER_SECOND)
        )

        # Build the strings
        if days == 0:
            days_str = ""
        elif abs(days) > 1:
            days_str = str(days) + " days"
        else:
            days_str = str(days) + " day"

        if combined_seconds == 0:
            time_str = str(hours) + ":" + "{0:02d}".format(minutes) + ":00"
        elif combined_seconds < 10:
            time_str = (
                str(hours)
                + ":"
                + "{0:02d}".format(minutes)
                + ":0"
                + util.decimal_stringify(combined_seconds)
            )
        else:
            time_str = (
                str(hours)
                + ":"
                + "{0:02d}".format(minutes)
                + ":"
                + util.decimal_stringify(combined_seconds)
            )

        if days_str == "":
            return time_str
        else:
            return days_str + ", " + time_str

    def __repr__(self):
        # Returns a string in the form attotime.timedelta(D[, S[, U[, N]]]), where D is negative for negative t
        nanoseconds = self.total_nanoseconds()

        reduced_tuple = self._reduce_nanoseconds(nanoseconds)

        if nanoseconds < 0:
            if (
                reduced_tuple[1] != 0
                or reduced_tuple[2] != 0
                or reduced_tuple[3] != 0
                or reduced_tuple[4] != 0
                or reduced_tuple[5] != 0
                or reduced_tuple[7] != 0
            ):
                # We go to a negative day, then count forward
                days = -(
                    abs(reduced_tuple[0])
                    + abs(reduced_tuple[6]) * constants.DAYS_PER_WEEK
                    + 1
                )
            else:
                days = -(
                    abs(reduced_tuple[0])
                    + abs(reduced_tuple[6]) * constants.DAYS_PER_WEEK
                )

            seconds = abs(reduced_tuple[1])
            microseconds = abs(reduced_tuple[2])
            milliseconds = abs(reduced_tuple[3])
            minutes = abs(reduced_tuple[4])
            hours = abs(reduced_tuple[5])
            nanoseconds = abs(reduced_tuple[7])

            # Now count forward
            remaining_nanoseconds = constants.NANOSECONDS_PER_DAY - (
                seconds * constants.NANOSECONDS_PER_SECOND
                + microseconds * constants.NANOSECONDS_PER_MICROSECOND
                + milliseconds * constants.NANOSECONDS_PER_MILLISECOND
                + minutes * constants.NANOSECONDS_PER_MINUTE
                + hours * constants.NANOSECONDS_PER_HOUR
                + nanoseconds
            )

            reduced_tuple = self._reduce_nanoseconds(remaining_nanoseconds)

            seconds = reduced_tuple[1]
            microseconds = reduced_tuple[2]
            milliseconds = reduced_tuple[3]
            minutes = reduced_tuple[4]
            hours = reduced_tuple[5]
            nanoseconds = reduced_tuple[7]
        else:
            days = reduced_tuple[0] + reduced_tuple[6] * constants.DAYS_PER_WEEK
            seconds = reduced_tuple[1]
            microseconds = reduced_tuple[2]
            milliseconds = reduced_tuple[3]
            minutes = reduced_tuple[4]
            hours = reduced_tuple[5]
            nanoseconds = reduced_tuple[7]

        combined_seconds = (
            hours * constants.SECONDS_PER_HOUR
            + minutes * constants.SECONDS_PER_MINUTE
            + seconds
        )
        combined_microseconds = (
            milliseconds * constants.MICROSECONDS_PER_MILLISECOND + microseconds
        )

        repr_string = self.__class__.__module__ + "(" + str(days)

        if combined_seconds != 0 or combined_microseconds != 0 or nanoseconds != 0:
            repr_string += ", " + str(combined_seconds)

        if combined_microseconds != 0 or nanoseconds != 0:
            repr_string += ", " + str(combined_microseconds)

        if nanoseconds != 0:
            repr_string += ", " + util.decimal_stringify(nanoseconds)

        repr_string += ")"

        return repr_string

    def total_seconds(self):
        return self.total_nanoseconds() / constants.NANOSECONDS_PER_SECOND

    def total_nanoseconds(self):
        # Converts the entire timedelta to nanoseconds
        return self._as_nanoseconds(
            days=self._native_timedelta.days,
            seconds=self._native_timedelta.seconds,
            microseconds=self._native_timedelta.microseconds,
            nanoseconds=self._nanoseconds,
        )

    @staticmethod
    def _as_nanoseconds(
        days=0,
        seconds=0,
        microseconds=0,
        milliseconds=0,
        minutes=0,
        hours=0,
        weeks=0,
        nanoseconds=0,
    ):
        return (
            Decimal(days) * constants.NANOSECONDS_PER_DAY
            + Decimal(seconds) * constants.NANOSECONDS_PER_SECOND
            + Decimal(microseconds) * constants.NANOSECONDS_PER_MICROSECOND
            + Decimal(milliseconds) * constants.NANOSECONDS_PER_MILLISECOND
            + Decimal(minutes) * constants.NANOSECONDS_PER_MINUTE
            + Decimal(hours) * constants.NANOSECONDS_PER_HOUR
            + Decimal(weeks) * constants.NANOSECONDS_PER_WEEK
            + Decimal(nanoseconds)
        )

    @staticmethod
    def _reduce_nanoseconds(nanoseconds):
        return _reduce_to_tuple(
            nanoseconds,
            constants.NANOSECONDS_PER_WEEK,
            constants.NANOSECONDS_PER_DAY,
            constants.NANOSECONDS_PER_HOUR,
            constants.NANOSECONDS_PER_MINUTE,
            constants.NANOSECONDS_PER_SECOND,
            constants.NANOSECONDS_PER_MILLISECOND,
            constants.NANOSECONDS_PER_MICROSECOND,
            1,
        )


def _reduce_to_tuple(
    value,
    week_reduction,
    day_reduction,
    hour_reduction,
    minute_reduction,
    second_reduction,
    millisecond_reduction,
    microsecond_reduction,
    nanosecond_reduction,
):
    remainder = abs(Decimal(value))

    reduced_weeks, remainder = getcontext().divmod(remainder, week_reduction)
    reduced_days, remainder = getcontext().divmod(remainder, day_reduction)
    reduced_hours, remainder = getcontext().divmod(remainder, hour_reduction)
    reduced_minutes, remainder = getcontext().divmod(remainder, minute_reduction)
    reduced_seconds, remainder = getcontext().divmod(remainder, second_reduction)
    reduced_milliseconds, remainder = getcontext().divmod(
        remainder, millisecond_reduction
    )
    reduced_microseconds, remainder = getcontext().divmod(
        remainder, microsecond_reduction
    )
    nanoseconds = remainder * nanosecond_reduction

    # Return the mess, ints for everything but nanoseconds
    return (
        int(math.copysign(reduced_days, value)),
        int(math.copysign(reduced_seconds, value)),
        int(math.copysign(reduced_microseconds, value)),
        int(math.copysign(reduced_milliseconds, value)),
        int(math.copysign(reduced_minutes, value)),
        int(math.copysign(reduced_hours, value)),
        int(math.copysign(reduced_weeks, value)),
        nanoseconds.copy_sign(Decimal(value)),
    )
