package cz.moneta.test.dsl.util;

import org.apache.commons.lang3.tuple.Pair;

import java.text.SimpleDateFormat;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.Random;
import java.util.Set;

public final class DateUtils {

    private static final Set<Pair<Month, Integer>> STATE_HOLIDAYS = new HashSet<Pair<Month, Integer>>() {{
        add(Pair.of(Month.JANUARY, 1));
        add(Pair.of(Month.MAY, 1));
        add(Pair.of(Month.MAY, 8));
        add(Pair.of(Month.JULY, 5));
        add(Pair.of(Month.JULY, 6));
        add(Pair.of(Month.SEPTEMBER, 28));
        add(Pair.of(Month.OCTOBER, 28));
        add(Pair.of(Month.NOVEMBER, 17));
        add(Pair.of(Month.DECEMBER, 24));
        add(Pair.of(Month.DECEMBER, 25));
        add(Pair.of(Month.DECEMBER, 26));
    }};

    private DateUtils() {
    }

    public static boolean isWorkingDay(LocalDate date) {
        return !isWeekend(date) && !isStateHoliday(date) && !isEaster(date);
    }

    private static boolean isWeekend(LocalDate date) {
        return date.getDayOfWeek() == DayOfWeek.SATURDAY || date.getDayOfWeek() == DayOfWeek.SUNDAY;
    }

    public static boolean isStateHoliday(LocalDate date) {
        return Optional.ofNullable(date)
                .map(d -> Pair.of(d.getMonth(), d.getDayOfMonth()))
                .filter(STATE_HOLIDAYS::contains)
                .isPresent();
    }

    private static boolean isEaster(LocalDate date) {
        // https://dzone.com/articles/algorithm-calculating-date
        int Y = date.getYear();
        int a = Y % 19;
        int b = Y / 100;
        int c = Y % 100;
        int d = b / 4;
        int e = b % 4;
        int f = (b + 8) / 25;
        int g = (b - f + 1) / 3;
        int h = (19 * a + b - d - g + 15) % 30;
        int i = c / 4;
        int k = c % 4;
        int L = (32 + 2 * e + 2 * i - h - k) % 7;
        int m = (a + 11 * h + 22 * L) / 451;
        int month = (h + L - 7 * m + 114) / 31;
        int day = ((h + L - 7 * m + 114) % 31) + 1;

        Set<LocalDate> easterDates = new HashSet<LocalDate>();
        easterDates.add(LocalDate.of(Y, month, day).plusDays(1));
        if (Y > 2015) easterDates.add(LocalDate.of(Y, month, day).minusDays(2));

        return easterDates.contains(date);
    }

    public static String getFormattedTodayDate(String dateFormat) {
        LocalDate localDate = LocalDate.now();
        return DateTimeFormatter.ofPattern(dateFormat).format(localDate);
    }

    public static String getFormattedTodayDateTime(String dateTimeFormat) {
        LocalDateTime localDateTime = LocalDateTime.now();
        return DateTimeFormatter.ofPattern(dateTimeFormat).format(localDateTime);
    }

    public static Date getTodayRawDate() {
        return new Date();
    }

    public static String getTodayRawDateForWso2() {
        return convertRawDateForWso2(getTodayRawDate());
    }

    public static String getDateWithFormat(Date date, String dateFormat) {
        return new SimpleDateFormat(dateFormat).format(date);
    }

    public static String getDateWithFormat(LocalDate date, String dateFormat) {
        return date.format(DateTimeFormatter.ofPattern(dateFormat));
    }

    public static String convertRawDateForWso2(Date date) {
        return getDateWithFormat(date, "yyyy-MM-dd");
    }

    public static String convertRawDateTimeForWso2(LocalDateTime date) {
        return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"));
    }

    public static String convertShortDateTimeForWso2(LocalDateTime date) {
        return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"));
    }

    public static String getFormattedTodayDateTimeForWso2() {
        return convertRawDateTimeForWso2(LocalDateTime.now());
    }

    public static LocalDate getNextWorkday() {
        return getNextWorkdayAfterDate(LocalDate.now());
    }

    public static LocalDate getNextWorkdayAfterDate(LocalDate startDate) {
        LocalDate futureWorkDay = startDate;
        do {
            futureWorkDay = futureWorkDay.plusDays(1);
        } while (!isWorkingDay(futureWorkDay));
        return futureWorkDay;
    }

    public static String getFormattedPreviousWorkday(String dateFormat) {
        LocalDate previousWorkday = LocalDate.now();
        do {
            previousWorkday = previousWorkday.minusDays(1);
        } while (!isWorkingDay(previousWorkday));

        return getDateWithFormat(previousWorkday, dateFormat);
    }

    public static String getTodayDateIncrementedByDays(int days, String dateFormat) {
        return getDateWithFormat(performDayIncrement(days, new Date()).getTime(), dateFormat);
    }

    public static Date getSpecificDayRaw(int day, int month, int year) {
        Calendar date = Calendar.getInstance();
        date.set(year, --month, day);
        return date.getTime();
    }

    public static Date getRawTodayDateIncrementedByDays(int days) {
        return performDayIncrement(days, new Date()).getTime();
    }

    private static Calendar performDayIncrement(int days, Date date) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.DATE, days);
        return calendar;
    }

    public static String getRandomDate(int yearFrom, int yearTo, String dateFormat) {
        Random random = new Random();
        int minDay = (int) LocalDate.of(yearFrom, 1, 1).toEpochDay();
        int maxDay = (int) LocalDate.of(yearTo, 12, 31).toEpochDay();
        long randomDay = minDay + random.nextInt(maxDay - minDay);
        LocalDate randomDate = LocalDate.ofEpochDay(randomDay);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormat);
        return randomDate.format(formatter);
    }

    public static String getLastDayOfYear(int year, String dateFormat) {
        return getDateWithFormat(LocalDate.of(year, 1, 1).with(TemporalAdjusters.lastDayOfYear()), dateFormat);
    }
}
