package de.duehl.basics.datetime.date;

/*
 * Copyright 2018 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import de.duehl.basics.datetime.date.parse.DateParser;

/**
 * Diese Hilfsklasse hält statische Methoden zur Berechnung rund um Datumswerte
 * zur Verfügung.
 *
 * @version 1.01     2018-11-16
 * @author Christian Dühl
 */

public class DateCalculations {

    /** Privater Konstruktor - keine Objekte anlegen! */
    private DateCalculations() {}

    /**
     * Bildet aus einem Stringformat ein Datum.
     *
     * @param date
     *            Datum in bestimmtem String-Format.
     */
    public static ImmutualDate parseDate(String date) {
        DateParser parser = new DateParser(date);
        return parser.parse();
    }

    /**
     * Testet, ob das übergeben Jahr ein Schaltjahr ist.
     *
     * @param year
     *            Zu testendes Jahr.
     */
    public static boolean isLeapYear(int year) {
        return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
    }

    /**
     * Gibt die Anzahl der Tage des angegebenen Monats/Jahres zurück.
     *
     * @param month
     *            Monat.
     * @param year
     *            Jahr (für Schaltjahre nötig).
     * @return Anzahl Tag (28 bis 31)
     */
    public static int monthDays(ImmutualDate date) {
        return monthDays(date.getMonth(), date.getYear());
    }

    /**
     * Gibt die Anzahl der Tage des angegebenen Monats/Jahres zurück.
     *
     * @param month
     *            Monat.
     * @param year
     *            Jahr (für Schaltjahre nötig).
     * @return Anzahl Tag (28 bis 31)
     */
    public static int monthDays(int month, int year) {
        switch (month) {
            case  1:
            case  3:
            case  5:
            case  7:
            case  8:
            case 10:
            case 12:
                return 31;
            case  4:
            case  6:
            case  9:
            case 11:
                return 30;
            case 2:
                if (isLeapYear(year)) {
                    return 29;
                }
                else {
                    return 28;
                }
            default:
                throw new RuntimeException("Monat " + month + " nicht sinnvoll.");
        }
    }

    /**
     * Berechnet Ostern nach Gausschem Algorithmus (verbessert durch Lichtenberg).
     * @see http://de.wikipedia.org/wiki/Gau%C3%9Fsche_Osterformel
     *
     * Zur Bestimmung des Osterdatums für das Jahr X berechne man der Reihe nach
     * folgende Größen:
     *
     *     1. die Säkularzahl:                                       K(X) = X div 100
     *     2. die säkulare Mondschaltung:                            M(K) = 15 + (3K + 3) div 4 - (8K + 13) div 25
     *     3. die säkulare Sonnenschaltung:                          S(K) = 2 - (3K + 3) div 4
     *     4. den Mondparameter:                                     A(X) = X mod 19
     *     5. den Keim für den ersten Vollmond im Frühling:        D(A,M) = (19A + M) mod 30
     *     6. die kalendarische Korrekturgröße:                    R(D,A) = D div 29 + (D div 28 - D div 29) (A div 11)
     *     7. die Ostergrenze:                                    OG(D,R) = 21 + D - R
     *     8. den ersten Sonntag im März:                         SZ(X,S) = 7 - (X + X div 4 + S) mod 7
     *     9. die Entfernung des Ostersonntags von der
     *        Ostergrenze (Osterentfernung in Tagen):           OE(OG,SZ) = 7 - (OG - SZ) mod 7
     *    10. das Datum des Ostersonntags als Märzdatum
     *        (32. März = 1. April usw.):                              OS = OG + OE
     *
     * @param yearX
     *            Das Jahr X, zu dem das Datum des Ostersonntags berechnet
     *            werden soll.
     * @return Datumsobjekt mit dem Ostersonntag des angegebenen Jahres.
     */
    public static ImmutualDate calculateEasterSunday(int yearX) {
        /* 1. die Säkularzahl: K(X) = X div 100 */
        int k = yearX / 100;
        /* 2. die säkulare Mondschaltung: M(K) = 15 + (3K + 3) div 4 - (8K + 13) div 25 */
        int m = 15 + (3*k + 3) / 4 - (8*k + 13) / 25;
        /* 3. die säkulare Sonnenschaltung: S(K) = 2 - (3K + 3) div 4 */
        int s = 2 - (3*k + 3) / 4;
        /* 4. den Mondparameter: A(X) = X mod 19 */
        int a = yearX % 19;
        /* 5. den Keim für den ersten Vollmond im Frühling: D(A,M) = (19A + M) mod 30 */
        int d = (19 * a + m) % 30;
        /* 6. die kalendarische Korrekturgröße: R(D,A) = D div 29 + (D div 28 - D div 29) (A div 11) */
        /* Die nach Denis Roegel kürzer geschriebene Größe R(D,A) = (D + A div 11) div 29 bewirkt dasselbe. */
        int r = (d + a/11) / 29;
        /* 7. die Ostergrenze: OG(D,R) = 21 + D - R */
        int og = 21 + d - r;
        /* 8. den ersten Sonntag im März: SZ(X,S) = 7 - (X + X div 4 + S) mod 7 */
        int sz = 7 - (yearX + (yearX / 4) + s) % 7;
        /* 9. die Entfernung des Ostersonntags von der
         *    Ostergrenze (Osterentfernung in Tagen):
         *    OE(OG,SZ) = 7 - (OG - SZ) mod 7
         */
        int oe =  7 - (og - sz) % 7;
        /* 10. das Datum des Ostersonntags als Märzdatum (32. März = 1. April usw.): OS = OG + OE */
        int os = og + oe;

        ImmutualDate date = new ImmutualDate(os, 3, yearX);
        return date.normalise();
    }

    /**
     * Berechnet den Wochentag des Datums.
     * @see http://www.informatik.uni-ulm.de/pm/mitarbeiter/mark/day_of_week.html
     * @see http://craigricks.com/dayofweek.html
     * @see http://en.wikipedia.org/wiki/Weekday_determination
     *
     * @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).
     * @return Wochentag
     */
    public static Weekday dayOfTheWeek(int day, int month, int year) {
        if (year < 1592 || year > 2699) {
            throw new RuntimeException("Berechnung des Tages der Woche ist nur zwischen "
                    + "1595 und 2699 möglich.");
        }
        int century = year / 100;       // Jahrhundert (erste beide Ziffern des Jahres
        int yearInCentury = year % 100; // letzte beiden Ziffern des Jahres

        int centuryCode = 2 * (3 - (century % 4));
        /* 15 => 0, 16 => 6, 17 => 4, 18 => 2, 19 => 0, 20 => 6, 21 => 4,
           22 => 2, 23 => 0, 24 => 6, 25 => 4, 26 => 2 */
        int yearcode = yearInCentury + yearInCentury / 4;

        /* Schaltjahrkorrektur: */
        int leapYear = 0;
        if (isLeapYear(year)) {
            leapYear = 1;
        }

        int[] monthCodeArray = { 1-leapYear, 4-leapYear, 4, 0, 2, 5, 0, 3, 6, 1, 4, 6 };
        int monthCode = monthCodeArray[month-1];

        int dayCode = centuryCode + yearcode + monthCode + day;
        --dayCode; // damit Sonntag auf die 0 fällt.
        dayCode = dayCode % 7;

        return Weekday.getWeekdayByDayCodeStartingSunday(dayCode);
    }

}
