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.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.TemporalAccessor;
import java.util.Objects;

@UtilityClass
public class DateTimeParsers {

    public static final DateTimeFormatter ISO_ZONED = DateTimeFormatter.ISO_ZONED_DATE_TIME;

    public static final DateTimeFormatter ISO_OPTIONAL = new DateTimeFormatterBuilder()
        .parseCaseInsensitive()
        .append(DateTimeFormatter.ISO_LOCAL_DATE)
        .appendLiteral('T')
        .append(DateTimeFormatter.ISO_LOCAL_TIME)
        .optionalStart()  // zone and offset made optional
        .appendOffsetId()
        .optionalStart()
        .appendLiteral('[')
        .parseCaseSensitive()
        .appendZoneRegionId()
        .appendLiteral(']')
        .optionalEnd()
        .optionalEnd()
        .toFormatter();


    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDateTime parseDateTime(@Nullable String input) {
        return parseDateTime(ISO_ZONED, input);
    }

    @Contract("_,!null -> !null; _,null -> null")
    public static @Nullable ZonedDateTime parseDateTime(@NotNull DateTimeFormatter format, @Nullable String input) {
        return Objects.isNull(input) ? null : ZonedDateTime.parse(input, format);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDateTime parseUtcDateTime(@Nullable String input) {
        return parseDateTime(DateTimeUtils.ZONE_UTC, ISO_OPTIONAL, input);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDateTime parsePolishDateTime(@Nullable String input) {
        return parseDateTime(DateTimeUtils.ZONE_PL, ISO_OPTIONAL, input);
    }

    @ZoneExplicit
    @Contract("_,!null -> !null; _,null -> null")
    public static @Nullable ZonedDateTime parseUtcDateTime(@NotNull DateTimeFormatter format, @Nullable String input) {
        return parseDateTime(DateTimeUtils.ZONE_UTC, format, input);
    }

    @ZoneExplicit
    @Contract("_,!null -> !null; _,null -> null")
    public static @Nullable ZonedDateTime parsePolishDateTime(@NotNull DateTimeFormatter format, @Nullable String input) {
        return parseDateTime(DateTimeUtils.ZONE_PL, format, input);
    }

    @ZoneExplicit
    @Contract("_,_,!null -> !null; _,_,null -> null")
    public static @Nullable ZonedDateTime parseDateTime(@NotNull ZoneId zone, @NotNull DateTimeFormatter format, @Nullable String input) {
        if(Objects.isNull(input)) {
            return null;
        }
        TemporalAccessor output = format.parseBest(input, ZonedDateTime::from, LocalDateTime::from);
        if(output instanceof ZonedDateTime) {
            return DateTimeUtils.withZone((ZonedDateTime)output, zone);
        }
        if(output instanceof LocalDateTime) {
            return ((LocalDateTime)output).atZone(zone);
        }
        throw new IllegalArgumentException();
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDate parseUtcDate(@Nullable String input) {
        return parseDate(DateTimeUtils.ZONE_UTC, input);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedDate parsePolishDate(@Nullable String input) {
        return parseDate(DateTimeUtils.ZONE_PL, input);
    }

    @ZoneExplicit
    @Contract("_,_,!null -> !null; _,_,null -> null")
    public static @Nullable ZonedDate parseDate(@NotNull DateTimeFormatter formatter, @NotNull ZoneId zone, @Nullable String input) {
        if(Objects.isNull(input)) {
            return null;
        }
        return ZonedDate.of(zone, LocalDate.parse(input, formatter));
    }

    @ZoneExplicit
    @Contract("_,!null -> !null; _,null -> null")
    public static @Nullable ZonedDate parseDate(@NotNull ZoneId zone, @Nullable String input) {
        if(Objects.isNull(input)) {
            return null;
        }
        return ZonedDate.of(zone, LocalDate.parse(input, DateTimeFormatters.DATE));
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedTime parseUtcTime(@Nullable String input) {
        return parseTime(DateTimeUtils.ZONE_UTC, input);
    }

    @ZoneExplicit
    @Contract("!null -> !null; null -> null")
    public static @Nullable ZonedTime parsePolishTime(@Nullable String input) {
        return parseTime(DateTimeUtils.ZONE_PL, input);
    }

    @ZoneExplicit
    @Contract("_,_,!null -> !null; _,_,null -> null")
    public static @Nullable ZonedTime parseTime(@NotNull DateTimeFormatter formatter, @NotNull ZoneId zone, @Nullable String input) {
        if(Objects.isNull(input)) {
            return null;
        }
        return ZonedTime.of(zone, LocalTime.parse(input, formatter));
    }

    @ZoneExplicit
    @Contract("_,!null -> !null; _,null -> null")
    public static @Nullable ZonedTime parseTime(@NotNull ZoneId zone, @Nullable String input) {
        if(Objects.isNull(input)) {
            return null;
        }
        return ZonedTime.of(zone, LocalTime.parse(input, DateTimeFormatters.TIME));
    }

    @ZoneExplicit
    @Contract("null,null -> null; !null,_ -> !null; null,!null -> fail")
    public static @Nullable ZonedDateTime parseExplodedPolishDateTime(@Nullable String dateValue, @Nullable String timeValue) {
        return parseExplodedDateTime(DateTimeUtils.ZONE_PL, dateValue, timeValue);
    }

    @ZoneExplicit
    @Contract("null,null -> null; !null,_ -> !null; null,!null -> fail")
    public static @Nullable ZonedDateTime parseExplodedUtcDateTime(@Nullable String dateValue, @Nullable String timeValue) {
        return parseExplodedDateTime(DateTimeUtils.ZONE_UTC, dateValue, timeValue);
    }

    @ZoneExplicit
    @Contract("_,null,null -> null; _,!null,_ -> !null; _,null,!null -> fail")
    public static @Nullable ZonedDateTime parseExplodedDateTime(@NotNull ZoneId zone, @Nullable String dateValue, @Nullable String timeValue) {
        if(Objects.isNull(dateValue) && Objects.isNull(timeValue)) {
            return null;
        }
        if(Objects.isNull(dateValue)) {
            throw new IllegalArgumentException();
        }
        if(Objects.isNull(timeValue)) {
            return DateTimeParsers.parseDate(zone, dateValue).withZero();
        } else {
            return DateTimeParsers.parseDate(zone, dateValue).withTime(DateTimeParsers.parseTime(zone, timeValue));
        }
    }

}
