package cz.senslog.analytics.utils;

import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.Arrays;
import java.util.function.BiConsumer;

public final class DateTrunc {

    public enum Option {
        SECOND  (1),
        MINUTE  (60),
        HOUR    (3_600),
        DAY     (86_400),
        WEEK    (604_800),
        MONTH   (2_629_743), // 2 629 743.83
        YEAR    (31_556_926)

        ;
        private final int sec;
        Option(int sec) {
            this.sec = sec;
        }
        public final int getSeconds() { return sec; }
    }

    private DateTrunc() {}

    public static OffsetDateTime trunc(OffsetDateTime dateTime, int period) {
        if (dateTime == null) { return null; }
        return OffsetDateTime.of(trunc(dateTime.toLocalDateTime(), period), dateTime.getOffset());
    }

    public static OffsetDateTime trunc(OffsetDateTime dateTime, Option option) {
        if (dateTime == null) { return null; }
        return OffsetDateTime.of(trunc(dateTime.toLocalDateTime(), option), dateTime.getOffset());
    }

    public static LocalDateTime trunc(LocalDateTime dateTime, int period) {
        return trunc(dateTime, PeriodUtils.disassemble(period));
    }

    public static LocalDateTime trunc(LocalDateTime dateTime, Option option) {
        return trunc(dateTime, PeriodUtils.disassemble(option));
    }

    private static LocalDateTime trunc(LocalDateTime dateTime, int[] optionComponents) {
        if (optionComponents == null || optionComponents.length != Option.values().length) {
            return dateTime;
        }

        LocalDateTime result = dateTime.withNano(0);
        for (Option option : Option.values()) {
            int value = optionComponents[option.ordinal()];
            if (value == -1) { continue; }
            switch (option) {
                case YEAR   ->  result = result.withYear(value == 0 ? 0 : result.getYear() - (result.getYear() % value));
                case MONTH  ->  result = result.withMonth(value == 0 ? 1 : result.getMonthValue() - (result.getMonthValue() % value));
                case WEEK   ->  { /* TODO: implement */}
                case DAY    ->  result = result.withDayOfMonth(value == 0 ? 1 : result.getDayOfMonth() - (result.getDayOfMonth() % value));
                case HOUR   ->  result = result.withHour(value == 0 ? 0 : result.getHour() - (result.getHour() % value));
                case MINUTE ->  result = result.withMinute(value == 0 ? 0 : result.getMinute() - (result.getMinute() % value));
                case SECOND ->  result = result.withSecond(value == 0 ? 0 : result.getSecond() - (result.getSecond() % value));
            }
        }
        return result;
    }

    private static class PeriodUtils {

        private static int[] disassemble(Option option) {
            int [] cmp = new int[Option.values().length];
            Arrays.fill(cmp, -1);
            if (option == null) {
                return cmp;
            }
            Arrays.fill(cmp, 0, option.ordinal(), 0);
            cmp[option.ordinal()] = 1;
            return cmp;
        }


        private static int[] disassemble(int period) {
            int [] cmp = new int[Option.values().length];
            BiConsumer<Option, Double> setValue = (o, v) -> cmp[o.ordinal()] = v.intValue() == 0 ? -1 : v.intValue();
            double periodSec = Math.min(period, Option.YEAR.sec);

            double years = periodSec / Option.YEAR.sec;
            setValue.accept(Option.YEAR, years);
            periodSec %= Option.YEAR.sec;

            double months =  round((years - (int)years) * 12.0, 5);
            setValue.accept(Option.MONTH, months);

            double weeksOfYears = periodSec / Option.WEEK.sec;
            setValue.accept(Option.WEEK, 0.0); // TODO

            double days = (periodSec % Option.WEEK.sec) / Option.DAY.sec;
            setValue.accept(Option.DAY, days);

            double hours = (days - (int)days) * 24.0;
            setValue.accept(Option.HOUR, hours);

            double minutes = (hours - (int)hours) * 60.0;
            setValue.accept(Option.MINUTE, minutes);

            double seconds = (minutes - (int)minutes) * 60.0;
            setValue.accept(Option.SECOND, seconds);

            for (int i = 0; i < cmp.length; i++) {
                if (cmp[i] > 0) { break; }
                cmp[i] = 0;
            }

            return cmp;
        }
    }

    public static double round(double value, int scale) {
        return Math.round(value * Math.pow(10, scale)) / Math.pow(10, scale);
    }
}