import ceylon.time.base { days, ms=milliseconds, years }
import ceylon.time.internal.math { floor, fdiv=floorDiv, mod=floorMod }

"Converts _Rata Die_ day number to a fixed date value.

 _Rata Die_ is fixed at Monday, January 1st, 1. (Gregorian)."
shared Integer rd( Integer t ) {
    value epoch = 0; // origin of all calculations
    return t - epoch;
}

"Common properties of _Unix time_."
shared object unixTime {

    "Fixed date value of the _Unix time_ epoch (1970-01-01)."
    shared Integer epoch => gregorian.fixedFrom([1970, 1, 1]);

    "Returns a _fixed date_ from the _unix time_ value."
    shared Integer fixedFromTime(Integer time) {
        return fdiv(time, ms.perDay) + epoch;
    }

    "Return milliseconds elapsed from 1970-01-01 00:00:00."
    shared Integer timeFromFixed( Integer date ) {
        return (date - epoch) * ms.perDay;
    }

    "Returns _time of day_ in milliseconds for the specified _unix time_ value."
    shared Integer timeOfDay( Integer time ) {
        return mod(time, ms.perDay);
    }
}

"Generic base interface of a _calendar system_.
 Chronology serves as a computational backend to 
 a Date representation of the same calendar system."
shared interface Chronology<Fields>
       given Fields satisfies Anything[] {

    "Epoch is the offset of the _fixed date_ day number that defines 
     the beginning of the calendar."
    shared formal Integer epoch;

    "Converts date tuple of this calendar system to an equivalent _fixed date_
     representation of the day of era."
    shared formal Integer fixedFrom( Fields date );

    "Converts a _fixed day_ number to a calendar date tuple."
    shared formal Fields dateFrom( Integer fixed );

    "Validate the given date."
    shared formal void checkDate( Fields date );

}

"An interface for calendar system that defines leap year rules.
 
 *Note:* This interface is meant to convey a Calendar that has some sort of leap year syntax."
shared interface LeapYear<Self, Fields> of Self
       satisfies Chronology<Fields>
       given Self satisfies Chronology<Fields>
       given Fields satisfies Anything[] {

    "Returns true if the specified year is a leap year according to the leap year rules of the given chronology."
    shared formal Boolean leapYear( Integer leapYear );
}

"Base class for a gregorian calendar chronology."
abstract shared class GregorianCalendar() of gregorian
         satisfies Chronology<[Integer, Integer, Integer]>
                 & LeapYear<GregorianCalendar, [Integer, Integer, Integer]> {}

"Represents the implementation of all calculations for
 the rules based on Gregorian Calendar."
shared object gregorian extends GregorianCalendar() {

    "Epoch of the gregorian calendar."
    shared actual Integer epoch = rd(1);

    shared Integer january = 1;
    shared Integer february = 2;
    shared Integer march = 3;
    shared Integer april = 4;
    shared Integer may = 5;
    shared Integer june = 6;
    shared Integer july = 7;
    shared Integer august = 8;
    shared Integer september = 9;
    shared Integer october = 10;
    shared Integer november = 11;
    shared Integer december = 12;

    "Gregorian leap year rule states that every fourth year
     is a leap year except century years not divisible by 400."
    shared actual Boolean leapYear(Integer year) {
        return (year % 100 == 0) then year % 400 == 0
                                 else year % 4 == 0;
    }

    "Return the _day of era_ from a given date."
    Integer fixed(Integer year, Integer month, Integer day) {
        return epoch - 1 + 365 * (year - 1) + floor((year - 1) / 4.0)
               - floor((year - 1) / 100.0) + floor((year - 1) / 400.0)
               + floor((367 * month - 362) / 12.0)
               + ((month > 2) then (leapYear(year) then -1 else -2) else 0)
               + day;
    }

    "Return the _day of era_ from a given date."
    shared actual Integer fixedFrom([Integer, Integer, Integer] date) {
        return fixed(date[0], date[1], date[2]);
    }

    "Assert that specified date has it conjunction of year, month and day as valid gregorian values."
    shared actual void checkDate([Integer, Integer, Integer] date) {
        "Invalid year value"
        assert(years.minimum <= date[0] && date[0] <= years.maximum);

        "Invalid date value"
        assert( date == dateFrom( fixedFrom(date) ) );
    }

    "Returns fixed date value of the first day of the gregorian year."
    shared Integer newYear(Integer year){
        return fixed(year, january, 1);
    }

    "Returns fixed date value of the last day of the gregorian year."
    shared Integer yearEnd(Integer year){
        return fixed(year, december, 31);
    }

    "Returns a gregorian year number of the fixed date value."
    shared Integer yearFrom(Integer fixed) {
        value d0 = fixed - epoch;
        value n400 = fdiv(d0, days.perFourCenturies);
        value d1 = mod(d0, days.perFourCenturies);
        value n100 = fdiv(d1, days.perCentury);
        value d2 = mod(d1, days.perCentury);
        value n4 = fdiv(d2, days.inFourYears);
        value d3 = mod(d2, days.inFourYears);
        value n1 = fdiv(d3, days.perYear);
        value year = 400 * n400 + 100 * n100 + 4 * n4 + n1;
        return (n100 == 4 || n1 == 4) then year else year + 1;
    }

    "Converts the fixed date value to an equivalent gregorian date."
    shared actual [Integer, Integer, Integer] dateFrom(Integer date) {
        value year = yearFrom(date);
        value priorDays = date - newYear(year);
        value correction = (date < fixed(year, march, 1)) 
                then 0 else (leapYear(year) then 1 else 2);
        value month = fdiv(12 * (priorDays + correction) + 373, 367);
        value day = 1 + date - fixed(year, month, 1);
        return [year, month, day];
    }

    "Returns the month number of the gregorian calendar from the fixed date value."
    shared Integer monthFrom(Integer date){
        return dateFrom(date)[1];
    }

    "Returns day of month value of the fixed date value."
    shared Integer dayFrom(Integer date){
        return dateFrom(date)[2];
    }

    "Returns _day of week_ value for the specified fixed date value."
    shared Integer dayOfWeekFrom(Integer date) {
        return mod(date, 7);
    }

}