#
# Copyright 2015 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import typing
import pandas as pd

if typing.TYPE_CHECKING:
    from exchange_calendars import ExchangeCalendar


class CalendarError(Exception):
    msg = None

    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def __str__(self):
        msg = self.msg.format(**self.kwargs)
        return msg

    __unicode__ = __str__
    __repr__ = __str__


class InvalidCalendarName(CalendarError):
    """
    Raised when a calendar with an invalid name is requested.
    """

    msg = "The requested ExchangeCalendar, {calendar_name}, does not exist."


class CalendarNameCollision(CalendarError):
    """
    Raised when the static calendar registry already has a calendar with a
    given name.
    """

    msg = "A calendar with the name {calendar_name} is already registered."


class CyclicCalendarAlias(CalendarError):
    """
    Raised when calendar aliases form a cycle.
    """

    msg = "Cycle in calendar aliases: [{cycle}]"


class NoSessionsError(CalendarError):
    """Raised if a requested calendar would have no sessions.

    NoSessionsError should be raised if `get_calendar` `start` and `end`
    parameters are passed as dates between and inclusive of which there are
    no sessions for the given calendar.
    """

    msg = (
        "The requested ExchangeCalendar, {calendar_name}, cannot be created as"
        " there would be no sessions between the requested `start` ('{start}')"
        " and `end` ('{end}') dates."
    )


class NotSessionError(ValueError):
    """Input does not represent a valid session.

    Raised if parameter expecting a session label receives input that
    parses correctly (UTC midnight) although is not a session.

    Parameters
    ----------
    calendar
        Calendar for which `ts` assumed as a session.

    ts
        Timestamp assumed as a session.

    param_name
        Name of a parameter that was to receive a session label.
    """

    def __init__(self, calendar: ExchangeCalendar, ts: pd.Timestamp, param_name: str):
        self.calendar = calendar
        self.ts = ts
        self.param_name = param_name

    def __str__(self) -> str:
        msg = (
            f"Parameter `{self.param_name}` takes a session"
            f" although received input that parsed to '{self.ts}' which"
        )

        if self.ts < self.calendar.first_session:
            msg += (
                " is earlier than the first session of calendar"
                f" '{self.calendar.name}' ('{self.calendar.first_session}')."
            )
        elif self.ts > self.calendar.last_session:
            msg += (
                " is later than the last session of calendar"
                f" '{self.calendar.name}' ('{self.calendar.last_session}')."
            )
        else:
            msg += f" is not a session of calendar '{self.calendar.name}'."
        return msg


class DateOutOfBounds(ValueError):
    """A date required to be within sessions' bounds is not.

    Parameters
    ----------
    calendar
        Calendar for which `date` required to be within sessions'
        bounds.

    date
        Date required to be within `calendar`'s sessions' bounds.

    param_name
        Name of a parameter receiving date.
    """

    def __init__(self, calendar: ExchangeCalendar, date: pd.Timestamp, param_name: str):
        self.calendar = calendar
        self.date = date
        self.param_name = param_name

    def __str__(self) -> str:
        msg = f"Parameter `{self.param_name}` receieved as '{self.date}' although"
        if self.date < self.calendar.first_session:
            msg += (
                " cannot be earlier than the first session of calendar"
                f" '{self.calendar.name}' ('{self.calendar.first_session}')."
            )
        elif self.date > self.calendar.last_session:
            msg += (
                " cannot be later than the last session of calendar"
                f" '{self.calendar.name}' ('{self.calendar.last_session}')."
            )
        else:
            assert (
                self.date < self.calendar.first_session
                or self.date > self.calendar.last_session
            )
        return msg


class NotTradingMinuteError(ValueError):
    """A timestamp assumed as a trading minute is not a trading minute.

    Parameters
    ----------
    calendar
        Calendar for which `minute` assumed as a trading minute.

    minute
        Minute assumed as a trading minute.

    param_name
        Name of a parameter that was to receive a trading minute.
    """

    def __init__(
        self,
        calendar: ExchangeCalendar,
        minute: pd.Timestamp,
        param_name: str,
    ):
        self.calendar = calendar
        self.minute = minute
        self.param_name = param_name

    def __str__(self) -> str:
        msg = (
            f"Parameter `{self.param_name}` takes a trading minute although"
            f" received input that parsed to '{self.minute}' which"
        )
        if self.minute < self.calendar.first_minute:
            msg += (
                " is earlier than the first trading minute of calendar"
                f" '{self.calendar.name}' ('{self.calendar.first_session}')."
            )
        elif self.minute > self.calendar.last_minute:
            msg += (
                " is later than the last trading minute of calendar"
                f" '{self.calendar.name}' ('{self.calendar.last_session}')."
            )
        else:
            msg += f" is not a trading minute of calendar '{self.calendar.name}'."
        return msg


class MinuteOutOfBounds(ValueError):
    """A minute required to be within bounds of trading minutes is not.

    Parameters
    ----------
    calendar
        Calendar for which `minute` required to be within bounds of
        trading minutes.

    minute
        Minute required to be within bounds of `calendar`'s trading
        minutes.

    param_name
        Name of a parameter receiving `minute`.
    """

    def __init__(
        self, calendar: ExchangeCalendar, minute: pd.Timestamp, param_name: str
    ):
        self.calendar = calendar
        self.minute = minute
        self.param_name = param_name

    def __str__(self) -> str:
        msg = f"Parameter `{self.param_name}` receieved as '{self.minute}' although"
        if self.minute < self.calendar.first_minute:
            msg += (
                " cannot be earlier than the first trading minute of calendar"
                f" '{self.calendar.name}' ('{self.calendar.first_minute}')."
            )
        elif self.minute > self.calendar.last_minute:
            msg += (
                " cannot be later than the last trading minute of calendar"
                f" '{self.calendar.name}' ('{self.calendar.last_minute}')."
            )
        else:
            assert (
                self.minute < self.calendar.first_minute
                or self.minute > self.calendar.last_minute
            )
        return msg


class RequestedSessionOutOfBounds(ValueError):
    """The requested session would fall beyond calendar bounds.

    Parameters
    ----------
    calendar
        Calendar for which session would be out-of-bounds.

    too_early
        True if requested session would be earlier than the first calendar
            session.
        False if requested session would be later than the last calendar
            session.
    """

    def __init__(self, calendar: ExchangeCalendar, too_early: bool):
        self.calendar = calendar
        self.adverb = "before" if too_early else "after"
        self.position = "first" if too_early else "last"
        self.bound = calendar.first_session if too_early else calendar.last_session

    def __str__(self) -> str:
        return (
            f"Requested session would fall {self.adverb} the calendar's {self.position}"
            f" session ('{self.bound}')."
        )


class RequestedMinuteOutOfBounds(ValueError):
    """The requested trading minute would fall beyond calendar bounds.

    Parameters
    ----------
    calendar
        Calendar for which minute would be out-of-bounds.

    too_early
        True if requested minute would be earlier than the first calendar
            minute.
        False if requested minute would be later than the last calendar
            minute.
    """

    def __init__(self, calendar: ExchangeCalendar, too_early: bool):
        self.calendar = calendar
        self.adverb = "before" if too_early else "after"
        self.position = "first" if too_early else "last"
        self.bound = calendar.first_minute if too_early else calendar.last_minute

    def __str__(self) -> str:
        return (
            f"Requested minute would fall {self.adverb} the calendar's {self.position}"
            f" trading minute ('{self.bound}')."
        )


class IndexOverlapError(ValueError):
    """Periods implied by indices overlap."""


class IntervalsOverlapError(IndexOverlapError):
    """Intervals of requested trading index would overlap."""

    # pylint: disable=missing-return-type-doc
    def __str__(self):  # noqa: D105
        return (
            "Unable to create trading index as intervals would overlap."
            " This can occur when the frequency is longer than a break or"
            " the gap between one session's close and the next session's"
            " open. To shorten intervals that would otherwise overlap"
            " either pass `curtail_overlaps` as True or pass"
            " `force_close` and/or `force_break_close` as True."
        )


class IndicesOverlapError(IndexOverlapError):
    """Indices of requested trading index would overlap."""

    # pylint: disable=missing-return-type-doc
    def __str__(self):  # noqa: D105
        return (
            "Unable to create trading index as an indice would fall to the"
            " right of (later than) the subsequent indice. This can occur"
            " when the frequency is longer than a break or the frequency"
            " is longer than the gap between one session's close and the"
            " next session's open. Consider  passing `closed` as `left`"
            " or passing `force_close` and/or `force_break_close`"
            " as True."
        )
