package ch.epfl.isochrone.timetable;

import static ch.epfl.isochrone.math.Math.*;

/**
 * Classe servant à stocker des dates
 * 
 * @author Charles Thiebaut (238935)
 * @author Arnaud Delamare (234841)
 * 
 */
public final class Date implements Comparable<Date> {

    private final int day;
    private final Month month;
    private final int year;

    /**
     * Contructeur Principale de la classe
     * 
     * @throws IllegalArgumentException
     *             si la date est négatif ou est plus grand que le jour dans le
     *             mois conserné.
     * @param day
     *            le jour du mois.
     * @param month
     *            le mois de l'année.
     * @param year
     *            l'année en cours.
     */
    public Date(int day, Month month, int year) {
        if (day < 1 || day > daysInMonth(month, year)) {
            throw new IllegalArgumentException();
        }
        this.day = day;
        this.month = month;
        this.year = year;
    }

    /**
     * Contructeur secondaire de la classe qui appelle le contructeur principale
     * avec d'autre type de paramètres.
     * 
     * @param day
     *            le jour du mois.
     * @param month
     *            le mois de l'année (en int).
     * @param year
     *            l'année en cours.
     */
    public Date(int day, int month, int year) {
        this(day, intToMonth(month), year);
    }

    /**
     * Contructeur secondaire de la classe qui appelle le contructeur principale
     * avec d'autre type de paramètres.
     * 
     * @param date
     *            une date créé à partir de la bibliothèque JAVA.
     */
    @SuppressWarnings("deprecation")
    public Date(java.util.Date date) {
        this(date.getDate(), date.getMonth() + 1, date.getYear() + 1900);
    }

    /**
     * @return le jour.
     */
    public int day() {
        return day;
    }

    /**
     * @return le mois.
     */
    public Month month() {
        return month;
    }

    /**
     * @return le mois en int.
     */
    public int intMonth() {
        return monthToInt(this.month);
    }

    /**
     * @return l'année.
     */
    public int year() {
        return year;
    }

    /**
     * @return le jour de la semaine de la date.
     */
    public DayOfWeek dayOfWeek() {
        int n = fixed()- 1;
        n = modF(n, 7);

        DayOfWeek[] jour = DayOfWeek.values();
        return jour[n];
    }

    /**
     * @return la date distance du nombre de jour donné de la date de
     *         l'instance.
     * @param daysDiff
     *            nb de jour de différence.
     */
    public Date relative(int daysDiff) {
        return fixedToDate(fixed() + daysDiff);
    }

    /**
     * @return la date JAVA qui correspond a la date de l'instance.
     */
    @SuppressWarnings("deprecation")
    public java.util.Date toJavaDate() {
        return new java.util.Date(this.year - 1900, monthToInt(this.month) - 1,
                day);
    }

    /**
     * @return un string qui retoune une représentation textuelle de la date.
     * 
     */
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(year);
        sb.append('-');
        
        int intMonth = intMonth();
        
        if (intMonth < 10) {
            sb.append(0); 
        }
        
        sb.append(intMonth);
        sb.append('-');
        
        if (day < 10) {
            sb.append(0); 
        }
        
        sb.append(day);
        
        return sb.toString();
    }

    /**
     * vérifie que l'objet "that" est une instance de date puis si celui-ci
     * correspond a la même, il retourne TRUE si c'est le cas.
     * 
     * @return un boolean vrai si l'objet "that" est égale à l'instance de date.
     * @param that
     *            l'objet que l'on vérifie comme étant equale.
     */
    public boolean equals(Object that) {
        if (that.getClass() == this.getClass()) {
            if (((Date) that).fixed()==this.fixed()) {
                return true;
            }
        }
        return false;
    }

    /**
     * @return le nombre de jour depuis le premier janvier de l'an 1.
     */
    public int hashCode() {
        return this.fixed();
    }

    /**
     * @return -1 si la date à laquelle on applique est antérieure la date passé
     *         en argument. 0 si elles sont egale. 1 si la date à laquelle on
     *         applique est postérieure la date passé en argument.
     * @param that
     *            l'objet que l'on veut comparer.
     */
    public int compareTo(Date that) {
        int a = this.fixed();
        int b = that.fixed();

        if (a < b) {
            return -1;
        } else {
            if (a == b) {
                return 0;
            } else {
                return 1;
            }
        }
    }

    /**
     * @param m
     *            le No du mois (entre 1 et 12)
     * @throws IllegalArgumentException
     *             si le mois m entre en argument est ou négatif ou plus grand
     *             que douze.
     * @return l'objet mois correspond au numéro entre en paramètre.
     * 
     */
    private static Month intToMonth(int m) { 
        if (m < 1 || m > 12) {
            throw new IllegalArgumentException();
        }

        Month[] months = Month.values();

        return months[m - 1]; // le tableau stocke de 0 à 11, alors que les
                            // numeros des mois sont entre 1 et 12
    }

    /**
     * @param m
     *            l'objet mois que l'on souhaite convertir en int.
     * @return le mois entré en paramètre en int.
     * 
     */
    private static int monthToInt(Month m) {
        return m.ordinal() + 1;
    }

    /**
     * @param year
     *            l'année que l'on souhaite tester.
     * @return TRUE si l'année est bissextile.
     * 
     */
    private static boolean isLeapYear(int year) {
        if (modF(year, 4) == 0 && modF(year, 400) == 0)
            return true;
        
        if (modF(year, 4) == 0 && modF(year, 100) != 0)
            return true;
        
        return false;
    }

    /**
     * @param m
     *            le mois doit on veut le nombre de jour.
     * @param y
     *            l'année, utile pour vérifier si celle-ci est bissextile.
     * 
     * @return le nombre de jour pour un mois donné.
     * 
     */
    private static int daysInMonth(Month m, int y) {
        switch (m) {

        case JANUARY:
            return 31;
        case FEBRUARY:
            if (isLeapYear(y)) {
                return 29;
            } else {
                return 28;
            }
        case MARCH:
            return 31;
        case APRIL:
            return 30;
        case MAY:
            return 31;
        case JUNE:
            return 30;
        case JULY:
            return 31;
        case AUGUST:
            return 31;
        case SEPTEMBER:
            return 30;
        case OCTOBER:
            return 31;
        case NOVEMBER:
            return 31;
        case DECEMBER:
            return 31;
        default:
            throw new Error();
        }
    }

    /**
     * @param d
     *            le jour du mois que l'on souhaite convertir
     * @param m
     *            le mois de l'année que l'on souhaite convertir.
     * @param y
     *            l'année de la date que l'on convertir.
     * 
     * @return le nombre de jour entre la date passé en argument et le 1 janvier
     *         de l'an 1.
     * 
     */
    private static int dateToFixed(int d, Month m, int y) {
        int y0 = y - 1;
        boolean l = isLeapYear(y);
        int c;

        if (monthToInt(m) <= 2) {
            c = 0;
        } else {
            if (l) {
                c = -1;
            } else {
                c = -2;
            }
        }

        return 365 * y0 + divF(y0, 4) - divF(y0, 100) + divF(y0, 400)
                + divF(367 * monthToInt(m) - 362, 12) + c + d;
    }

    /**
     * @param n
     *            le nb de nombre depuis le premier janvier de l'an 1, nombre
     *            que l'on souhaite convertir en date
     * 
     * @return la date conresponde au nombre de jour passé en argument.
     * 
     */
    private static Date fixedToDate(int n) {
        int d0 = n - 1;
        int n400 = divF(d0, 146097);
        int d1 = modF(d0, 146097);
        int n100 = divF(d1, 36524);
        int d2 = modF(d1, 36524);
        int n4 = divF(d2, 1461);
        int d3 = modF(d2, 1461);
        int n1 = divF(d3, 365);
        int y0 = 400 * n400 + 100 * n100 + 4 * n4 + n1;

        int y;

        if (n100 == 4 || n1 == 4) {
            y = y0;
        } else {
            y = y0 + 1;
        }

        int p = n - dateToFixed(1, intToMonth(1), y);
        boolean l = isLeapYear(y);
        int c;

        if (n < dateToFixed(1, intToMonth(3), y)) {
            c = 0;
        } else {
            if (l) {
                c = 1;
            } else {
                c = 2;
            }
        }

        int m = divF(12 * (p + c) + 373, 367);
        int d = n - dateToFixed(1, intToMonth(m), y) + 1;

        return new Date(d, m, y);
    }

    /**
     * @return le nombre de jour entre la date instancier et le premier janvier
     *         de l'an 1.
     * 
     */
    private int fixed() {
        return dateToFixed(day, month, year);
    }

    public static enum DayOfWeek {
        MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY;

    }

    public static enum Month {
        JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER;
    }

}
