from __future__ import unicode_literals
from __future__ import division

from collections import deque
from numbers import Number

from pyluach.dates import HebrewDate
from pyluach.utils import memoize


def _adjust_postponed(date):
    """Return actual date of fast day.

    For usual date of a fast day returns fast day adjusted for any
    postponements.
    """
    if date.weekday() == 7:
        if date.month in [12, 13]:
            date -= 2
        else:
            date += 1
    return date


@memoize(maxlen=50)
def _fast_day_table(year):
    table = dict()
    workingdate = _adjust_postponed(HebrewDate(year, 7, 3))
    table[workingdate] = 'Tzom Gedalia'

    workingdate = _adjust_postponed(HebrewDate(year, 10, 10))
    table[workingdate] = '10 of Teves'

    month = 13 if Year(year).leap else 12
    workingdate = _adjust_postponed(HebrewDate(year, month, 13))
    table[workingdate] = 'Taanis Esther'

    workingdate = _adjust_postponed(HebrewDate(year, 4, 17))
    table[workingdate] = '17 of Tamuz'

    workingdate = _adjust_postponed(HebrewDate(year, 5, 9))
    table[workingdate] = '9 of Av'

    return table


def holiday(date, israel=False):
    """Return Jewish holiday of given date.

    The holidays include the major and minor religious Jewish
    holidays including fast days.

    Parameters
    ----------
    date : ``HebrewDate``, ``GregorianDate``, or ``JulianDay``
      Any date that implements a ``to_heb()`` method which returns a
      ``HebrewDate`` can be used.

    israel : boolian, optional
      ``True`` if you want the holidays according to the israel
      schedule. Defaults to ``False``.

    Returns
    -------
    str or ``None``
      The name of the holiday or ``None`` if the given date is not
      a Jewish holiday.
    """
    date = date.to_heb()
    year = date.year
    month = date.month
    day = date.day
    table = _fast_day_table(year)
    if date in table:
        return table[date]
    if month == 7:
        if day in range(1, 3):
            return 'Rosh Hashana'
        elif day == 10:
            return 'Yom Kippur'
        elif day in range(15, 22):
            return 'Succos'
        elif day == 22:
            return 'Shmini Atzeres'
        elif day == 23 and israel == False:
            return 'Simchas Torah'
    elif(
         (month == 9 and day in range(25, 30)) or
         date in [(HebrewDate(year, 9, 29) + n) for n in range(1, 4)]
         ):
        return 'Chanuka'
    elif month == 11 and day == 15:
        return "Tu B'shvat"
    elif month == 12:
        leap = HebrewDate._is_leap(year)
        if day == 14:
            return 'Purim Katan' if leap else 'Purim'
        if day == 15 and not leap:
            return 'Shushan Purim'
    elif month == 13:
        if day == 14:
                return 'Purim'
        elif day == 15:
            return 'Shushan Purim'
    elif month == 1 and day in range(15, 22 if israel else 23):
        return 'Pesach'
    elif month == 2 and day == 18:
        return "Lag Ba'omer"
    elif month == 3 and (day == 6 if israel else day in (6, 7)):
        return 'Shavuos'
    elif month == 5 and day == 15:
        return "Tu B'av"


class Year(object):

    """
    A Year object represents a Hebrew calendar year.

    It provided the following operators:

    =====================  ================================================
    Operation              Result
    =====================  ================================================
    year2 = year1 + int    New ``Year`` ``int``  days after year1.
    year2 = year1 - int    New ``Year`` ``int`` days before year1.
    int = year1 - year2    ``int`` equal to the absolute value of
                           the difference between year2 and year1.
    bool = year1 == year2  True if year1 represents the same year as year2.
    =====================  ================================================

    Parameters
    ----------
    year : int
      A Hebrew year.

    Attributes
    ----------
    year : int
      The hebrew year.
    leap : bool
      True if the year is a leap year else false.
    """

    def __init__(self, year):

        """
        The initializer for a Year object.
        """
        if year < 1:
            raise ValueError('Year {0} is before creation.'.format(year))
        self.year = year
        self.leap = HebrewDate._is_leap(year)

    def __repr__(self):
        return 'Year({0})'.format(self.year)

    def __len__(self):
        return HebrewDate._days_in_year(self.year)

    def __eq__(self, other):
        if isinstance(other, Year) and self.year == other.year:
            return True
        return False

    def __add__(self, other):
        """Add int to year."""
        return Year(self.year + other)

    def __sub__(self, other):
        """Subtract int or Year from Year.

        If other is an int return a new Year other before original year. If
        other is a Year object, return delta of the two years as an int.
        """
        if isinstance(other, Year):
            return abs(self.year - other.year)
        else:
            try:
                return Year(self.year - other)
            except AttributeError:
                raise TypeError('Only an int or another Year object can'
                                ' be subtracted from a year.')

    def __iter__(self):
        """Yield integer for each month in year."""
        months = [7, 8, 9, 10, 11, 12, 13, 1, 2, 3, 4, 5, 6]
        if not self.leap:
            months.remove(13)
        for month in months:
            yield month

    def itermonths(self):
        """Yield Month instance for each month of the year.

        Yields
        ------
        Month
          The next month in the Hebrew calendar year as a
          ``luachcal.hebrewcal.Month`` instance beginning with
          Tishrei and ending with Elul.
        """
        for month in self:
            yield Month(self.year, month)

    def iterdays(self):
        """Yield integer for each day of the year.

        Yields
        ------
        int
          An integer beginning with 1 representing the next day of
          the year.
        """
        for day in range(1, len(self) + 1):
            yield day

    def iterdates(self):
        """Yield HebrewDate instance for each day of the year.

        Yields
        ------
        ``HebrewDate``
            The next date of the Hebrew calendar year starting with
            the first of Tishrei.
        """
        for month in self.itermonths():
            for day in month:
                yield HebrewDate(self.year, month.month, day)


class Month(object):

    """
    A Month object represents a month of the Hebrew calendar.

    Parameters
    ----------
    year : int
    month : int
      The month as an integer starting with 7 for Tishrei through 13
      if necessary for Adar Sheni and then 1-6 for Nissan - Elul.

    Attributes
    ----------
    year : int
      The Hebrew year.
    month : int
      The month as an integer starting with 7 for Tishrei through 13
      if necessary for Adar Sheni and then 1-6 for Nissan - Elul.
    name : str
      The name of the month.
    """

    _monthnames = {7: 'Tishrei', 8: 'Cheshvan', 9: 'Kislev', 10: 'Teves',
                  11: 'Shvat', 13:'Adar Sheni', 1: 'Nissan', 2: 'Iyar',
                  3: 'Sivan', 4: 'Tamuz', 5: 'Av', 6: 'Elul'}

    def __init__(self, year, month):
        if year < 1:
            raise ValueError('Year is before creation.')
        self.year = year
        leap = HebrewDate._is_leap(self.year)
        yearlength = 13 if leap else 12
        if month < 1 or month > yearlength:
            raise ValueError('''Month must be between 1 and 12 for a normal
            year and 13 for a leap year.''')
        self.month = month
        self._monthnames[12] = 'Adar Rishon' if leap else 'Adar'
        self.name = self._monthnames[self.month]

    def __repr__(self):
        return 'Month({0}, {1})'.format(self.year, self.month)

    def __len__(self):
        return HebrewDate._month_length(self.year, self.month)

    def __iter__(self):
        for day in range(1, len(self) + 1):
            yield day

    def __eq__(self, other):
        if(
           isinstance(other, Month) and
           self.year == other.year and
           self.month == other.month):
            return True
        return False

    def __add__(self, other):
        yearmonths = list(Year(self.year))
        index = yearmonths.index(self.month)
        leftover_months = len(yearmonths[index + 1:])
        if other <= leftover_months:
            return Month(self.year, yearmonths[index + other])
        return Month(self.year + 1, 7).__add__(other - 1 - leftover_months)


    def __sub__(self, other):
        if isinstance(other, Number):
            yearmonths = list(Year(self.year))
            index = yearmonths.index(self.month)
            leftover_months = index
            if other <= leftover_months:
                return Month(self.year, yearmonths[index - other])
            return Month(self.year - 1,
                         deque(Year(self.year - 1), maxlen=1).pop()).__sub__(
                                                    other - 1 - leftover_months
                                                    )
                    # Recursive call on the last month of the previous year. 
        try:
            return abs(self._elapsed_months() - other._elapsed_months())
        except AttributeError:
            raise TypeError('''You can only subtract a number or a month
                            object from a month''')

    def starting_weekday(self):
        """Return first weekday of the month.

        Returns
        -------
        int
          The weekday of the first day of the month starting with Sunday as 1
          through Saturday as 7.
        """
        return HebrewDate(self.year, self.month, 1).weekday()

    def _elapsed_months(self):
        '''Return number of months elapsed from beginning of calendar'''
        yearmonths = tuple(Year(self.year))
        months_elapsed = (
                      (235 * ((self.year-1) // 19)) +
                      (12 * ((self.year-1) % 19)) +
                      (7 * ((self.year-1) % 19) + 1) // 19 +
                      yearmonths.index(self.month)
                      )
        return months_elapsed

    def iterdates(self):
        """Return iterator that yields an instance of HebrewDate.

        Yields
        ------
        ``HebrewDate``
          The next Hebrew Date of the year starting the first day of
          Tishrei through the last day of Ellul.
        """
        for day in self:
            yield HebrewDate(self.year, self.month, day)

