"""
This module contains constants and helper functions for converting between different time bases. All time based
functions take inputs and output in UTC.
"""

from datetime import datetime, tzinfo, timedelta
from typing import List, Tuple

# noinspection Mypy
import numpy as np

# Microsecond constants
MICROSECONDS_IN_MILLISECOND: float = 1000.0
MICROSECONDS_IN_SECOND: float = 1000000.0
MICROSECONDS_IN_MINUTE: float = 60000000.0
MICROSECONDS_IN_HOUR: float = 3600000000.0
MICROSECONDS_IN_DAY: float = 86400000000.0
MICROSECONDS_IN_WEEK: float = 604800000000.0

# Millisecond constants
MILLISECONDS_IN_SECOND: float = 1000.0
MILLISECONDS_IN_MINUTE: float = 60000.0
MILLISECONDS_IN_HOUR: float = 3600000.0
MILLISECONDS_IN_DAY: float = 86400000.0
MILLISECONDS_IN_WEEK: float = 604800000.0

# Second constants
SECONDS_IN_MINUTE: float = 60.0
SECONDS_IN_HOUR: float = 3600.0
SECONDS_IN_DAY: float = 86400.0
SECONDS_IN_WEEK: float = 604800.0

# Minute constants
MINUTES_IN_HOUR: float = 60.0
MINUTES_IN_DAY: float = 1440.0
MINUTES_IN_WEEK: float = 10080.0

# Hour constants
HOURS_IN_DAY: float = 24.0
HOURS_IN_WEEK: float = 168.0

# Day constants
DAYS_IN_WEEK: float = 7.0

# A datetime object representing the epoch.
EPOCH: datetime = datetime.utcfromtimestamp(0)


class UTC(tzinfo):
    """
    This class represents UTC time.
    """

    def utcoffset(self, dt):
        return timedelta(0)

    def tzname(self, dt):
        return "UTC"

    def dst(self, dt):
        return timedelta(0)


# An instance of the above class that can be used anywhere the class is needed
UTC_INSTANCE = UTC()


# Microsecond conversions
def microseconds_to_milliseconds(microseconds: float) -> float:
    """
    Converts microseconds to milliseconds.

    :param microseconds: Number of microseconds.
    :return: Number of milliseconds.
    """
    return microseconds / MICROSECONDS_IN_MILLISECOND


def microseconds_to_seconds(microseconds: float) -> float:
    """
    Converts microseconds to seconds.

    :param microseconds: Number of microseconds.
    :return: Number of seconds.
    """
    return microseconds / MICROSECONDS_IN_SECOND


def microseconds_to_minutes(microseconds: float) -> float:
    """
    Converts microseconds to minutes.

    :param microseconds: Number of microseconds.
    :return: Number of minutes.
    """
    return microseconds / MICROSECONDS_IN_MINUTE


def microseconds_to_hours(microseconds: float) -> float:
    """
    Converts microseconds to hours.

    :param microseconds: Number of microseconds.
    :return: Number of hours.
    """
    return microseconds / MICROSECONDS_IN_HOUR


def microseconds_to_days(microseconds: float) -> float:
    """
    Converts microseconds to days.

    :param microseconds: Number of microseconds.
    :return: Number of days.
    """
    return microseconds / MICROSECONDS_IN_DAY


def microseconds_to_weeks(microseconds: float) -> float:
    """
    Converts microseconds to weeks.

    :param microseconds: Number of microseconds.
    :return: Number of weeks.
    """
    return microseconds / MICROSECONDS_IN_WEEK


# Millisecond conversions
def milliseconds_to_microseconds(milliseconds: float) -> float:
    """
    Converts milliseconds to microseconds.

    :param milliseconds: Number of milliseconds.
    :return: Number of microseconds.
    """
    return milliseconds * MICROSECONDS_IN_MILLISECOND


def milliseconds_to_seconds(milliseconds: float) -> float:
    """
    Converts milliseconds to seconds.

    :param milliseconds: Number of milliseconds.
    :return: Number of seconds.
    """
    return milliseconds / MILLISECONDS_IN_SECOND


def milliseconds_to_minutes(milliseconds: float) -> float:
    """
    Converts milliseconds to minutes.

    :param milliseconds: Number of milliseconds.
    :return: Number of minutes.
    """
    return milliseconds / MILLISECONDS_IN_MINUTE


def milliseconds_to_hours(milliseconds: float) -> float:
    """
    Converts milliseconds to hours.

    :param milliseconds: Number of milliseconds.
    :return: Number of hours.
    """
    return milliseconds / MILLISECONDS_IN_HOUR


def milliseconds_to_days(milliseconds: float) -> float:
    """
    Converts milliseconds to days.

    :param milliseconds: Number of milliseconds.
    :return: Number of days.
    """
    return milliseconds / MILLISECONDS_IN_DAY


def milliseconds_to_weeks(milliseconds: float) -> float:
    """
    Converts milliseconds to weeks.

    :param milliseconds: Number of milliseconds.
    :return: Number of weeks.
    """
    return milliseconds / MILLISECONDS_IN_WEEK


# Second conversions
def seconds_to_microseconds(seconds: float) -> float:
    """
    Converts seconds to microseconds.

    :param seconds: Number of seconds.
    :return: Number of microseconds.
    """
    return seconds * MICROSECONDS_IN_SECOND


def seconds_to_milliseconds(seconds: float) -> float:
    """
    Converts seconds to milliseconds.

    :param seconds: Number of seconds.
    :return: Number of milliseconds.
    """
    return seconds * MILLISECONDS_IN_SECOND


def seconds_to_minutes(seconds: float) -> float:
    """
    Converts seconds to minutes.

    :param seconds: Number of seconds.
    :return: Number of minutes.
    """
    return seconds / SECONDS_IN_MINUTE


def seconds_to_hours(seconds: float) -> float:
    """
    Converts seconds to hours.

    :param seconds: Number of seconds.
    :return: Number of hours.
    """
    return seconds / SECONDS_IN_HOUR


def seconds_to_days(seconds: float) -> float:
    """
    Converts seconds to days.

    :param seconds: Number of seconds.
    :return: Number of days.
    """
    return seconds / SECONDS_IN_DAY


def seconds_to_weeks(seconds: float) -> float:
    """
    Converts seconds to weeks.

    :param seconds: Number of seconds.
    :return: Number of weeks.
    """
    return seconds / SECONDS_IN_WEEK


# Minute conversions
def minutes_to_microseconds(minutes: float) -> float:
    """
    Converts minutes to microseconds.

    :param minutes: Number of minutes.
    :return: Number of microseconds.
    """
    return minutes * MICROSECONDS_IN_MINUTE


def minutes_to_milliseconds(minutes: float) -> float:
    """
    Converts minutes to milliseconds.

    :param minutes: Number of minutes.
    :return: Number of milliseconds.
    """
    return minutes * MILLISECONDS_IN_MINUTE


def minutes_to_seconds(minutes: float) -> float:
    """
    Converts minutes to seconds.

    :param minutes: Number of minutes.
    :return: Number of seconds.
    """
    return minutes * SECONDS_IN_MINUTE


def minutes_to_hours(minutes: float) -> float:
    """
    Converts minutes to hours.

    :param minutes: Number of minutes.
    :return: Number of hours.
    """
    return minutes / MINUTES_IN_HOUR


def minutes_to_days(minutes: float) -> float:
    """
    Converts minutes to days.

    :param minutes: Number of minutes.
    :return: Number of days.
    """
    return minutes / MINUTES_IN_DAY


def minutes_to_weeks(minutes: float) -> float:
    """
    Converts minutes to weeks.

    :param minutes: Number of minutes.
    :return: Number of weeks.
    """
    return minutes / MINUTES_IN_WEEK


# Hour conversions
def hours_to_microseconds(hours: float) -> float:
    """
    Converts hours to microseconds.

    :param hours: Number of hours.
    :return: Number of microseconds.
    """
    return hours * MICROSECONDS_IN_HOUR


def hours_to_milliseconds(hours: float) -> float:
    """
    Converts hours to milliseconds.

    :param hours: Number of hours.
    :return: Number of milliseconds.
    """
    return hours * MILLISECONDS_IN_HOUR


def hours_to_seconds(hours: float) -> float:
    """
    Converts hours to seconds.

    :param hours: Number of hours.
    :return: Number of seconds.
    """
    return hours * SECONDS_IN_HOUR


def hours_to_minutes(hours: float) -> float:
    """
    Converts hours to minutes.

    :param hours: Number of hours.
    :return: Number of minutes.
    """
    return hours * MINUTES_IN_HOUR


def hours_to_days(hours: float) -> float:
    """
    Converts hours to days.

    :param hours: Number of hours.
    :return: Number of days.
    """
    return hours / HOURS_IN_DAY


def hours_to_weeks(hours: float) -> float:
    """
    Converts hours to weeks.

    :param hours: Number of hours.
    :return: Number of weeks.
    """
    return hours / HOURS_IN_WEEK


# Week conversions
def weeks_to_microseconds(weeks: float) -> float:
    """
    Converts weeks to microseconds.

    :param weeks: Number of weeks.
    :return: Number of microseconds.
    """
    return weeks * MICROSECONDS_IN_WEEK


def weeks_to_milliseconds(weeks: float) -> float:
    """
    Converts weeks to milliseconds.

    :param weeks: Number of weeks.
    :return: Number of milliseconds.
    """
    return weeks * MILLISECONDS_IN_WEEK


def weeks_to_seconds(weeks: float) -> float:
    """
    Converts weeks to seconds.

    :param weeks: Number of weeks.
    :return: Number of seconds.
    """
    return weeks * SECONDS_IN_WEEK


def weeks_to_minutes(weeks: float) -> float:
    """
    Converts weeks to minutes.

    :param weeks: Number of weeks.
    :return: Number of minutes.
    """
    return weeks * MINUTES_IN_WEEK


def weeks_to_hours(weeks: float) -> float:
    """
    Converts weeks to hours.

    :param weeks: Number of weeks.
    :return: Number of hours.
    """
    return weeks * HOURS_IN_WEEK


def weeks_to_days(weeks: float) -> float:
    """
    Converts weeks to days.

    :param weeks: Number of weeks.
    :return: Number of days.
    """
    return weeks * DAYS_IN_WEEK


def datetime_from(
    year: int, month: int, day: int, hour: int = 0, minute: int = 0, second: int = 0
) -> datetime:
    """
    Returns a datetime object in UTC based off the given parameters.

    :param year: The year.
    :param month: The month.
    :param day: The day.
    :param hour: The hour (optional, default=0).
    :param minute: The minute (optional, default=0).
    :param second: The second (optional, default=0).
    :return: An instance of a datetime.
    """
    return datetime(year, month, day, hour, minute, second)


def datetime_to_epoch_seconds_utc(date_time: datetime) -> float:
    """
    Given a datetime, return the number of seconds since the epoch UTC.

    :param date_time: An instance of a datetime.
    :return: A UTC second timestamp.
    """
    return (date_time - EPOCH).total_seconds()


def datetime_to_epoch_milliseconds_utc(date_time: datetime) -> float:
    """
    Given a datetime, return the number of milliseconds since the epoch UTC.

    :param date_time: An instance of a datetime.
    :return: A UTC millisecond timestamp.
    """
    epoch_seconds: float = datetime_to_epoch_seconds_utc(date_time)
    return seconds_to_milliseconds(epoch_seconds)


def datetime_to_epoch_microseconds_utc(date_time: datetime) -> float:
    """
    Given a datetime, return the number of microseconds since the epoch UTC.

    :param date_time: The datetime object to convert.
    :return: A UTC microsecond timestamp.
    """
    epoch_seconds: float = datetime_to_epoch_seconds_utc(date_time)
    return seconds_to_microseconds(epoch_seconds)


def datetime_from_epoch_seconds_utc(epoch_seconds_utc: float) -> datetime:
    """
    Given number of seconds since the epoch UTC, return a Python datetime object.

    :param epoch_seconds_utc: A UTC second timestamp.
    :return: A datetime object.
    """
    return EPOCH + timedelta(seconds=epoch_seconds_utc)


def datetime_from_epoch_milliseconds_utc(epoch_milliseconds_utc: float) -> datetime:
    """
    Given number of milliseconds since the epoch UTC, return a Python datetime object.

    :param epoch_milliseconds_utc: UTC millisecond timestamp.
    :return: Datetime object.
    """
    return EPOCH + timedelta(milliseconds=epoch_milliseconds_utc)


def datetime_from_epoch_microseconds_utc(epoch_microseconds_utc: float) -> datetime:
    """
    Given number of microseconds since the epoch UTC, return a Python datetime object.

    :param epoch_microseconds_utc: UTC microsecond timestamp.
    :return: A datetime object.
    """
    return EPOCH + timedelta(microseconds=epoch_microseconds_utc)


def datetimes_from_epoch_seconds_utc(epochs_seconds_utc: List[int]) -> List[datetime]:
    """
    Concerts a list of timestamps as seconds since the epoch UTC to a list of datetime objects.

    :param epochs_seconds_utc: List of timestamps to convert.
    :return: A list of datetimes.
    """
    return list(map(datetime_from_epoch_seconds_utc, epochs_seconds_utc))


def generate_timestamps_s_utc(
    start_timestamp_s_utc: float, sample_rate_hz: float, num_samples: int
) -> np.ndarray:
    """
    Given a starting timestamp, a sample rate, and a number of samples, compute timestamps for all samples.

    :param start_timestamp_s_utc: The start timestamp.
    :param sample_rate_hz: The sample rate.
    :param num_samples: The number of samples.
    :return: Timestamps for each sample.
    """
    sample_interval_s: float = 1.0 / sample_rate_hz
    return (np.arange(num_samples) * sample_interval_s) + start_timestamp_s_utc


def now() -> datetime:
    """
    Returns the current datetime in UTC.

    :return: The current datetime in UTC.
    """
    return datetime.utcnow()


class DateIterator:
    """
    This class provides an iterator over dates. That is, it takes a start date and an end date and returns a tuple
    of year, month, day for each date between the start and end.
    """

    def __init__(self, start_timestamp_utc_s: int, end_timestamp_utc_s: int):

        start_dt_full: datetime = datetime.utcfromtimestamp(start_timestamp_utc_s)
        end_dt_full: datetime = datetime.utcfromtimestamp(end_timestamp_utc_s)

        self.start_dt: datetime = datetime_from(
            start_dt_full.year, start_dt_full.month, start_dt_full.day
        )
        self.end_dt: datetime = datetime_from(
            end_dt_full.year, end_dt_full.month, end_dt_full.day
        )

        self._one_day: timedelta = timedelta(days=1)

    def __iter__(self) -> "DateIterator":
        return self

    def __next__(self) -> Tuple[str, str, str]:
        """
        Returns the next date in the iterator.
        :return: The next date in the iterator.
        """
        if self.start_dt > self.end_dt:
            raise StopIteration()

        year = str(self.start_dt.year)
        month = f"{self.start_dt.month:02}"
        day = f"{self.start_dt.day:02}"

        self.start_dt += self._one_day

        return year, month, day


class DateIteratorAPIM:
    """
    This class provides an iterator over dates. That is, it takes a start date and an end date and returns a tuple
    of year, month, day and hour for each date between the start and end.
    """

    def __init__(self, start_timestamp_utc_s: int, end_timestamp_utc_s: int):

        start_dt_full: datetime = datetime.utcfromtimestamp(start_timestamp_utc_s)
        end_dt_full: datetime = datetime.utcfromtimestamp(end_timestamp_utc_s)

        self.start_dt: datetime = datetime_from(
            start_dt_full.year,
            start_dt_full.month,
            start_dt_full.day,
            start_dt_full.hour,
        )
        self.end_dt: datetime = datetime_from(
            end_dt_full.year, end_dt_full.month, end_dt_full.day, end_dt_full.hour
        )

        self._one_day: timedelta = timedelta(days=1)
        self._one_hour: timedelta = timedelta(hours=1)

    def __iter__(self) -> "DateIteratorAPIM":
        return self

    def __next__(self) -> Tuple[str, str, str, str]:
        """
        Returns the next date in the iterator.

        :return: The next date in the iterator.
        """
        if self.start_dt > self.end_dt:
            raise StopIteration()

        year = str(self.start_dt.year)
        month = f"{self.start_dt.month:02}"
        day = f"{self.start_dt.day:02}"
        hour = f"{self.start_dt.hour:02}"

        self.start_dt += self._one_hour

        return year, month, day, hour


def truncate_dt_ymd(date_time: datetime) -> datetime:
    """
    Truncates the provides datetime to only year, month, day.

    :param date_time: The datetime to truncate.
    :return: A truncated datetime.
    """
    return datetime(date_time.year, date_time.month, date_time.day)


def truncate_dt_ymdh(date_time: datetime) -> datetime:
    """
    Truncates the provides datetime to only year, month, day, hour.

    :param date_time: The datetime to truncate.
    :return: A truncated datetime.
    """
    return datetime(date_time.year, date_time.month, date_time.day, date_time.hour)
