# -*- coding: utf-8 -*-
# This file is part of convertdate.
# http://github.com/fitnr/convertdate
# http://opensource.org/licenses/MIT
# Copyright (c) 2016, fitnr <fitnr@fakeisthenewreal>
# Licensed under the MIT license
"""
The `French Republican calendar <https://en.wikipedia.org/wiki/French_Republican_calendar>`__
was created during the heroic overthrow of the Ancien Regime.

Leap year calculations in the French Republican calendar are a matter of
dispute. By default, `convertdate` calculates leap years using the
autumnal equinox. You can also use one of three more systematic methods
proposed over the years.

-   Romme, a co-creator of the calendar, proposed leap years in years
    divisible by four, except for years divisible by 100.
-   Some concordances were drawn up in the 19th century that gave leap
    years every 4 years, in years that give a remainder of three when
    divided by four (19, 23, 27, etc...).
-   Von Mädler proposed leap years in years divisible by four, except
    for years divisible by 128.

You can specify any of these three methods with the method keyword
argument in `french_republican` conversion functions.

.. code-block:: python

   from convertdate import french_republican

   french_republican.to_gregorian(20, 1, 1), method='romme')
   # (1811, 9, 23)

   french_republican.to_gregorian(20, 1, 1), method='continuous')
   # (1811, 9, 24)

   french_republican.to_gregorian(20, 1, 1), method='madler')
   # (1811, 9, 23)

All the conversion methods correctly assign the leap years implemented
while calendar was in use (3, 7, 11).
"""
from math import trunc

from pymeeus.Sun import Sun

from . import gregorian
from .data.french_republican_days import french_republican_days

# julian day (1792, 9, 22)
EPOCH = 2375839.5

YEAR_EPOCH = 1791.0

DAYS_IN_YEAR = 365.0

MOIS = MONTHS = [
    "Vendémiaire",
    "Brumaire",
    "Frimaire",
    "Nivôse",
    "Pluviôse",
    "Ventôse",
    "Germinal",
    "Floréal",
    "Prairial",
    "Messidor",
    "Thermidor",
    "Fructidor",
    "Sansculottides",
]

LEAP_CYCLE_DAYS = 1461.0  # 365 * 4 + 1
LEAP_CYCLE_YEARS = 4.0


def leap(year, method=None):
    """
    Determine if this is a leap year in the FR calendar using one of three methods: 4, 100, 128
    (every 4th years, every 4th or 400th but not 100th, every 4th but not 128th)

    Methods:
        * 4 (concordance rule): leap every four years: 3, 7, 11, 15, ... etc
        * 100 (Romme's rule): leap every 4th and 400th year, but not 100th:
            20, 24, ... 96, 104, ... 396, 400, 404 ...
        * 128 (von Mädler's rule): leap every 4th but not 128th: 20, 24, ... 124, 132, ...
        * equinox [default]: use calculation of the equinox to determine date, never returns a leap year
    """
    method = method or 'equinox'

    if year in (3, 7, 11):
        return True

    if year < 15:
        return False

    if method in (4, 'continuous') or (year <= 16 and method in (128, 'madler', 4, 'continuous')):
        return year % 4 == 3

    if method in (100, 'romme'):
        return (year % 4 == 0 and year % 100 != 0) or year % 400 == 0

    if method in (128, 'madler'):
        return year % 4 == 0 and year % 128 != 0

    if method == 'equinox':
        # Is equinox on 366th day after (year, 1, 1)
        startjd = to_jd(year, 1, 1, method='equinox')
        if premier_da_la_annee(startjd + 367) - startjd == 366.0:
            return True
    else:
        raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox")

    return False


def _previous_fall_equinox(jd):
    '''Return the julian day count of the previous fall equinox.'''
    y, _, _ = gregorian.from_jd(jd)
    eqx = Sun.get_equinox_solstice(y, "autumn").jde()
    if eqx > jd:
        eqx = Sun.get_equinox_solstice(y - 1, "autumn").jde()

    return eqx


def _next_fall_equinox(jd):
    '''Return the julian day count of the previous fall equinox.'''
    y, _, _ = gregorian.from_jd(jd)
    eqx = Sun.get_equinox_solstice(y, "autumn").jde()
    if eqx < jd:
        eqx = Sun.get_equinox_solstice(y + 1, "autumn").jde()

    return eqx


def premier_da_la_annee(jd):
    """
    Returns Julian day number containing fall equinox (first day of the FR year)
    of the current FR year.
    """
    previous = trunc(_previous_fall_equinox(jd) - 0.5) + 0.5

    if previous + 364 < jd:
        # test if current day is the equinox if the previous equinox was a long time ago
        nxt = trunc(_next_fall_equinox(jd) - 0.5) + 0.5

        if nxt <= jd:
            return nxt

    return previous


def to_jd(year, month, day, method=None):
    '''Obtain Julian day from a given French Revolutionary calendar date.'''
    method = method or 'equinox'

    if day < 1 or day > 30:
        raise ValueError("Invalid day for this calendar")

    if month > 13:
        raise ValueError("Invalid month for this calendar")

    if month == 13 and day > 5 + leap(year, method=method):
        raise ValueError("Invalid day for this month in this calendar")

    if method == 'equinox':
        return _to_jd_equinox(year, month, day)

    return _to_jd_schematic(year, month, day, method)


def _to_jd_schematic(year, month, day, method):
    '''Calculate JD using various leap-year calculation methods'''
    y0, y1, y2, y3, y4, y5 = 0, 0, 0, 0, 0, 0

    intercal_cycle_yrs, over_cycle_yrs, leap_suppression_yrs = None, None, None

    # Use the every-four-years method below year 16 (madler) or below 15 (romme)
    if (method in (100, 'romme') and year < 15) or (method in (128, 'madler') and year < 17):
        method = 4

    if method in (4, 'continuous'):
        # Leap years: 15, 19, 23, ...
        y5 = -365

    elif method in (100, 'romme'):
        year = year - 13
        y5 = DAYS_IN_YEAR * 12 + 3

        leap_suppression_yrs = 100.0
        leap_suppression_days = 36524  # leap_cycle_days * 25 - 1

        intercal_cycle_yrs = 400.0
        intercal_cycle_days = 146097  # leap_suppression_days * 4 + 1

        over_cycle_yrs = 4000.0
        over_cycle_days = 1460969  # intercal_cycle_days * 10 - 1

    elif method in (128, 'madler'):
        year = year - 17
        y5 = DAYS_IN_YEAR * 16 + 4

        leap_suppression_days = 46751  # 32 * leap_cycle_days - 1
        leap_suppression_yrs = 128

    else:
        raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox")

    if over_cycle_yrs:
        y0 = trunc(year / over_cycle_yrs) * over_cycle_days
        year = year % over_cycle_yrs

    # count intercalary cycles in days (400 years long or None)
    if intercal_cycle_yrs:
        y1 = trunc(year / intercal_cycle_yrs) * intercal_cycle_days
        year = year % intercal_cycle_yrs

    # count leap suppresion cycles in days (100 or 128 years long)
    if leap_suppression_yrs:
        y2 = trunc(year / leap_suppression_yrs) * leap_suppression_days
        year = year % leap_suppression_yrs

    y3 = trunc(year / LEAP_CYCLE_YEARS) * LEAP_CYCLE_DAYS
    year = year % LEAP_CYCLE_YEARS

    # Adjust 'year' by one to account for lack of year 0
    y4 = year * DAYS_IN_YEAR

    yj = y0 + y1 + y2 + y3 + y4 + y5

    mj = (month - 1) * 30

    return EPOCH + yj + mj + day - 1


def _to_jd_equinox(an, mois, jour):
    '''Return jd of this FR date, counting from the previous equinox.'''
    day_of_adr = (30 * (mois - 1)) + (jour - 1)
    equinoxe = _next_fall_equinox(gregorian.to_jd(an + YEAR_EPOCH, 1, 1))
    return trunc(equinoxe - 0.5) + 0.5 + day_of_adr


def from_jd(jd, method=None):
    """Calculate date in the French Revolutionary
    calendar from Julian day.  The five or six
    "sansculottides" are considered a thirteenth
    month in the results of this function."""
    method = method or 'equinox'

    if method == 'equinox':
        return _from_jd_equinox(jd)

    return _from_jd_schematic(jd, method)


def _from_jd_schematic(jd, method):
    '''Convert from JD using various leap-year calculation methods'''
    if jd < EPOCH:
        raise ValueError("Can't convert days before the French Revolution")

    # days since Epoch
    J = trunc(jd) + 0.5 - EPOCH

    y0, y1, y2, y3, y4, y5 = 0, 0, 0, 0, 0, 0
    intercal_cycle_days = leap_suppression_days = over_cycle_days = None

    # Use the every-four-years method below year 17
    if (J <= DAYS_IN_YEAR * 12 + 3 and method in (100, 'romme')) or (
        J <= DAYS_IN_YEAR * 17 + 4 and method in (128, 'madler')
    ):
        method = 4

    # set p and r in Hatcher algorithm
    if method in (4, 'continuous'):
        # Leap years: 15, 19, 23, ...
        # Reorganize so that leap day is last day of cycle
        J = J + 365
        y5 = -1

    elif method in (100, 'romme'):
        # Year 15 is not a leap year
        # Year 16 is leap, then multiples of 4, not multiples of 100, yes multiples of 400
        y5 = 12
        J = J - DAYS_IN_YEAR * 12 - 3

        leap_suppression_yrs = 100.0
        leap_suppression_days = 36524  # LEAP_CYCLE_DAYS * 25 - 1

        intercal_cycle_yrs = 400.0
        intercal_cycle_days = 146097  # leap_suppression_days * 4 + 1

        over_cycle_yrs = 4000.0
        over_cycle_days = 1460969  # intercal_cycle_days * 10 - 1

        y0 = trunc(J / over_cycle_days) * over_cycle_yrs
        J = J % over_cycle_days

        y1 = trunc(J / intercal_cycle_days) * intercal_cycle_yrs
        J = J % intercal_cycle_days

    elif method in (128, 'madler'):
        # Year 15 is a leap year, then year 20 and multiples of 4, not multiples of 128
        y5 = 16
        J = J - DAYS_IN_YEAR * 16 - 4

        leap_suppression_yrs = 128
        leap_suppression_days = 46751  # 32 * leap_cycle_days - 1

    else:
        raise ValueError("Unknown leap year method. Try: continuous, romme, madler or equinox")

    if leap_suppression_days:
        y2 = trunc(J / leap_suppression_days) * leap_suppression_yrs
        J = J % leap_suppression_days

    y3 = trunc(J / LEAP_CYCLE_DAYS) * LEAP_CYCLE_YEARS

    if J % LEAP_CYCLE_DAYS == LEAP_CYCLE_DAYS - 1:
        J = 1460
    else:
        J = J % LEAP_CYCLE_DAYS

    # 0 <= J <= 1460
    # J needs to be 365 here on leap days ONLY

    y4 = trunc(J / DAYS_IN_YEAR)

    if J == DAYS_IN_YEAR * 4:
        y4 = y4 - 1
        J = 365.0
    else:
        J = J % DAYS_IN_YEAR

    year = y0 + y1 + y2 + y3 + y4 + y5

    month = trunc(J / 30.0)
    J = J - month * 30

    return trunc(year) + 1, month + 1, trunc(J) + 1


def _from_jd_equinox(jd):
    '''Calculate the FR day using the equinox as day 1'''
    jd = trunc(jd) + 0.5
    equinoxe = premier_da_la_annee(jd)

    an = int(gregorian.from_jd(equinoxe)[0] - YEAR_EPOCH)
    mois = trunc((jd - equinoxe) / 30.0) + 1
    jour = int((jd - equinoxe) % 30) + 1

    return (an, mois, jour)


def decade(jour):
    return trunc(jour / 100.0) + 1


def day_name(month, day):
    return french_republican_days[month][day - 1]


def from_gregorian(year, month, day, method=None):
    return from_jd(gregorian.to_jd(year, month, day), method=method)


def to_gregorian(an, mois, jour, method=None):
    return gregorian.from_jd(to_jd(an, mois, jour, method=method))


def format(an, mois, jour):
    """Convert a FR date into a string with the format DD MONTH YYYY."""
    # pylint: disable=redefined-builtin
    return "{0} {1} {2}".format(jour, MOIS[mois - 1], an)
