# AUTOGENERATED! DO NOT EDIT! File to edit: nbs/00_core.ipynb (unless otherwise specified).


from __future__ import annotations


__all__ = ['logger', 'GPSTime']

# Cell
"""Copyright 2020 The Aerospace Corporation"""

# Cell
#nbdev_comment from __future__ import annotations
import datetime

import numpy as np

from typing import Union
from logging import getLogger

from .datetime import tow2datetime, datetime2tow

logger = getLogger(__name__)

# Cell
class GPSTime:
    """Time representation for GPS.

    Attributes
    ----------
    week_number : int
        The number of weeks since the start of the GPS epoch, 6 Jan 1980.
    time_of_week : float
        The number of seconds into the week. The zero time is at midnight on
        Sunday morning, i.e. betwen Saturday and Sunday. Should be between 0
        and 604800 because otherwise, the week number would be incorrect.

    Raises
    ------
    TypeError
        For various operators if not the selected types are not implemented.

    Todo
    ----
    .. todo:: Add datetimes as valid types for the add/subtract

    """

    _SEC_IN_WEEK: int = 604800

    def __init__(self, week_number: int, time_of_week: Union[int, float]) -> None:
        """Object constructor.

        This sets the week number and the time of week and ensures that the
        time of week is a float. It also calls `correct_weeks()`, which checks
        to see if the time of week is negative or past the end of the week.

        Parameters
        ----------
        week_number : int
            The number of week
        time_of_week : Union[int, float]
            The time of week in seconds

        Returns
        -------
        None

        """
        self.week_number = int(week_number)
        self.time_of_week = float(time_of_week)

        self.correct_weeks()
        if self.week_number < 0:
            logger.warning("Week number is less than 0")

    def to_datetime(self) -> datetime.datetime:
        """Convert the `GPSTime` to a datetime.

        This method calls `tow2datetime()` to convert the `GPSTime` to a
        datetime object.

        Returns
        -------
        datetime.datetime
            The equivalent datetime representation of the `GPSTime`

        Notes
        -----
        .. note::
            Datetimes are limited to microsecond resolution, so this
            conversion may lose some fidelity.

        """
        return tow2datetime(self.week_number, self.time_of_week)

    @classmethod
    def from_datetime(cls, time: datetime.datetime) -> GPSTime:
        """Create a `GPSTime` for a datetime.

        Parameters
        ----------
        time : datetime.datetime
            The datetime that will be converted to a `GPSTime`

        Returns
        -------
        GPSTime
            The `GPSTime` corresponding to the datetime. This is a lossless
            conversion.

        Raises
        ------
        TypeError
            If the input value is not a datetime

        Notes
        -----
        This is a classmethod and thus can be called without instantiating the
        object first.

        """
        if not isinstance(time, datetime.datetime):
            raise TypeError("time must be a datetime")

        week_num, tow = datetime2tow(time)

        return cls(week_num, tow)

    def to_zcount(self) -> float:
        """Get the current Z-Count.

        Returns
        -------
        float
            The time of week divided by 1.5

        """
        return self.time_of_week / 1.5

    def correct_weeks(self) -> None:
        """Correct the week number based on the time of week.

        If the time of week is less than 0 or greater than 604800 seconds,
        then the week number and time of week will be corrected to ensure that
        the time of week is within the week indicated by the week number.

        Returns
        -------
        None

        """
        if (self.time_of_week >= self._SEC_IN_WEEK) or (self.time_of_week < 0):
            weeks_to_add = int(self.time_of_week // self._SEC_IN_WEEK)
            new_time_of_week = float(self.time_of_week % self._SEC_IN_WEEK)

            self.week_number += weeks_to_add
            self.time_of_week = new_time_of_week
        else:
            pass

    def __add__(
        self,
        other: Union[
            int, float, GPSTime, datetime.datetime, datetime.timedelta, np.ndarray
        ],
    ) -> Union[GPSTime, np.ndarray]:
        """Addition, apply an offset to a `GPSTime`.

        This is the addition of a `GPSTime` and another object. In this
        context, addition means moving the clock of the first argument
        forward by some amount.

        Suppose `a` is a `GPSTime` and the value give for other represents a
        positive time. The value returned will be a `GPSTime` object that is
        the amount of time represented by other after `a`.

        Parameters
        ----------
        other : Union[int, float, GPSTime, datetime.datetime,
                      datetime.timedelta, np.ndarray]
            The other value to add to the `GPSTime`. `int` and `float` values
            are the number of seconds to add to the `GPSTime`. `GPSTime` and
            `datetime.timedelta` have explicit unit definitions that are used.
             If the value is a datetime.datetime, it is converted to a GPSTime
             before adding.

        Returns
        -------
        Union[GPSTime, np.ndarray]
            The sum of the `GPSTime` and `other`. If other is an np.array,
            returns the sum for each element

        Raises
        ------
        TypeError
            If other is not a supported type

        Notes
        -----
        .. note::
            Apart from adding of `datetime.timedelta` objects, this
            functionality does not exist with datetimes.

        .. note::
            This function can be used to "add" a negative amount of time,
            which can yield different results than subtraction.

        """
        if isinstance(other, bool):
            raise TypeError(
                "unsupported operand type(s) for -: '{}' and '{}'".format(
                    type(self), type(other)
                )
            )
        if isinstance(other, int) or isinstance(other, float):
            gps_time_to_add = GPSTime(0, float(other))
        elif isinstance(other, datetime.timedelta):
            gps_time_to_add = GPSTime(0, other.total_seconds())
        elif isinstance(other, datetime.datetime):
            gps_time_to_add = GPSTime.from_datetime(other)
        elif isinstance(other, GPSTime):
            gps_time_to_add = other
        elif isinstance(other, np.ndarray):
            input = np.array([self])
            return input + other
        else:
            raise TypeError(
                "unsupported operand type(s) for +: '{}' and '{}'".format(
                    type(self), type(other)
                )
            )

        week_num = self.week_number + gps_time_to_add.week_number
        time_of_week = self.time_of_week + gps_time_to_add.time_of_week
        return GPSTime(week_num, time_of_week)

    def __sub__(
        self,
        other: Union[
            int, float, GPSTime, datetime.datetime, datetime.timedelta, np.ndarray
        ],
    ) -> Union[GPSTime, float, np.ndarray]:
        """Subtraction.

        This method is used to represent subtraction. Depending on the type of
        the arguments, it can be used to find the time offset by an amount or
        the number of seconds between two times.

        Parameters
        ----------
        other : Union[int, float, GPSTime, datetime.datetime,
                      datetime.timedelta, np.ndarray]
            The other value to subtract from the `GPSTime`. `int` and `float`
            values are the number of seconds to subtract from the `GPSTime`.
            `GPSTime` and `datetime.timedelta` have explicit unit definitions
            that are used. If the value is a datetime.datetime, it is
            converted to a GPSTime before subtracting.

        Returns
        -------
        Union[GPSTime, float, np.ndarray]
            A float will be return if both values are `GPSTime` objects that
            represents the number of seconds between the objects. A GPSTime
            will be returned otherwise and it represents offsetting the time
            backward by the amount given. If the input is an np.ndarray, then
            returns the operation for each element

        Raises
        ------
        TypeError
            If other is not a supported type

        Notes
        -----
        Subtracting a non-`GPSTime` object is equivalent to adding the opposite
        of its value

        """
        if isinstance(other, bool):
            raise TypeError(
                "unsupported operand type(s) for -: '{}' and '{}'".format(
                    type(self), type(other)
                )
            )
        if isinstance(other, int) or isinstance(other, float):
            gps_time_to_sub = float(other)
            return GPSTime(self.week_number, self.time_of_week - gps_time_to_sub)

        elif isinstance(other, datetime.timedelta):
            gps_time_to_sub = other.total_seconds()
            return GPSTime(self.week_number, self.time_of_week - gps_time_to_sub)

        elif isinstance(other, datetime.datetime):
            other_gpstime = GPSTime.from_datetime(other)

            weeks_diff = self.week_number - other_gpstime.week_number
            time_diff = self.time_of_week - other_gpstime.time_of_week

            return float(weeks_diff * self._SEC_IN_WEEK + time_diff)

        elif isinstance(other, GPSTime):
            weeks_diff = self.week_number - other.week_number
            time_diff = self.time_of_week - other.time_of_week

            return float(weeks_diff * self._SEC_IN_WEEK + time_diff)

        elif isinstance(other, np.ndarray):
            if other.dtype == np.object:
                _type = np.reshape(other, sum([i for i in other.shape]))[0].__class__

                if _type in (self.__class__, datetime.datetime):
                    input = np.array([self])
                    return np.array(input - other, dtype=float)
                elif _type is datetime.timedelta:
                    input = np.array([self])
                    return np.array(input - other, dtype=object)
            elif other.dtype in (
                int,
                float,
            ):
                input = np.array([self])
                return np.array(input - other, dtype=object)

        else:
            raise TypeError(
                "unsupported operand type(s) for -: '{}' and '{}'".format(
                    type(self), type(other)
                )
            )

    def __lt__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Less Than.

        .. note:: In this context "less than" is equivalent to "before"

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is before its comparison

        Raises
        ------
        TypeError
            If an invalid type

        """
        if isinstance(other, datetime.datetime):
            other_time = GPSTime.from_datetime(other)
        elif isinstance(other, GPSTime):
            other_time = other
        else:
            raise TypeError(
                "'<' not supported between instances of '{}' and '{}'".format(
                    type(self), type(other)
                )
            )
        return (self - other_time) < 0

    def __gt__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Greater Than.

        .. note:: In this context "greater than" is equivalent to "after"

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is after its comparison

        Raises
        ------
        TypeError
            If an invalid type

        """
        if isinstance(other, datetime.datetime):
            other_time = GPSTime.from_datetime(other)
        elif isinstance(other, GPSTime):
            other_time = other
        else:
            raise TypeError(
                "'>' not supported between instances of '{}' and '{}'".format(
                    type(self), type(other)
                )
            )
        return (self - other_time) > 0

    def __eq__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Equality.

        .. note:: In this context "equality" is equivalent to "coincident"

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is the same time as its comparison

        Raises
        ------
        TypeError
            If an invalid type

        """
        if isinstance(other, datetime.datetime):
            other_time = GPSTime.from_datetime(other)
        elif isinstance(other, GPSTime):
            other_time = other
        else:
            raise TypeError(
                "'>' not supported between instances of '{}' and '{}'".format(
                    type(self), type(other)
                )
            )
        return (self - other_time) == 0

    def __le__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Less Than or Equals.

        Calls the `__lt__()` and `__eq__()` methods

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is before or at the same time as its
            comparison object.

        Raises
        ------
        TypeError
            If an invalid type

        """
        return self.__lt__(other) or self.__eq__(other)

    def __ge__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Greater Than or Equals.

        Calls the `__gt__()` and `__eq__()` methods

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is after or at the same time as its
            comparison object.

        Raises
        ------
        TypeError
            If an invalid type

        """
        return self.__gt__(other) or self.__eq__(other)

    def __ne__(self, other: Union[GPSTime, datetime.datetime]) -> bool:
        """Comparison: Not Equals.

        Inverts the result of the `__eq__()` method

        Parameters
        ----------
        other : Union[GPSTime, datetime.datetime]
            The object to compare. Datatimes will be converted to `GPSTime`

        Returns
        -------
        bool
            True if the current object is not the same time as its comparison

        Raises
        ------
        TypeError
            If an invalid type

        """
        return not (self.__eq__(other))

    def __hash__(self):
        """Make GPSTime hashable."""
        return hash(str(self.week_number) + str(self.time_of_week))

    def __repr__(self) -> str:
        """Representation of the object.

        Returns
        -------
        str
            The representation of the object

        """
        return "GPSTime(week_number={}, time_of_week={})".format(
            self.week_number, self.time_of_week
        )