package net.ranides.assira.time2;

import lombok.experimental.UtilityClass;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.sql.Timestamp;
import java.time.*;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.Locale;
import java.util.Objects;

@UtilityClass
public class DateTimeUtils {

    public static final ZoneId ZONE_PL = ZoneId.of("Poland");
    public static final ZoneId ZONE_UTC = ZoneId.of("Etc/GMT0");

    public static Locale locale() {
        return Locale.getDefault();
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDateTime withZoneUtc(@Nullable ZonedDateTime input) {
        return withZone(input, ZONE_UTC);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDateTime withZonePolish(@Nullable ZonedDateTime input) {
        return withZone(input, ZONE_PL);
    }

    @ZoneExplicit
    @Contract("!null,_ -> !null; null,_ -> null")
    public static @Nullable ZonedDateTime withZone(@Nullable ZonedDateTime input, @NotNull ZoneId zone) {
        return Objects.isNull(input) ? null : input.withZoneSameInstant(zone);
    }

    @ZoneExplicit
    @ZoneHazard
    @Contract("!null,_ -> !null; null,_ -> null")
    public static @Nullable ZonedDate withZone(@Nullable ZonedDate input, @NotNull ZoneId zone) {
        return Objects.isNull(input) ? null : input.withZone(zone);
    }

    @ZoneExplicit
    @ZoneHazard
    @Contract("!null,_ -> !null; null,_ -> null")
    public static @Nullable ZonedTime withZone(@Nullable ZonedTime input, @NotNull ZoneId zone) {
        return Objects.isNull(input) ? null : input.withZone(zone);
    }

    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedTime asTime(@Nullable ZonedDateTime input) {
        return Objects.isNull(input) ? null : ZonedTime.of(input);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedTime asPolishTime(@Nullable ZonedDateTime input) {
        return asTime(withZonePolish(input));
    }

    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDate asDate(@Nullable ZonedDateTime input) {
        return Objects.isNull(input) ? null : ZonedDate.of(input);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDate asPolishDate(@Nullable ZonedDateTime input) {
        return asDate(withZonePolish(input));
    }

    @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
    public static @Nullable ZonedDateTime min(@Nullable ZonedDateTime date1, @Nullable ZonedDateTime date2) {
        if(Objects.isNull(date1)) {
            return date2;
        }
        if(Objects.isNull(date2)) {
            return date1;
        }
        return date1.isBefore(date2) ? date2 : date1;
    }

    @Contract("!null,_ -> !null; _,!null -> !null; null,null -> null")
    public static @Nullable ZonedDateTime max(@Nullable ZonedDateTime date1, @Nullable ZonedDateTime date2) {
        if(Objects.isNull(date1)) {
            return date2;
        }
        if(Objects.isNull(date2)) {
            return date1;
        }
        return date1.isAfter(date2) ? date1 : date2;
    }

    @Contract("!null,null -> false; null,!null -> false; null,null -> false")
    public static boolean isSameDate(@Nullable ZonedDateTime dateTime1, @Nullable ZonedDateTime dateTime2) {
        if(Objects.isNull(dateTime1) != Objects.isNull(dateTime2)) {
            return false;
        }
        if(Objects.isNull(dateTime1)) {
            return false;
        }
        return dateTime1.truncatedTo(ChronoUnit.DAYS).isEqual(dateTime2.truncatedTo(ChronoUnit.DAYS));
    }

    @ZoneHazard
    @Contract("!null,null -> false; null,!null -> false; null,null -> false")
    public static boolean isSameDate(@Nullable ZonedDate date1, @Nullable ZonedDate date2) {
        if(Objects.isNull(date1) != Objects.isNull(date2)) {
            return false;
        }
        if(Objects.isNull(date1)) {
            return false;
        }
        return date1.isEqual(date2);
    }

    public static @NotNull Duration diff(@NotNull ZonedDateTime begin, @NotNull ZonedDateTime end) {
        Objects.requireNonNull(begin);
        Objects.requireNonNull(end);
        if(begin.isAfter(end)) {
            return Duration.of(24, ChronoUnit.HOURS).minus(Duration.between(end, begin));
        } else {
            return Duration.between(begin, end);
        }
    }

    @ZoneHazard
    public static @NotNull Duration diff(@NotNull ZonedTime begin, @NotNull ZonedTime end) {
        Objects.requireNonNull(begin);
        Objects.requireNonNull(end);
        if(begin.isAfter(end)) {
            return Duration.of(24, ChronoUnit.HOURS).minus(end.until(begin));
        } else {
            return end.until(begin);
        }
    }

    @ZoneHazard
    public static @NotNull Period diff(@NotNull ZonedDate begin, @NotNull ZonedDate end) {
        Objects.requireNonNull(begin);
        Objects.requireNonNull(end);
        if(begin.isAfter(end)) {
            return begin.until(end);
        } else {
            return end.until(begin);
        }
    }

    @Contract("_, null,null -> true")
    public static boolean isBetween(@NotNull ZonedDateTime input, @Nullable ZonedDateTime begin, @Nullable ZonedDateTime end) {
        Objects.requireNonNull(input);
        return (Objects.isNull(begin) || !input.isBefore(begin)) && (Objects.isNull(end) || input.isBefore(end));
    }

    @Contract("_, null,null -> true")
    public static boolean isBetweenInclusive(@NotNull ZonedDateTime input, @Nullable ZonedDateTime begin, @Nullable ZonedDateTime end) {
        Objects.requireNonNull(input);
        return (Objects.isNull(begin) || !input.isBefore(begin)) && (Objects.isNull(end) || !input.isAfter(end));
    }

    @Contract("_, null,null -> true")
    public static boolean isBetween(@NotNull ZonedDateTime input, @Nullable ZonedDate begin, @Nullable ZonedDate end) {
        Objects.requireNonNull(input);
        return (Objects.isNull(begin) || !begin.isAfter(input)) && (Objects.isNull(end) || end.isAfter(input));
    }

    @Contract("_, null,null -> true")
    public static boolean isBetweenInclusive(@NotNull ZonedDateTime input, @Nullable ZonedDate begin, @Nullable ZonedDate end) {
        Objects.requireNonNull(input);
        return (Objects.isNull(begin) || !begin.isAfter(input)) && (Objects.isNull(end) || !end.isBefore(input));
    }

    @ZoneHazard
    public static ZonedDateTime asDateTime(@NotNull ZonedDate date, @NotNull ZonedTime time) {
        Objects.requireNonNull(date);
        Objects.requireNonNull(time);
        return date.withTime(time);
    }

    @ZoneExplicit
    @Deprecated
    @Contract("_,null -> null; _,!null -> !null")
    public static @Nullable ZonedDateTime asDateTime(@NotNull ZoneId zone, @Nullable Timestamp timestamp) {
        Objects.requireNonNull(zone);
        if(Objects.isNull(timestamp)) {
            return null;
        }
        return timestamp.toLocalDateTime().atZone(zone);
    }

    @Contract("null -> null; !null -> !null")
    public static @Nullable ZonedDateTime asDateTime(@Nullable Timestamp timestamp) {
        return asDateTime(ZoneId.systemDefault(), timestamp);
    }

    @Contract("null -> false")
    public static boolean hasMinTime(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && LocalTime.MIN.equals(datetime.toLocalTime());
    }

    @Contract("null -> false")
    public static boolean hasMaxTime(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && LocalTime.MAX.equals(datetime.toLocalTime());
    }

    @ZoneExplicit
    @Contract("null -> false")
    public static boolean hasPolishMinTime(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && LocalTime.MIN.equals(withZonePolish(datetime).toLocalTime());
    }

    @ZoneExplicit
    @Contract("null -> false")
    public static boolean hasPolishMaxTime(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && LocalTime.MAX.equals(withZonePolish(datetime).toLocalTime());
    }

    @Deprecated
    @Contract("null -> null; !null -> !null")
    public static @Nullable ZonedDateTime withLastDayOfMonth(@Nullable ZonedDateTime datetime) {
        return Objects.isNull(datetime) ? null : datetime.with(TemporalAdjusters.lastDayOfMonth());
    }

    @Deprecated
    @Contract("null -> null; !null -> !null")
    public static @Nullable ZonedDateTime withLastMinuteOfDay(@Nullable ZonedDateTime datetime) {
        return Objects.isNull(datetime) ? null : datetime.withHour(23).withMinute(59).withSecond(59);
    }

    @Contract("null -> false")
    public static boolean isSaturday(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && datetime.getDayOfWeek() == DayOfWeek.SATURDAY;
    }

    @Contract("null -> false")
    public static boolean isSunday(@Nullable ZonedDateTime datetime) {
        return Objects.nonNull(datetime) && datetime.getDayOfWeek() == DayOfWeek.SATURDAY;
    }

    @ZoneExplicit
    @Contract("null -> false")
    public static boolean isPolishSaturday(@Nullable ZonedDateTime datetime) {
        return isSaturday(withZonePolish(datetime));
    }

    @ZoneExplicit
    @Contract("null -> false")
    public static boolean isPolishSunday(@Nullable ZonedDateTime datetime) {
        return isSunday(withZonePolish(datetime));
    }

}
