package net.ranides.assira.time;

import lombok.experimental.UtilityClass;
import net.ranides.assira.text.StrBuilder;

import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility class built on top of {@link SimpleDateFormat}.
 */
@UtilityClass
public class TimeFormat {

    private static final String DATE = "yyyy-MM-dd";

    private static final String TIME = "HH:mm:ss";

    private static final String DATE_TIME = "yyyy-MM-dd HH:mm:ss";

    private static final Pattern TIME_TO_MS =
            Pattern.compile("(?:([0-9.]+)\\s*h)?\\s*(?:([0-9.]+)\\s*m)?\\s*(?:([0-9.]+)\\s*s)?\\s*(?:([0-9.]+)\\s*ms)?\\s*(?:([0-9.]+)\\s*[uµ]s)?\\s*(?:([0-9.]+)\\s*ns)?");

    /**
     * Converts timestamp to ISO date format (yyyy-MM-dd)
     *
     * @param time time
     * @return String
     */
    public static String asDate(Date time) {
        return asText(DATE, time);
    }

    /**
     * Converts timestamp to ISO time format (HH:mm:ss)
     *
     * @param time time
     * @return String
     */
    public static String asTime(Date time) {
        return asText(TIME, time);
    }

    /**
     * Converts timestamp to ISO datetime format (yyyy-MM-dd HH:mm:ss)
     *
     * @param time time
     * @return String
     */
    public static String asDateTime(Date time) {
        return asText(DATE_TIME, time);
    }

    /**
     * Converts timestamp using default locale and format string supported by {@link SimpleDateFormat}
     *
     * @param pattern pattern
     * @param time time
     * @return String
     */
    private static String asText(String pattern, Date time) {
        return new SimpleDateFormat(pattern).format(time);
    }

    /**
     * Converts text to Duration type.
     *
     * Supported format of text is: #h #m #s #ms #us #µs #ns
     *
     * @param text text
     * @return Duration
     */
    public static Duration asDuration(String text) {
        Matcher hit = TIME_TO_MS.matcher(text.replace(',', '.'));
        if (hit.matches()) {
            double h = asNumber(hit.group(1));
            double m = asNumber(hit.group(2));
            double s = asNumber(hit.group(3));
            double ms = asNumber(hit.group(4));
            double us = asNumber(hit.group(5));
            double ns = asNumber(hit.group(6));
            return Duration
                    .ofSeconds((long) (3600.0 * h + 60 * m))
                    .plusMillis((long) (1000.0 * s))
                    .plusNanos((long) (1_000_000.0 * ms + 1_000.0 * us + ns));
        }
        return Duration.ZERO;
    }

    /**
     * Converts Duration to text.
     *
     * Format of returned text is: #h #m #s #ms
     *
     * @param duration duration
     * @return String
     */
    public static String asText(Duration duration) {
        return asText(duration, ChronoUnit.MILLIS);
    }

    /**
     * Converts Duration to text.
     * Precision defines smallest unit included in text.
     * It does not truncate skipped components but use then to round visible part.
     *
     * Format of returned text is: #h #m #s #ms #µs #ns
     *
     * Please note that we use unicode character for microseconds.
     *
     * @param duration duration
     * @param precision precision
     *
     * @return String
     */
    public static String asText(Duration duration, TemporalUnit precision) {
        long ts = duration.getSeconds();
        long tn = duration.getNano();

        long h = ts / 3600;
        long m = (ts % 3600) / 60;
        long s = (ts % 60);
        long ms = tn / 1_000_000;
        long us = (tn % 1_000_000) / 1_000;
        long ns = tn % 1_000;

        if(!enabled(ChronoUnit.NANOS, precision) && ns>=500) {
            us += 1;
        }
        if(!enabled(ChronoUnit.MICROS, precision) && us>=500) {
            ms += 1;
        }
        if(!enabled(ChronoUnit.MILLIS, precision) && ms>=500) {
            s += 1;
        }
        if(!enabled(ChronoUnit.SECONDS, precision) && s>=30) {
            m += 1;
        }
        if(!enabled(ChronoUnit.MINUTES, precision) && m>=30) {
            h += 1;
        }

        StrBuilder sb = new StrBuilder();
        sb.open("", "", " ");

        if (enabled(ChronoUnit.HOURS, precision) && h!=0) {
            sb.item().append(h).append("h");
        }
        if (enabled(ChronoUnit.MINUTES, precision) && m!=0) {
            sb.item().append(m).append("m");
        }
        if (enabled(ChronoUnit.SECONDS, precision) && s!=0) {
            sb.item().append(s).append("s");
        }
        if (enabled(ChronoUnit.MILLIS, precision) && ms!=0) {
            sb.item().append(ms).append("ms");
        }
        if (enabled(ChronoUnit.MICROS, precision) && us!=0) {
            sb.item().append(us).append("µs");
        }
        if (enabled(ChronoUnit.NANOS, precision) && ns!=0) {
            sb.item().append(ns).append("ns");
        }

        return sb.toString();
    }

    private static boolean enabled(TemporalUnit a, TemporalUnit b) {
        return a.getDuration().compareTo(b.getDuration()) >= 0;
    }

    private static double asNumber(String value) {
        return value == null ? 0 : Double.parseDouble(value);
    }
}
