/*
 * Copyright (c) 2017 Data and Web Science Group, University of Mannheim, Germany (http://dws.informatik.uni-mannheim.de/)
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

package de.uni_mannheim.informatik.dws.winter.preprocessing.datatypes;


import java.util.Calendar;

/**
 * Useful Calendar utilities.
 *
 * @author BalusC
 * http://balusc.blogspot.com/2007/09/calendarutil.html
 */
public final class CalendarUtil {

    // Init ---------------------------------------------------------------------------------------

    private CalendarUtil() {
        // Utility class, hide the constructor.
    }

    // Validators ---------------------------------------------------------------------------------

    /**
     * Checks whether the given day, month and year combination is a valid date or not.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @return True if the given day, month and year combination is a valid date.
     */
    public static boolean isValidDate(int year, int month, int day) {
        return isValidDate(year, month, day, 0, 0, 0);
    }

    /**
     * Checks whether the given hour, minute and second combination is a valid time or not.
     * @param hour The hour part of the time.
     * @param minute The minute part of the time.
     * @param second The second part of the time.
     * @return True if the given hour, minute and second combination is a valid time.
     */
    public static boolean isValidTime(int hour, int minute, int second) {
        return isValidDate(1, 1, 1, hour, minute, second);
    }

    /**
     * Checks whether the given day, month, year, hour, minute and second combination is a valid
     * date or not.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @param hour The hour part of the date.
     * @param minute The minute part of the date.
     * @param second The second part of the date.
     * @return True if the given day, month, year, hour, minute and second combination is a valid
     * date.
     */
    public static boolean isValidDate(
        int year, int month, int day, int hour, int minute, int second)
    {
        try {
            getValidCalendar(year, month, day, hour, minute, second);
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }

    /**
     * Validate the actual date of the given date elements and returns a calendar instance based on
     * the given date elements. The time is forced to 00:00:00.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @return A Calendar instance prefilled with the given date elements.
     * @throws IllegalArgumentException If the given date elements does not represent a valid date.
     */
    public static Calendar getValidCalendar(int year, int month, int day) {
        return getValidCalendar(year, month, day, 0, 0, 0);
    }

    /**
     * Validate the actual date of the given date elements and returns a calendar instance based on
     * the given date elements.
     * @param year The year part of the date.
     * @param month The month part of the date.
     * @param day The day part of the date.
     * @param hour The hour part of the date.
     * @param minute The minute part of the date.
     * @param second The second part of the date.
     * @return A Calendar instance prefilled with the given date elements.
     * @throws IllegalArgumentException If the given date elements does not represent a valid date.
     */
    public static Calendar getValidCalendar(
        int year, int month, int day, int hour, int minute, int second)
    {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.setLenient(false); // Don't automatically convert invalid date.
        calendar.set(year, month - 1, day, hour, minute, second);
        calendar.getTimeInMillis(); // Lazy update, throws IllegalArgumentException if invalid date.
        return calendar;
    }

    // Changers -----------------------------------------------------------------------------------

    /**
     * Add the given amount of years to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of years to.
     * @param years The amount of years to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addYears(Calendar calendar, int years) {
        calendar.add(Calendar.YEAR, years);
    }

    /**
     * Add the given amount of months to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of months to.
     * @param months The amount of months to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMonths(Calendar calendar, int months) {
        calendar.add(Calendar.MONTH, months);
    }

    /**
     * Add the given amount of days to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of days to.
     * @param days The amount of days to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addDays(Calendar calendar, int days) {
        calendar.add(Calendar.DATE, days);
    }

    /**
     * Add the given amount of hours to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of hours to.
     * @param hours The amount of hours to be added to the given calendar. Negative values are also
     * allowed, it will just go back in time.
     */
    public static void addHours(Calendar calendar, int hours) {
        calendar.add(Calendar.HOUR, hours);
    }

    /**
     * Add the given amount of minutes to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of minutes to.
     * @param minutes The amount of minutes to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMinutes(Calendar calendar, int minutes) {
        calendar.add(Calendar.MINUTE, minutes);
    }

    /**
     * Add the given amount of seconds to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of seconds to.
     * @param seconds The amount of seconds to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addSeconds(Calendar calendar, int seconds) {
        calendar.add(Calendar.SECOND, seconds);
    }

    /**
     * Add the given amount of millis to the given calendar. The changes are reflected in the given
     * calendar.
     * @param calendar The calendar to add the given amount of millis to.
     * @param millis The amount of millis to be added to the given calendar. Negative values are
     * also allowed, it will just go back in time.
     */
    public static void addMillis(Calendar calendar, int millis) {
        calendar.add(Calendar.MILLISECOND, millis);
    }

    // Comparators --------------------------------------------------------------------------------

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year.
     */
    public static boolean sameYear(Calendar one, Calendar two) {
        return one.get(Calendar.YEAR) == two.get(Calendar.YEAR);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year and month.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year and month.
     */
    public static boolean sameMonth(Calendar one, Calendar two) {
        return one.get(Calendar.MONTH) == two.get(Calendar.MONTH) && sameYear(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month and day.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month and day.
     */
    public static boolean sameDay(Calendar one, Calendar two) {
        return one.get(Calendar.DATE) == two.get(Calendar.DATE) && sameMonth(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day and
     * hour.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day and hour.
     */
    public static boolean sameHour(Calendar one, Calendar two) {
        return one.get(Calendar.HOUR_OF_DAY) == two.get(Calendar.HOUR_OF_DAY) && sameDay(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day,
     * hour and minute.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day, hour and
     * minute.
     */
    public static boolean sameMinute(Calendar one, Calendar two) {
        return one.get(Calendar.MINUTE) == two.get(Calendar.MINUTE) && sameHour(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same year, month, day,
     * hour, minute and second.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same year, month, day, hour, minute
     * and second.
     */
    public static boolean sameSecond(Calendar one, Calendar two) {
        return one.get(Calendar.SECOND) == two.get(Calendar.SECOND) && sameMinute(one, two);
    }

    /**
     * Returns <tt>true</tt> if the two given calendars are dated on the same time. The difference
     * from <tt>one.equals(two)</tt> is that this method does not respect the time zone.
     * @param one The one calendar.
     * @param two The other calendar.
     * @return True if the two given calendars are dated on the same time.
     */
    public static boolean sameTime(Calendar one, Calendar two) {
        return one.getTimeInMillis() == two.getTimeInMillis();
    }

    // Calculators --------------------------------------------------------------------------------

    /**
     * Retrieve the amount of elapsed years between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed years between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedYears(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.YEAR);
    }

    /**
     * Retrieve the amount of elapsed months between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed months between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedMonths(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.MONTH);
    }

    /**
     * Retrieve the amount of elapsed days between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed days between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedDays(Calendar before, Calendar after) {
        return elapsed(before, after, Calendar.DATE);
    }

    /**
     * Retrieve the amount of elapsed hours between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed hours between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedHours(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 3600000); // 1h = 60m = 3600s = 3600000ms
    }

    /**
     * Retrieve the amount of elapsed minutes between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed minutes between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedMinutes(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 60000); // 1m = 60s = 60000ms
    }

    /**
     * Retrieve the amount of elapsed seconds between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed seconds between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int elapsedSeconds(Calendar before, Calendar after) {
        return (int) elapsedMillis(before, after, 1000); // 1sec = 1000ms.
    }

    /**
     * Retrieve the amount of elapsed milliseconds between the two given calendars.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The amount of elapsed milliseconds between the two given calendars.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static long elapsedMillis(Calendar before, Calendar after) {
        return elapsedMillis(before, after, 1); // 1ms is apparently 1ms.
    }

    /**
     * Calculate the total of elapsed time from years up to seconds between the two given calendars.
     * It returns an int array with the elapsed years, months, days, hours, minutes and seconds
     * respectively.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @return The elapsed time between the two given calendars in years, months, days, hours,
     * minutes and seconds.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    public static int[] elapsedTime(Calendar before, Calendar after) {
        int[] elapsedTime = new int[6];
        Calendar clone = (Calendar) before.clone(); // Otherwise changes are been reflected.

        elapsedTime[0] = elapsedYears(clone, after);
        addYears(clone, elapsedTime[0]);

        elapsedTime[1] = elapsedMonths(clone, after);
        addMonths(clone, elapsedTime[1]);

        elapsedTime[2] = elapsedDays(clone, after);
        addDays(clone, elapsedTime[2]);

        elapsedTime[3] = elapsedHours(clone, after);
        addHours(clone, elapsedTime[3]);

        elapsedTime[4] = elapsedMinutes(clone, after);
        addMinutes(clone, elapsedTime[4]);

        elapsedTime[5] = elapsedSeconds(clone, after);

        return elapsedTime;
    }

    // Helpers ------------------------------------------------------------------------------------

    /**
     * Retrieve the amount of elapsed time between the two given calendars based on the given
     * calendar field as definied in the Calendar constants, e.g. <tt>Calendar.MONTH</tt>.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @param field The calendar field as definied in the Calendar constants.
     * @return The amount of elapsed time between the two given calendars based on the given
     * calendar field.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    private static int elapsed(Calendar before, Calendar after, int field) {
        checkBeforeAfter(before, after);
        Calendar clone = (Calendar) before.clone(); // Otherwise changes are been reflected.
        int elapsed = -1;
        while (!clone.after(after)) {
            clone.add(field, 1);
            elapsed++;
        }
        return elapsed;
    }

    /**
     * Retrieve the amount of elapsed milliseconds between the two given calendars and directly
     * divide the outcome by the given factor. E.g.: if the division factor is 1, then you will get
     * the elapsed milliseconds unchanged; if the division factor is 1000, then the elapsed
     * milliseconds will be divided by 1000, resulting in the amount of elapsed seconds.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @param factor The division factor which to divide the milliseconds with, expected to be at
     * least 1.
     * @return The amount of elapsed milliseconds between the two given calendars, divided by the
     * given factor.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar or
     * if the division factor is less than 1.
     */
    private static long elapsedMillis(Calendar before, Calendar after, int factor) {
        checkBeforeAfter(before, after);
        if (factor < 1) {
            throw new IllegalArgumentException(
                "Division factor '" + factor + "' should not be less than 1.");
        }
        return (after.getTimeInMillis() - before.getTimeInMillis()) / factor;
    }

    /**
     * Check if the first calendar is actually dated before the second calendar.
     * @param before The first calendar with expected date before the second calendar.
     * @param after The second calendar with expected date after the first calendar.
     * @throws IllegalArgumentException If the first calendar is dated after the second calendar.
     */
    private static void checkBeforeAfter(Calendar before, Calendar after) {
        if (before.after(after)) {
            throw new IllegalArgumentException(
                "The first calendar should be dated before the second calendar.");
        }
    }

}
