import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class LoadingBar {

   private static final String TIME_FORMAT = "HH:mm";
   private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_FORMAT);
   private static final Pattern TIME_PATTERN = Pattern.compile("(?>[01]\\d|2[0-4]):[0-5]\\d");
   private static final Pattern LUNCH_DURATION_PATTERN = Pattern.compile("\\d+");
   private static final Pattern OFFSET_PATTERN = Pattern.compile("[+-]\\d+");
   private static final DecimalFormat PERCENTAGE_FORMAT = new DecimalFormat("00.00");
   private static final int MIN_LUNCH_DURATION = 30;
   private static final LocalTime LATEST_LUNCH_TIME = LocalTime.of(13, 30);
   private static final int MINS_PER_HOUR = 60;
   private static final long DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH = 5L * MINS_PER_HOUR;
   private static final int MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH = 6 * MINS_PER_HOUR;
   private static final long MAX_NUMBER_WORK_MINS = 8L * MINS_PER_HOUR;
   private static final int LINE_LENGTH = 100;
   private static final MathContext MC_INTEGER = new MathContext(1, RoundingMode.HALF_EVEN);


   private enum DaySection {
      MITTAG("-m", "Mittag"),
      ZAPFENSTREICH("-z", "Zapfenstreich");

      private final String param;
      private final String description;


      DaySection(String param, String description) {
         this.param = param;
         this.description = description;
      }


      public static DaySection byParam(String param) {
         return Arrays.stream(DaySection.values()).filter((DaySection section) -> section.getParam().equals(param)).findFirst().orElse(null);
      }


      public String getParam() {
         return param;
      }


      public String getDescription() {
         return description;
      }
   }


   public static void main(String[] args) {
      if (args.length > 0 && Objects.equals(args[0], "--help")) {
         printHelp();
         return;
      }
      verifyMinimumNumberOfArgs(args);
      String nextArg = args[0];
      verifyTimeFormat(nextArg, "Erstes Argument");
      var startTime = LocalTime.parse(nextArg, TIME_FORMATTER);
      nextArg = args[1];
      var section = DaySection.byParam(nextArg);
      verifyDaySection(section, nextArg);
      if (section == DaySection.MITTAG) {
         handleMittagspause(args, startTime);
      } else {
         handleZapfenstreich(args, startTime);
      }
   }


   private static void handleMittagspause(String[] args, LocalTime startTime) {
      if (args.length == 2) {
         showLoadingBarMittagspause(startTime);
         return;
      }
      String nextArg = args[2];
      if (OFFSET_PATTERN.matcher(nextArg).matches()) {
         showLoadingBarMittagspause(startTime, Integer.parseInt(nextArg));
         return;
      }
      verifyTimeFormat(nextArg, "Argument nach " + DaySection.MITTAG.getParam());
      var maxMittagspause = LocalTime.parse(nextArg, TIME_FORMATTER);
      showLoadingBarMittagspause(startTime, maxMittagspause);
   }


   private static void handleZapfenstreich(String[] args, LocalTime startTime) {
      Integer lunchDuration = null;
      if (args.length == 2) {
         showLoadingBarZapfenstreich(startTime);
         return;
      }
      String nextArg = args[2];
      LocalTime maxZapfenstreich = null;
      int endTimeOffset = 0;
      if (TIME_PATTERN.matcher(nextArg).matches()) {
         maxZapfenstreich = LocalTime.parse(nextArg, TIME_FORMATTER);
      } else if (OFFSET_PATTERN.matcher(nextArg).matches()) {
         endTimeOffset = Integer.parseInt(nextArg);
      } else {
         verifyDurationFormat(nextArg, "Argument nach " + DaySection.ZAPFENSTREICH.getParam(), true); // FSFIXME erweitere Fehlermeldung
         lunchDuration = Integer.parseInt(nextArg);
      }
      if (args.length == 3) {
         if (maxZapfenstreich == null && endTimeOffset == 0) {
            showLoadingBarZapfenstreich(startTime, lunchDuration);
         } else if (maxZapfenstreich == null) {
            showLoadingBarZapfenstreich(startTime, lunchDuration, endTimeOffset);
         } else {
            showLoadingBarZapfenstreich(startTime, lunchDuration, maxZapfenstreich);
         }
         return;
      }
      nextArg = args[3];
      if (lunchDuration == null) {
         System.out.println("Letztes Argument darf nur auf Mittagspausendauer folgen.");
         System.exit(1);
      }
      if (maxZapfenstreich == null && !OFFSET_PATTERN.matcher(nextArg).matches()) {
         verifyTimeFormat(nextArg, "Letztes Argument nach " + DaySection.ZAPFENSTREICH.getParam() + " und Mittagspausendauer");
         maxZapfenstreich = LocalTime.parse(nextArg, TIME_FORMATTER);
         showLoadingBarZapfenstreich(startTime, lunchDuration, maxZapfenstreich);
         return;
      }
      verifyOffsetFormat(nextArg, "Letztes Argument nach " + DaySection.ZAPFENSTREICH.getParam() + " und Enduhrzeit");
      endTimeOffset = Integer.parseInt(nextArg);
      showLoadingBarZapfenstreich(startTime, lunchDuration, endTimeOffset);
   }


   private static void verifyMinimumNumberOfArgs(String[] args) {
      if (args.length >= 2) {
         return;
      }
      System.out.println("Mindestens 2 Argumente müssen gegeben sein.");
      printHelp();
      System.exit(1);
   }


   private static void verifyTimeFormat(String param, String errMsgPrefix) {
      verifyInputFormat(TIME_PATTERN, param, errMsgPrefix, true, false);
   }


   private static void verifyDurationFormat(String param, String errMsgPrefix, boolean timeInputPossible) {
      verifyInputFormat(LUNCH_DURATION_PATTERN, param, errMsgPrefix, false, timeInputPossible);
   }


   private static void verifyOffsetFormat(String param, String errMsgPrefix) {
      verifyInputFormat(OFFSET_PATTERN, param, errMsgPrefix, false, false);
   }


   private static void verifyInputFormat(Pattern pattern, String param, String errMsgPrefix, boolean timeInput, boolean timeInputPossible) {
      if (pattern.matcher(param).matches()) {
         return;
      } // FSFIXME fine tune message -> HH:mm, mm, -+mm
      var firstInputPart = timeInput ? "Uhrzeitformat (" + TIME_FORMAT + ")" : "Minutenanzahl (" + LUNCH_DURATION_PATTERN + ")";
      var possibleTimeInputPart = !timeInput && timeInputPossible ? " oder Uhrzeitformat (" + TIME_FORMAT + ")" : "";
      System.out.println(errMsgPrefix + " \"" + param + "\" muss " + firstInputPart + possibleTimeInputPart + " entsprechen.");
      System.exit(1);
   }


   private static void verifyDaySection(DaySection section, String param) {
      if (section != null) {
         return;
      }
      List<String> sectionDescs = Arrays.stream(DaySection.values()).map((DaySection ds) -> ds.getDescription() + " (" + ds.getParam() + ")")
          .collect(Collectors.toList());
      String sectionDescsJoined = String.join(" oder ", sectionDescs);
      System.out.println("Argument nach Startzeit \"" + param + "\" muss Angabe für " + sectionDescsJoined + " sein.");
      System.exit(1);
   }


   private static void printHelp() {
      System.out.println("Mögliche Argumente für LoadingBar:\n"
          + "Normalfall Vormittag (Mittagspause <= " + LATEST_LUNCH_TIME + ")\n"
          + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + "\n"
          + "Vormittag mit expliziter Mittagspause (<= " + LATEST_LUNCH_TIME + ")\n"
          + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + " " + TIME_FORMAT + "\n"
          + "Vormittag mit abweichender Minutenanzahl Arbeitszeit\n"
          + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + " -+mm\n"
          + "Normalfall Nachmittag (Mittagspause " + MIN_LUNCH_DURATION + " min)\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + "\n"
          + "Nachmittag mit expliziter Länge Mittagspause (Mittagspause unter " + MIN_LUNCH_DURATION + " min wird auf " + MIN_LUNCH_DURATION + " min korrigiert)\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " mm\n"
          + "Nachmittag mit explizitem Feierabend (Mittagspause je nach Minimum (s.u.))\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " " + TIME_FORMAT + "\n"
          + "Nachmittag mit abweichender Minutenanzahl Arbeitszeit\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " -+mm\n"
          + "Nachmittag mit explizitem Feierabend u. expliziter Länge Mittagspause (Mittagspause unter Minimum (s.u.) wird auf Minimum korrigiert)\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " mm " + TIME_FORMAT + "\n"
          + "Nachmittag mit explizitem Feierabend u. abweichender Minutenanzahl Arbeitszeit\n"
          + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " " + TIME_FORMAT + " -+mm\n\n"
          + "Mittagspause minimum in Minuten:\n"
          + " - bis " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min ("
          + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH / MINS_PER_HOUR + " std):  0\n"
          + " - bis " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min + "
          + MIN_LUNCH_DURATION + " min: Arbeitszeit - " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min\n"
          + " - ab  " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min + " + MIN_LUNCH_DURATION + " min: "
          + MIN_LUNCH_DURATION + " min\n"
      );
   }


   private static void showLoadingBarMittagspause(LocalTime startTime) {
      showLoadingBarMittagspause(startTime, null);
   }


   private static void showLoadingBarMittagspause(LocalTime startTime, int endTimeOffset) {
      LocalTime endTime = startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH + endTimeOffset);
      LocalTime trueEndTime = endTime.isAfter(LATEST_LUNCH_TIME) ? LATEST_LUNCH_TIME : endTime;
      showLoadingBar(startTime, trueEndTime);
   }


   private static void showLoadingBarMittagspause(LocalTime startTime, LocalTime manualEndTime) {
      LocalTime endTime = manualEndTime != null ? manualEndTime : startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH);
      LocalTime trueEndTime = endTime.isAfter(LATEST_LUNCH_TIME) ? LATEST_LUNCH_TIME : endTime;
      showLoadingBar(startTime, trueEndTime);
   }


   private static void showLoadingBarZapfenstreich(LocalTime startTime) {
      showLoadingBarZapfenstreich(startTime, -1, 0);
   }


   private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration) {
      showLoadingBarZapfenstreich(startTime, manualLunchDuration, 0);
   }


   private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration, int endTimeOffset) {
      long minLunchDuration = getMinLunchDuration(endTimeOffset);
      long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration);
      LocalTime trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration + endTimeOffset);
      realShowLoadingBarZapfenstreich(startTime, realLunchDuration, trueEndTime);
   }


   private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration, LocalTime manualEndTime) {
      LocalTime trueEndTime = manualEndTime;
      long minLunchDuration = getMinLunchDuration(startTime, trueEndTime);
      long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration);
      if (trueEndTime == null) {
         trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration);
      }
      realShowLoadingBarZapfenstreich(startTime, realLunchDuration, trueEndTime);
   }


   private static long getMinLunchDuration(int endTimeOffset) {
      if (endTimeOffset == 0) {
         return MIN_LUNCH_DURATION;
      }
      long totalDuration = MAX_NUMBER_WORK_MINS + endTimeOffset;
      long effectiveLunchDuration = totalDuration - MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH;
      return getMinLunchDuration(effectiveLunchDuration);
   }


   private static long getMinLunchDuration(LocalTime startTime, LocalTime endTime) {
      if (endTime == null) {
         return MIN_LUNCH_DURATION;
      }
      long totalDuration = startTime.until(endTime, ChronoUnit.MINUTES);
      int effectiveLunchDuration = ((int) totalDuration) - MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH;
      return getMinLunchDuration(effectiveLunchDuration);
   }


   private static long getMinLunchDuration(long effectiveLunchDuration) {
      if (effectiveLunchDuration < 0) {
         effectiveLunchDuration = 0;
      }
      return Math.min(effectiveLunchDuration, MIN_LUNCH_DURATION);
   }


   private static long getRealLunchDuration(Integer manualLunchDuration, long minLunchDuration) {
      return manualLunchDuration != null && manualLunchDuration >= minLunchDuration ? manualLunchDuration : minLunchDuration;
   }


   private static void realShowLoadingBarZapfenstreich(LocalTime startTime, long manualLunchDuration, LocalTime endTime) {
      if (manualLunchDuration > 0) {
         var totalWorkTime = LocalTime.MIDNIGHT.plusMinutes(startTime.until(endTime, ChronoUnit.MINUTES) - manualLunchDuration);
         System.out.print("Arbeitszeit: " + TIME_FORMATTER.format(totalWorkTime) + "; ");
      }
      showLoadingBar(startTime, endTime);
   }


   private static void showLoadingBar(LocalTime startTime, LocalTime endTime) {
      long initialMinutes = startTime.until(endTime, ChronoUnit.MINUTES);
      System.out.print(minutesToTimeString(initialMinutes) + " gesamt; Endzeit: " + TIME_FORMATTER.format(endTime) + "\n");
      long passedMinutes = startTime.until(LocalTime.now(), ChronoUnit.MINUTES);
      if (passedMinutes > initialMinutes) {
         passedMinutes = initialMinutes;
      } else if (passedMinutes < 0) {
         System.out.println(fillLoadingBar(initialMinutes, 0, false));
         return;
      }
      while (passedMinutes < initialMinutes) {
         System.out.print(fillLoadingBar(initialMinutes, passedMinutes, true));
         try {
            var now = LocalTime.now();
            var oneMinuteLater = now.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES);
            // +1 second to adjust for ignored milliseconds as it is better to switch between 00 and 01 as between 59 and 00
            TimeUnit.SECONDS.sleep(now.until(oneMinuteLater, ChronoUnit.SECONDS) + 1);
            // TimeUnit.SECONDS.sleep(1L); // DEBUG
         } catch (InterruptedException ie) {
            throw new RuntimeException(ie);
         }
         passedMinutes++;
      }
      System.out.println(fillLoadingBar(initialMinutes, passedMinutes, false));
   }


   private static String fillLoadingBar(long initialMinutes, long passedMinutes, boolean progressive) {
      BigDecimal wholePercentage = BigDecimal.valueOf(100)
         .multiply(BigDecimal.valueOf(passedMinutes) // kind of reverse dreisatz to avoid to have e.g. 99.9999 instead of 100 %
         .divide(BigDecimal.valueOf(initialMinutes), MathContext.DECIMAL64));
      long remainingMinutes = initialMinutes - passedMinutes;
      int numberOfEquals = wholePercentage.intValue();
      var sb = new StringBuilder("[");
      for (int i = 0; i < LINE_LENGTH; i++) {
         if (i < numberOfEquals) {
            sb.append("=");
         } else {
            sb.append("-");
         }
      }
      sb.append("] ").append(PERCENTAGE_FORMAT.format(wholePercentage)).append("% ")
          .append(minutesToTimeString(passedMinutes)).append(" - ").append(minutesToTimeString(remainingMinutes));
      if (progressive) {
         sb.append("\r");
      }
      return sb.toString();
   }


   private static String minutesToTimeString(long minutes) {
      var minutesBD = BigDecimal.valueOf(minutes);
      BigDecimal[] hoursAndMinutes = minutesBD.divideAndRemainder(BigDecimal.valueOf(MINS_PER_HOUR), MC_INTEGER);
      return LocalTime.of(hoursAndMinutes[0].intValue(), hoursAndMinutes[1].intValue()).format(TIME_FORMATTER);
   }
}