/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package com.planimod.core;

import java.util.Calendar;
import java.util.Date;

/**
 * Utility class to manipulate calendar events object.
 * 
 * @author Patrik Dufresne
 * 
 */
public class TimeRanges {

    private static <T extends AbstractCalendarEvent> T clone(T event, boolean keepId) {
        AbstractCalendarEvent newEvent;
        if (event instanceof Task) {
            Task pe = (Task) event;
            newEvent = new Task();
            ((Task) newEvent).setEmployee(pe.getEmployee());
            ((Task) newEvent).setProductionEvent(pe.getProductionEvent());
            ((Task) newEvent).setPosition(pe.getPosition());
            ((Task) newEvent).setLocked(pe.getLocked());
        } else if (event instanceof Shift) {
            Shift se = (Shift) event;
            newEvent = new Shift();
            ((Shift) newEvent).setTeam(se.getTeam());
        } else if (event instanceof NonAvailability) {
            NonAvailability nae = (NonAvailability) event;
            newEvent = new NonAvailability();
            ((NonAvailability) newEvent).setEmployee(nae.getEmployee());
        } else {
            throw new IllegalArgumentException();
        }
        if (keepId) {
            newEvent.setId(event.getId());
        }
        newEvent.setStartDate(event.getStartDate());
        newEvent.setEndDate(event.getEndDate());
        newEvent.setSummary(event.getSummary());
        return (T) newEvent;
    }

    /**
     * Check if the first time range constraint or is equals to the second time range.
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if s1 <= s2 < e2 <= e1
     */
    public static boolean containsEquals(Calendar s1, Calendar e1, Calendar s2, Calendar e2) {
        if (s1.after(e1) || s2.after(e2)) {
            throw new IllegalArgumentException();
        }
        return s1.compareTo(s2) <= 0 && s2.before(e2) && e2.compareTo(e1) <= 0;
    }

    /**
     * Check if the first time range constraint or is equals to the second time range.
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if s1 <= s2 < e2 <= e1
     */
    public static boolean containsEquals(Date s1, Date e1, Date s2, Date e2) {
        return containsEquals(newCalendar(s1), newCalendar(e1), newCalendar(s2), newCalendar(e2));
    }

    /**
     * Check if the event <code>e1</code> contains or is equals to the time range specified by <code>start</code> and
     * <code>end</code>.
     * 
     * @param e1
     *            the event
     * @param start
     *            the start date
     * @param end
     *            the end date
     * @return True if e1.start <= start < end <= e2.end
     */
    public static boolean containsEquals(TimeRange e1, Date start, Date end) {
        return containsEquals(e1.getStartDate(), e1.getEndDate(), start, end);
    }

    public static boolean containsEquals(TimeRange e1, TimeRange e2) {
        return containsEquals(e1.getStartDate(), e1.getEndDate(), e2.getStartDate(), e2.getEndDate());
    }

    public static Date getDayEnd(Date date) {
        // TODO fix this by using Calendar to append a field value
        return new Date(getDayStart(date).getTime() + 1000 * 60 * 60 * 24);
    }

    public static Date getDayStart(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        return cal.getTime();
    }

    /**
     * Return the first days of the first week of the month. The first week of a month is define by
     * {@link Calendar#getMinimalDaysInFirstWeek()}.
     * 
     * @param date
     *            The original date.
     * 
     * @return
     */
    public static Date getFirstDayOfFirstWeekOfMonth(Date date, int firstDayOfWeek) {
        Calendar cal = Calendar.getInstance();
        cal.setFirstDayOfWeek(firstDayOfWeek);
        cal.setTime(date);
        cal.set(Calendar.WEEK_OF_MONTH, 1);
        cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    public static Date getHourEnd(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.add(Calendar.HOUR, 1);
        return cal.getTime();
    }

    /**
     * Return the date without minutes, seconds and milliseconds.
     * 
     * @param date
     * 
     * @return
     */
    public static Date getHourStart(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        return cal.getTime();
    }

    public static Date getLastDayOfLastWeekOfMonth(Date date, int firstDayOfWeek) {
        Calendar cal = Calendar.getInstance();
        cal.setFirstDayOfWeek(firstDayOfWeek);
        cal.setTime(date);
        cal.add(Calendar.MONTH, 1);
        cal.set(Calendar.WEEK_OF_MONTH, 1);
        cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    /**
     * Return the first date of the month using the specified date.
     * 
     * @param date
     */
    public static Date getMonthStart(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.DAY_OF_MONTH, 1);
        return cal.getTime();
    }

    /**
     * Compute the ending date of a week.
     * 
     * @param week
     *            the week
     * @return the ending date
     */
    public static Date getWeekEnd(Date week, int firstDayOfWeek) {

        if (week == null) {
            throw new NullPointerException();
        }

        Calendar cal = Calendar.getInstance();
        cal.setTime(week);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);

        cal.add(Calendar.WEEK_OF_YEAR, 1);

        return cal.getTime();
    }

    /**
     * This function is used to calculate the date value of a the time specified.
     * <p>
     * This is used if you want the date value of a time (6:00 AM) but for a specific week.
     * 
     * @param time
     *            the time value
     * @param week
     *            the week
     * @param firstDayOfWeek
     *            the first day of week reference
     * @return A date value
     */
    public static Date getWeekReflexion(Date time, Date week, int firstDayOfWeek) {

        Calendar weekCal = Calendar.getInstance();
        weekCal.setFirstDayOfWeek(firstDayOfWeek);
        weekCal.setTime(week);
        int year = weekCal.get(Calendar.YEAR);
        int weekOfYear = weekCal.get(Calendar.WEEK_OF_YEAR);

        Calendar timeCal = Calendar.getInstance();
        timeCal.setFirstDayOfWeek(firstDayOfWeek);
        timeCal.setTime(time);
        timeCal.set(Calendar.YEAR, year);
        timeCal.set(Calendar.WEEK_OF_YEAR, weekOfYear);
        return timeCal.getTime();
    }

    /**
     * Compute the starting date of a week.
     * 
     * @param week
     *            the week
     * @return the starting date.
     */
    public static Date getWeekStart(Date week, int firstDayOfWeek) {
        if (week == null) {
            throw new NullPointerException();
        }

        Calendar cal = Calendar.getInstance();
        cal.setTime(week);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.DAY_OF_WEEK, firstDayOfWeek);

        return cal.getTime();
    }

    /**
     * Check if the first time range intersect with the second time range
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if s2 < s1 < e2 < e1
     */
    public static boolean intersectAfter(Calendar s1, Calendar e1, Calendar s2, Calendar e2) {
        return s2.before(s1) && s1.before(e2) && e2.before(e1);
    }

    /**
     * Check if the first time range intersect with the second time range
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if s2 < s1 < e2 < e1
     */
    public static boolean intersectAfter(Date s1, Date e1, Date s2, Date e2) {
        return intersectAfter(newCalendar(s1), newCalendar(e1), newCalendar(s2), newCalendar(e2));
    }

    public static boolean intersectAfter(TimeRange event, Date start, Date end) {
        return intersectAfter(event.getStartDate(), event.getEndDate(), start, end);
    }

    public static boolean intersectAfter(TimeRange e1, TimeRange e2) {
        return intersectAfter(e1.getStartDate(), e1.getEndDate(), e2.getStartDate(), e2.getEndDate());
    }

    /**
     * Check if the first time range happen before with the first time range and intersect with it.
     * 
     * @param s1
     *            the first time range
     * @param e1
     *            the first time range
     * @param s2
     *            the second time range
     * @param e2
     *            the second time range
     * @return True if s1 < s2 < e1 < e2
     */

    public static boolean intersectBefore(Calendar s1, Calendar e1, Calendar s2, Calendar e2) {
        return s1.before(s2) && s2.before(e1) && e1.before(e2);
    }

    /**
     * Check if the first time range happen before with the first time range and intersect with it.
     * 
     * @param s1
     *            the first time range
     * @param e1
     *            the first time range
     * @param s2
     *            the second time range
     * @param e2
     *            the second time range
     * @return True if s1 < s2 < e1 < e2
     */
    public static boolean intersectBefore(Date s1, Date e1, Date s2, Date e2) {
        return intersectBefore(newCalendar(s1), newCalendar(e1), newCalendar(s2), newCalendar(e2));
    }

    public static boolean intersectBefore(TimeRange event, Date start, Date end) {
        return intersectBefore(event.getStartDate(), event.getEndDate(), start, end);
    }

    public static boolean intersectBefore(TimeRange e1, TimeRange e2) {
        return intersectBefore(e1.getStartDate(), e1.getEndDate(), e2.getStartDate(), e2.getEndDate());
    }

    /**
     * Check if the first time range intersect, contains or equals the second time range. Return true if and only if
     * <code>s2 < e1 And s1 < e2</code>
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if the time ranges are intersecting.
     */
    public static boolean intersectEquals(Calendar s1, Calendar e1, Calendar s2, Calendar e2) {
        if (s1.after(e1) || s2.after(e2)) {
            throw new IllegalArgumentException();
        }
        return s2.before(e1) && s1.before(e2);
    }

    /**
     * Check if the first time range intersect, contains or equals the second time range. Return true if and only if
     * <code>s2 < e1 And s1 < e2</code>
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if the time ranges are intersecting.
     */
    public static boolean intersectEquals(Date s1, Date e1, Date s2, Date e2) {
        return intersectEquals(newCalendar(s1), newCalendar(e1), newCalendar(s2), newCalendar(e2));
    }

    /**
     * Check if the event intersect, constains, or equals the time range. Return true if and only if
     * <code>start < event.end And event.start < end</code>
     * 
     * @param event
     * @param start
     * @param end
     * @return
     */
    public static boolean intersectEquals(TimeRange event, Date start, Date end) {
        return intersectEquals(event.getStartDate(), event.getEndDate(), start, end);
    }

    /**
     * Check if the first events intersect, contains, or equals the second time range. Return true if and only if
     * <code>e2.start < e1.end And e1.start < e2.end</code>
     * 
     * @param e1
     * @param e2
     * @return
     */
    public static boolean intersectEquals(TimeRange e1, TimeRange e2) {
        return intersectEquals(e1.getStartDate(), e1.getEndDate(), e2.getStartDate(), e2.getEndDate());
    }

    /**
     * Check if two time range are intersecting. Two time range are intersecting if and only if
     * <code>s1 < s2 < e1 < e2 OR s2 < s1 < e2 < e1</code>
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if the time ranges are intersecting.
     */
    public static boolean intersects(Calendar s1, Calendar e1, Calendar s2, Calendar e2) {
        if (s1.after(e1) || s2.after(e2)) {
            throw new IllegalArgumentException();
        }
        return (s1.before(s2) && s2.before(e1) && e1.before(e2)) || (s2.before(s1) && s1.before(e2) && e2.before(e1));
    }

    /**
     * Check if two time range are intersecting. Two time range are intersecting if and only if
     * <code>s1 < s2 < e1 < e2 OR s2 < s1 < e2 < e1</code>
     * 
     * @param s1
     * @param e1
     * @param s2
     * @param e2
     * @return True if the time ranges are intersecting.
     */
    public static boolean intersects(Date s1, Date e1, Date s2, Date e2) {
        return intersects(newCalendar(s1), newCalendar(e1), newCalendar(s2), newCalendar(e2));
    }

    /**
     * Check if the given calendar event intersect with the the start and end date.
     * 
     * @param event
     * @param start
     * @param end
     * @return
     */
    public static boolean intersects(TimeRange event, Date start, Date end) {
        return intersects(event.getStartDate(), event.getEndDate(), start, end);
    }

    public static boolean intersects(TimeRange e1, TimeRange e2) {
        return intersects(e1.getStartDate(), e1.getEndDate(), e2.getStartDate(), e2.getEndDate());
    }

    /**
     * Create a new Calendar instance for the given date.
     * 
     * @param s1
     * @return
     */
    private static Calendar newCalendar(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        return cal;
    }

    /**
     * Compute the previous week for a given time range.
     * 
     * @param firstDayOfWeek
     * 
     * @return
     */
    public static Date previousWeek(Date date, int firstDayOfWeek) {
        // Compute the start of the week
        return getWeekStart(new Date(getWeekStart(date, firstDayOfWeek).getTime() - 1), firstDayOfWeek);
    }

    /**
     * Compute the previous week for a given time range.
     * 
     * @param firstDayOfWeek
     * 
     * @return
     */
    public static TimeRange previousWeek(TimeRange range, int firstDayOfWeek) {
        // The start of the given time range should be the end of the previous
        // week.
        Date end = getWeekStart(range.getStartDate(), firstDayOfWeek);
        // Compute the start of the week
        Date start = getWeekStart(new Date(end.getTime() - 1), firstDayOfWeek);
        return new ConcreteTimeRange(start, end);
    }

    /**
     * Round down the date.
     * 
     * @param date
     * @return return a date where second and millisecond are set to 0.
     */
    public static Date removeSecond(Date date) {
        Calendar cal = Calendar.getInstance();
        cal.setTime(date);
        cal.set(Calendar.MILLISECOND, 0);
        cal.set(Calendar.SECOND, 0);
        return cal.getTime();
    }

    private TimeRanges() {
        // Private constructor for utility class.
    }
}
