package de.vorsorge.theo.util;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.MathContext;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

public class FabianUtil {

   private static final String STD_DIR_TEMP = "/FIXME/temp/";
   public static final String ENDING_TXT = "txt";
   public static final String ENDING_JSON = "json";
   public static final String ENDING_HTML = "html";
   public static final String ENDING_PDF = "pdf";
   public static final String ENDING_TIFF = "tiff";


   private FabianUtil() {}


   public static class ProgressHandler {

      private static final DecimalFormat TIME_DECIMAL_FORMAT = new DecimalFormat("00");
      protected static final int DEFAULT_LOG_DISTANCE = 50;
      protected static final int DEFAULT_TIME_LOG_DISTANCE = 15;

      private final ZonedDateTime startTime;
      private final BigDecimal logDistance;
      private final int timeLogDistance;
      private BigDecimal counter = BigDecimal.ZERO;
      private ZonedDateTime lastLoggedAt;


      public ProgressHandler() {
         this(DEFAULT_LOG_DISTANCE, DEFAULT_TIME_LOG_DISTANCE);
      }


      public ProgressHandler(int logDistance) {
         this(logDistance, DEFAULT_TIME_LOG_DISTANCE);
      }


      public ProgressHandler(int logDistance, int timeLogDistance) {
         startTime = ZonedDateTime.now();
         this.logDistance = validateBD(logDistance, "Log-Distanz");
         this.timeLogDistance = validateInt(timeLogDistance, "Zeitbasierte Log-Distanz");
      }


      protected BigDecimal validateBD(int bd, String name) {
         return BigDecimal.valueOf(validateInt(bd, name));
      }


      protected int validateInt(int n, String name) {
         if (n < 1) {
            throw new RuntimeException(name + " darf nicht 0 oder negativ sein.");
         }
         return n;
      }


      protected synchronized BigDecimal getCounter() {
         return counter;
      }


      protected synchronized int getTimeLogDistance() {
         return timeLogDistance;
      }


      public synchronized void tickAndLog() {
         tick();
         logIf();
      }


      protected synchronized void tick() {
         counter = counter.add(BigDecimal.ONE);
      }


      public synchronized void logAndTick() {
         logIf();
         tick();
      }


      public synchronized void logIf() {
         if (shouldLog()) {
            log();
         }
      }


      protected synchronized boolean shouldLog() {
         return counter.remainder(logDistance).signum() == 0 || isBored();
      }


      private synchronized boolean isBored() {
         return getTimeSinceLastLogged() >= timeLogDistance;
      }


      protected synchronized long getTimeSinceLastLogged() {
         ZonedDateTime time = lastLoggedAt == null ? startTime : lastLoggedAt;
         return time.until(ZonedDateTime.now(), ChronoUnit.SECONDS);
      }


      public synchronized void log() {
         Duration timePassed = getTimePassed();
         System.out.println(counter + " Stück erledigt. " + formatDuration(timePassed) + " vergangen.");
         updateLastLoggedAt();
      }


      protected synchronized void updateLastLoggedAt() {
         lastLoggedAt = ZonedDateTime.now();
      }


      protected synchronized Duration getTimePassed() {
         return Duration.ofSeconds(startTime.until(ZonedDateTime.now(), ChronoUnit.SECONDS));
      }


      protected String formatDuration(Duration dur) {
         return formatNumber(dur.toHours()) + ":" + formatNumber(dur.toMinutesPart()) + ":" + formatNumber(dur.toSecondsPart());
      }


      protected String formatNumber(Number n) {
         return TIME_DECIMAL_FORMAT.format(n == null ? 0 : n);
      }


      public synchronized void logFinal() {
         Duration timePassed = getTimePassed();
         System.out.println(counter + " Stück (100 %) erledigt. " + formatDuration(timePassed) + " vergangen.");
      }
   }


   public static class ProgressHandlerWithTotal extends ProgressHandler {

      private static final DecimalFormat PERCENTAGE_FORMAT = new DecimalFormat("00.00");

      private final BigDecimal total;


      public ProgressHandlerWithTotal(int total) {
         this(DEFAULT_LOG_DISTANCE, DEFAULT_TIME_LOG_DISTANCE, total);
      }


      public ProgressHandlerWithTotal(int logDistance, int total) {
         this(logDistance, DEFAULT_TIME_LOG_DISTANCE, total);
      }


      public ProgressHandlerWithTotal(int logDistance, int timeLogDistance, int total) {
         super(logDistance, timeLogDistance);
         this.total = validateBD(total, "Gesamt-Anzahl");
      }


      @Override
      protected synchronized boolean shouldLog() {
         return super.shouldLog() && getCounter().compareTo(total) <= 0;
      }


      @Override
      public synchronized void log() {
         BigDecimal counter = getCounter();
         BigDecimal percentage = BigDecimal.valueOf(100).divide(total, MathContext.DECIMAL64).multiply(counter);
         Duration timePassed = getTimePassed();

         String remainingTimePart = "";
         if (counter.signum() != 0) {
            BigDecimal secondsPassed = BigDecimal.valueOf(timePassed.getSeconds());
            BigDecimal totalSeconds = secondsPassed.divide(counter, MathContext.DECIMAL64).multiply(total);
            BigDecimal secondsToGo = totalSeconds.subtract(secondsPassed);
            Duration timeToGo = Duration.of(secondsToGo.longValue(), ChronoUnit.SECONDS);
            remainingTimePart = ", " + formatDuration(timeToGo) + " verbleibend";
         }

         String counterPrint = total.compareTo(BigDecimal.TEN) >= 0 ? formatNumber(counter) : counter.toString();
         System.out.println(counterPrint + " Stück von " + total + " (" + PERCENTAGE_FORMAT.format(percentage) + " %) erledigt. "
             + formatDuration(timePassed) + " vergangen" + remainingTimePart + ".");
         updateLastLoggedAt();
      }


      public void logContinuouslyTimeBased() {
         new Thread(this::logTimeBased).start();
      }


      private synchronized void logTimeBased() {
         while (getCounter().compareTo(total) != 0) {
            logIf();
            long timeSinceLastLogged = getTimeSinceLastLogged();
            try {
               TimeUnit.SECONDS.sleep(timeSinceLastLogged - getTimeLogDistance());
            } catch (InterruptedException ie) {
               Thread.currentThread().interrupt();
               return;
            }
         }
      }
   }


   public static void writeTestOutput(String filename, String ending, String content) {
      writeTestOutput(filename, ending, strToBytes(content));
   }


   public static byte[] strToBytes(String str) {
      return str.getBytes(StandardCharsets.UTF_8);
   }


   public static void writeTestOutput(String filename, String ending, byte[] content) {
      writeToFile(STD_DIR_TEMP + "testOutput/", filename, ending, content);
   }


   public static void writeDump(String ending, String content) {
      writeDump(ending, strToBytes(content));
   }


   public static void writeDump(String ending, byte[] content) {
      writeToFile(STD_DIR_TEMP, "dump", ending, content);
   }


   public static void writeDump(String filename, String ending, String content) {
      writeDump(filename, ending, strToBytes(content));
   }


   public static void writeDump(String filename, String ending, byte[] content) {
      writeToFile(STD_DIR_TEMP, filename, ending, content);
   }


   public static void writeToFile(String path, String filename, String ending, byte[] content) {
      try {
         Files.write(Paths.get(path + filename + "." + ending), content);
      } catch (IOException e) {
         throw new RuntimeException(e);
      }
   }
}