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 int day;
    private Month month; 
    private 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.       
     */  
    
    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 = dateToFixed(day, month, year)-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.
     */  
    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()
    {
        String s1;
        if (intMonth()<10)
        {
            s1="0"+intMonth();
        }
        else
        {
            s1=""+intMonth();
        }
        
        String s2;
        if (day<10)
        {
            s2="0"+day;
        }
        else
        {
            s2=""+day;
        }
        
        return year + "-" + s1 + "-" + s2;
    }
    
    /**
     * 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 (((ch.epfl.isochrone.timetable.Date) that).day()==this.day() && ((ch.epfl.isochrone.timetable.Date) that).intMonth()==this.intMonth() && ((ch.epfl.isochrone.timetable.Date) that).year()==this.year())
            {
                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[] mois = Month.values() ;
        
        return mois[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)
        {
            if (modF(year,100)==0)
            {
                if (modF(year,400)==0)
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                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;
    }
    
    
}
