package de.duehl.basics.date;

import static de.duehl.basics.date.DateCalculations.*;

/**
 * Diese Klasse stellt ein Datum dar und bietet Methoden rund um dieses an.
 *
 * Es sollte besser ImmutualDate verwendet werden.
 *
 * @version 1.01     2015-11-10
 * @author Christian Dühl
 */

@Deprecated
public class MutualDate implements Comparable<MutualDate> {

    /** Tag des Monats von 1 bis 31. */
    private int day;

    /** Monat von 1 (Januar) bis 12 (Dezember). */
    private int month;

    /** Jahr als vierstellige Zahl (2012). */
    private int year;

    /**
     * Konstruktor.
     *
     * @param day
     *            Tag des Monats von 1 bis 31.
     * @param month
     *            Monat von 1 (Januar) bis 12 (Dezember).
     * @param year
     *            Jahr als vierstellige Zahl (2012).
     */
    public MutualDate(final int day, final int month, final int year) {
        this.day   = day;
        this.month = month;
        this.year  = year;
    }

//    /**
//     * Copy-Konstruktor.
//     *
//     * @param date
//     *            Datum als MySimpleDate-Objekt.
//     */
//    public MySimpleDate(final MySimpleDate date) {
//        copyFromOtherDate(date);
//    }

//    /**
//     * Kopiert das Datum von einem anderen Datum.
//     *
//     * @param date
//     *            Das andere Datum, von dem kopiert wird.
//     */
//    private void copyFromOtherDate(MySimpleDate date) {
//        this.day   = date.getDay();
//        this.month = date.getMonth();
//        this.year  = date.getYear();
//    }

    /**
     * Erstellt eine Kopie des Datums.
     *
     * @return Kopie des Datums.
     */
    public MutualDate copy() {
        return new MutualDate(day, month, year);
    }

    /**
     * Konstruktor
     *
     * @param date
     *            Datum in bestimmtem String-Format.
     */
    public MutualDate(final String date) {
        this(parseDate(date));
    }

    /**
     * Konstruktor, erzeugt Datumsobjekt mit dem heutigen Datum.
     */
    public MutualDate() {
        this(parseDate(new java.util.Date().toString()));
    }

    /**
     * Konstruktor, übernimmt das Datum aus einem ImmutalDate.
     *
     * @param immutualDate
     *            Unveränderliches Datum.
     */
    public MutualDate(final ImmutualDate immutualDate) {
        this.day   = immutualDate.getDay();
        this.month = immutualDate.getMonth();
        this.year  = immutualDate.getYear();
    }


    /* Getter und Setter */


    /** Getter für den Tag des Monats von 1 bis 31. */
    public int getDay() {
        return day;
    }

    /** Setter für den Tag des Monats von 1 bis 31. */
    public void setDay(final int day) {
        this.day = day;
    }

    /** Getter für den Monat von 1 (Januar) bis 12 (Dezember). */
    public int getMonth() {
        return month;
    }

    /** Setter für den Monat von 1 (Januar) bis 12 (Dezember). */
    public void setMonth(final int month) {
        this.month = month;
    }

    /** Getter für das Jahr als vierstellige Zahl (2012). */
    public int getYear() {
        return year;
    }

    /** Setter für das Jahr als vierstellige Zahl (2012). */
    public void setYear(final int year) {
        this.year = year;
    }

    /**
     * Testet, ob das Datum zulässig ist. Das Jahr muss zwischen 1000 und 9999
     * liegen, der Monat zwischen 1 und 12 und der Tag zwischen 1 und dem zum
     * Monat und Jahr passenden Anzahl an Tagen des Monats.
     *
     * @return Wahrheitswert: true genau dann, wenn das Datum nach den obigen
     *         Kriterien valide ist.
     */
    public boolean isValid() {
        /* Jahr ist zu klein oder zu groß: */
        if (year < 1000 || year > 9999) {
            return false;
        }

        /* Monat ist zu klein oder zu groß: */
        if (month < 1 || month > 12) {
            return false;
        }

        /* Tag ist ist zu klein oder zu groß: */
        if (day < 1 || day > monthDays(month, year)) {
            return false;
        }

        return true;
    }

    /**
     * Testet, ob das Datum zulässig ist. Das Jahr muss zwischen 0 und 9999
     * liegen, der Monat zwischen 1 und 12 und der Tag zwischen 1 und dem zum
     * Monat und Jahr passenden Anzahl an Tagen des Monats.
     *
     * @return Wahrheitswert: true genau dann, wenn das Datum nach den obigen
     *         Kriterien valide ist.
     */
    public boolean isValidWithYearZero() {
        /* Jahr ist zu klein oder zu groß: */
        if (year < 0 || year > 9999) {
            return false;
        }

        /* Monat ist zu klein oder zu groß: */
        if (month < 1 || month > 12) {
            return false;
        }

        /* Tag ist ist zu klein oder zu groß: */
        if (day < 1 || day > monthDays(month, year)) {
            return false;
        }

        return true;
    }

    /**
     * Testet, ob das Datum in einem Schaltjahr liegt.
     */
    public boolean isLeapYear() {
        return isLeapYear(year);
    }

    /**
     * Testet, ob das übergeben Jahr ein Schaltjahr ist.
     *
     * @param year
     *            Zu testendes Jahr.
     */
    private static boolean isLeapYear(final int year) {
        return DateCalculations.isLeapYear(year);
    }

    /**
     * Berechnet den Abstand zweier Datumswerte in Tagen.
     *
     * @param that
     *            Anderes Datum als MySimpleDate-Objekt, zu dem der Abstand
     *            berechnet werden soll.
     */
    public int calculateDayDifference(final MutualDate that) {
        int thisDay   = this.getDay();
        int thatDay   = that.getDay();
        int thisMonth = this.getMonth();
        int thatMonth = that.getMonth();
        int thisYear  = this.getYear();
        int thatYear  = that.getYear();

        int abstand   = 0;


        /*
         *  Tagesdifferenz:
         */
        abstand = thisDay - thatDay;
        thisDay = thatDay;

        /*
         *  Monatsdifferenz:
         */
        while (thisMonth > thatMonth) {
            abstand += monthDays(thatMonth, thatYear);
            ++thatMonth;
        }
        while (thisMonth < thatMonth) {
            abstand -= monthDays(thisMonth, thisYear);
            ++thisMonth;
        }

        /*
         *  Jahresdifferenz:
         */
        while (thisYear > thatYear) {
            if (isLeapYear(thatYear)   && thisMonth <  3 ||
                isLeapYear(thatYear+1) && thisMonth >= 3)
            {
                abstand += 366;
            }
            else {
                abstand += 365;
            }
            ++thatYear;
        }
        while (thisYear < thatYear) {
            if (isLeapYear(thisYear)   && thisMonth <  3 ||
                isLeapYear(thisYear+1) && thisMonth >= 3)
            {
                abstand -= 366;
            }
            else {
                abstand -= 365;
            }
            ++thisYear;
        }

        return -abstand;
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl
     * Tage zum Datum.
     *
     * @param delta
     *            Anzahl Tage, die addiert (oder bei negativer Anzahl
     *            subtrahiert) werden sollen.
     */
    public void addDays(final int delta) {
        day += delta;
        normalise();
    }

    /**
     * Addiert (oder subtrahiert bei negativen Zahlen) die angegebene Anzahl
     * Monate zum Datum.
     *
     * @param numberOfMonths
     *            Anzahl Monate, die addiert (oder bei negativer Anzahl
     *            subtrahiert) werden sollen.
     */
    public void addMonths(final int numberOfMonths) {
        int editedNumberOfMonths = numberOfMonths;
        boolean subtract = false;
        if (editedNumberOfMonths < 0) {
            subtract = true;
            editedNumberOfMonths = Math.abs(editedNumberOfMonths);
        }
        for (int i=0; i<editedNumberOfMonths; ++i) {
            if (subtract) {
                day -= monthDays(month, year);
            }
            else {
                day += monthDays(month, year);
            }
        }
        normalise();
    }

    /**
     * Normalisiert das Datum, zu kleine Tage oder zu große Tage werden auf
     * andere Monate oder Jahre umberechnet.
     */
    void normalise() {
        /* negatives Delta an Monaten: */
        while (month < 1) {
            month += 12;
            --year;
        }
        // 12.00.2011 -> 12.12.2010
        // 23.-1.2012 -> 23.11.2011

        /* positives Delta an Monaten: */
        while (month > 12) {
            month -= 12;
            ++year;
        }
        // 03.13.2011 -> 03.01.2012

        /* negatives Delta an Tagen: */
        while (day < 1) {
            --month;
            if (month == 0) {
                month = 12;
                --year;
            }
            day += monthDays(month, year);
        }
        // 00.05.2003 -> 30.04.2003
        // -3.05.2003 -> 27.04.2003

        /* positives Delta: */
        while (day > monthDays(month, year)) {
            day -= monthDays(month, year);
            ++month;
            if (month == 13) {
                month = 1;
                ++year;
            }
        }
        // 32.05.2003 -> 01.06.2003
    }

    /**
     * Berechnet den Wochentag des Datums.
     *
     * @return Wochentag
     */
    public Weekday dayOfTheWeek() {
        return DateCalculations.dayOfTheWeek(day, month, year);
    }

    /**
     * Ermittelt, ob das Datum ein Arbeitstag ist.
     *
     * @return Wahrheitswert
     */
    public boolean isWorkDay() {
        Weekday weekday = dayOfTheWeek();

        if (weekday == Weekday.SATURDAY || weekday == Weekday.SUNDAY) {
            return false;
        }

        if (isHoliday()) {
            return false;
        }

        return true;
    }

    /**
     * Ermittelt, ob das Datum ein Feiertag ist.
     *
     * @return Wahrheitswert
     */
    public boolean isHoliday() {
        /*
         * Fixe Feiertage:
         */
        if (
            day ==  1 && month ==  1 || // Neujahr
          //day ==  6 && month ==  1 || // Dreikönigstag
            day ==  1 && month ==  5 || // Tag der Arbeit
          //day == 19 && month ==  6 || // Fronleichnam ist nicht fest, sondern beweglich!
            day == 15 && month ==  8 || // Mariae Himmelfahrt
            day ==  3 && month == 10 || // Tag der deutschen Einheit
            day ==  1 && month == 11 || // Allerheiligen
            day == 24 && month == 12 || // Weihnachten
            day == 25 && month == 12 || // Erster Weihnachtstag
            day == 26 && month == 12 || // Zweiter Weihnachtstag
            day == 31 && month == 12    // Silvester
        ) {
            return true;
        }

        /*
         * Bewegliche Feiertage:
         */

        /* Ostersonntag: */
        MutualDate easter = new MutualDate(calculateEasterSunday(year));

        /* Ostermontag: */
        MutualDate easterMonday = easter.copy();
        easterMonday.addDays(1);

        /* Karfreitag: */
        MutualDate goodFriday = easter.copy();
        goodFriday.addDays(-2);

        /* Christi Himmelfahrt: */
        MutualDate ascensionDay  = easter.copy();
        ascensionDay.addDays(39);

        /* Pfingstmontag: */
        MutualDate whitMonday  = easter.copy();
        whitMonday.addDays(50);

        /* Fronleichnam: */
        MutualDate corpusChristi = easter.copy();
        corpusChristi.addDays(60);

        if (this.equals(easter)
                || this.equals(easterMonday)
                || this.equals(goodFriday)
                || this.equals(ascensionDay)
                || this.equals(whitMonday)
                || this.equals(corpusChristi)
                ) {
            return true;
        }

        return false;
    }

    /**
     * Ermittelt, ob das Datum vor dem angegebenen Datum liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false
     * zurückgegeben.
     */
    public boolean before(final MutualDate that) {
        if (this.year < that.year)
            return true;
        if (this.year > that.year)
            return false;

        if (this.month < that.month)
            return true;
        if (this.month > that.month)
            return false;

        if (this.day < that.day)
            return true;
        return false;
    }

    /**
     * Ermittelt, ob das Datum vor dem angegebenen Datum liegt oder diesem
     * gleicht.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false
     * zurückgegeben.
     */
    public boolean beforeOrEqual(final MutualDate that) {
        if (this.year < that.year)
            return true;
        if (this.year > that.year)
            return false;

        if (this.month < that.month)
            return true;
        if (this.month > that.month)
            return false;

        if (this.day <= that.day)
            return true;
        return false;
    }

    /**
     * Ermittelt, ob das Datum nach dem angegebenen Datum liegt.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false
     * zurückgegeben.
     */
    public boolean after(final MutualDate that) {
        return that.before(this);
    }

    /**
     * Ermittelt, ob das Datum nach dem angegebenen Datum liegt oder diesem
     * gleicht.
     *
     * @param that
     *            Vergleichsdatum
     * @return Wahrheitswert. Sind beide Datumswerte gleich, wird false
     *         zurückgegeben.
     */
    public boolean afterOrEqual(final MutualDate that) {
        return that.beforeOrEqual(this);
    }

    /**
     * Ermittelt, ob das Datum im Bereich der beiden angegebenen Datumswerte liegt.
     *
     * @param first
     *            Erster erlaubter Datumswert.
     * @param last
     *            Letzter erlaubter Datumswert.
     * @return Wahrheitswert.
     */
    public boolean between(final MutualDate first, final MutualDate last) {
        if (!first.beforeOrEqual(last)) {
            throw new RuntimeException("Der Übergebene Datumsbereich von "
                    + first + " bis " + last + " ist kein gültiger Bereich.");
        }
        return afterOrEqual(first) && beforeOrEqual(last);
    }


    /**
     * Berechnet den Abstand zu dem übergebenen Datum in Tagen. Der Abstand zu
     * einem Datum, das weiter in der Zukunft liegt als dieses, ist positiv,
     * der Abstand zu einem Datum in der Vergangenheit ist negativ.
     *
     * @param that
     *            Das Datum mit dem verglichen wird.
     * @return Differenz in Tagen.
     */
    public int calculateDayDistanceTo(final MutualDate that) {
        /* Vorarbeit: Bestimmung des früheren und späteren Datums */
        int earlyYear;
        int earlyMonth;
        int earlyDay;
        int lateYear;
        int lateMonth;
        int lateDay;
        boolean switchedDates;
        if (before(that)) {
            earlyYear  = this.getYear();
            earlyMonth = this.getMonth();
            earlyDay   = this.getDay();
            lateYear   = that.getYear();
            lateMonth  = that.getMonth();
            lateDay    = that.getDay();
            switchedDates = false;
        }
        else {
            earlyYear  = that.getYear();
            earlyMonth = that.getMonth();
            earlyDay   = that.getDay();
            lateYear   = this.getYear();
            lateMonth  = this.getMonth();
            lateDay    = this.getDay();
            switchedDates = true;
        }

        int distance = 0;

        /* 1. beide Datumswerte auf den 1. des Monats bringen: */
        distance -= (earlyDay - 1);
        earlyDay = 1;
        distance += (lateDay - 1);
        lateDay = 1;

        /* 2. beide Datumswerte auf Januar bringen: */
        while (earlyMonth > 1) {
            --earlyMonth;
            distance -= monthDays(earlyMonth, earlyYear);
        }
        while (lateMonth > 1) {
            --lateMonth;
            distance += monthDays(lateMonth, lateYear);
        }

        /* 3. früheres Jahr auf das spätere bringen: */
        while (earlyYear < lateYear) {
            if (isLeapYear(earlyYear)) {
                distance += 366;
            }
            else {
                distance += 365;
            }
            ++earlyYear;
        }

        /*
         * 4. Falls die Datumswerte am Anfang vertauscht wurden, distance mit
         * -1 multiplizieren:
         */
        if (switchedDates) {
            distance *= -1;
        }

        return distance;
    }

    /** Stringrepräsentation im Format 22.07.2013. */
    @Override
    public String toString() {
        return String.format("%02d.%02d.%04d", day, month, year);
    }

    /** Interne Stringrepräsentation im Format 2013/07/22. */
    public String toStringInternational() {
        return String.format("%04d/%02d/%02d", year, month, day);
    }

    /** Berechnung des Hash-Codes. */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + day;
        result = prime * result + month;
        result = prime * result + year;
        return result;
    }

    /** Bestimmt, wann zwei Objekte als gleich gelten sollen. */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        MutualDate other = (MutualDate) obj;
        if (day != other.day)
            return false;
        if (month != other.month)
            return false;
        if (year != other.year)
            return false;
        return true;
    }

    /**
     * Vergleicht ein Datum mit einem anderen Datum
     *
     * @param that
     * @return <0, wenn dieses Datum vor dem anderen Datum liegt, 0 wenn beide
     *         Datumswerte den gleichen Tag bezeichnen und >0, wenn dieses Datum
     *         nach dem anderen Datum liegt.
     */
    @Override
    public int compareTo(final MutualDate that) {
        return -calculateDayDistanceTo(that);
    }

//    public boolean isDayZero() {
//        return 0 == day && 0 == month && 0 == year;
//    }

    /*
    TODO:

    Achtung wir haben eine doppelte Methode...
        calculateDayDistanceTo()
    und
        calculateDayDifference()
    !

    */

}