package com.airlock.iam.configeditor.shared.application;

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.google.common.annotations.VisibleForTesting;
import difflib.Chunk;
import difflib.Delta;
import difflib.DiffUtils;
import difflib.Patch;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

public final class ConfigurationDiffLogger {
   private static final String NEW_CONFIGURATION = "New configuration:";
   private static final String OLD_CONFIGURATION = "Old configuration:";
   private static final String SEPARATOR = "   .   ";
   private static final String SEPARATOR_FOR_SEPARATING_LINE = ".......";
   private static final ELogger LOG = ELoggerFactory.getLogger(ConfigurationDiffLogger.class);
   private final ELogger logger;

   public ConfigurationDiffLogger() {
      this.logger = LOG;
   }

   @VisibleForTesting
   ConfigurationDiffLogger(ELogger logger) {
      this.logger = logger;
   }

   public void logDifferences(File oldConfiguration, File newConfiguration) throws IOException {
      StringBuilder logMessage = new StringBuilder(
         "The configuration file "
            + oldConfiguration.getAbsolutePath()
            + " is going to be updated, the following changes exist between the active and new configuration:"
            + System.lineSeparator()
      );
      Patch<String> patch = DiffUtils.diff(
         FileUtils.readLines(oldConfiguration, StandardCharsets.UTF_8), FileUtils.readLines(newConfiguration, StandardCharsets.UTF_8)
      );
      int maxPaddingOldConfig = determineMaxPaddingOfOldConfig(patch);
      int maxPaddingNewConfig = determineMaxPaddingOfNewConfig(patch);
      appendTitle(logMessage, maxPaddingOldConfig);
      appendSeparatingLine(logMessage, maxPaddingOldConfig, maxPaddingNewConfig);
      logAllDifferences(logMessage, patch, maxPaddingOldConfig, maxPaddingNewConfig);
      this.logger.info(logMessage);
   }

   private static void logAllDifferences(StringBuilder logMessage, Patch<String> patch, int maxPaddingOldConfig, int maxPaddingNewConfig) {
      for (Delta<String> delta : patch.getDeltas()) {
         Chunk<String> oldConfigChunk = delta.getOriginal();
         Chunk<String> newConfigChunk = delta.getRevised();
         handleConcreteConfigDifference(logMessage, maxPaddingOldConfig, oldConfigChunk, newConfigChunk);
         appendSeparatingLine(logMessage, maxPaddingOldConfig, maxPaddingNewConfig);
      }
   }

   private static void handleConcreteConfigDifference(
      StringBuilder logMessage, int maxPaddingOldConfig, Chunk<String> oldConfigChunk, Chunk<String> newConfigChunk
   ) {
      int lineNumberPaddingOldConfig = getRequiredLineNumberPadding(oldConfigChunk);
      int lineNumberPaddingNewConfig = getRequiredLineNumberPadding(newConfigChunk);
      int lineNumberInOldConfig = oldConfigChunk.getPosition();
      int lineNumberInNewConfig = newConfigChunk.getPosition();
      Iterator<String> oldConfigIterator = oldConfigChunk.getLines().iterator();
      Iterator<String> newConfigIterator = newConfigChunk.getLines().iterator();

      while (oldConfigIterator.hasNext() && newConfigIterator.hasNext()) {
         lineNumberInOldConfig++;
         lineNumberInNewConfig++;
         String oldLine = getPaddedLineNumber(lineNumberPaddingOldConfig, lineNumberInOldConfig) + oldConfigIterator.next();
         String newLine = getPaddedLineNumber(lineNumberPaddingNewConfig, lineNumberInNewConfig) + newConfigIterator.next();
         appendDifferenceLine(logMessage, maxPaddingOldConfig, oldLine, newLine);
      }

      if (oldConfigIterator.hasNext()) {
         while (oldConfigIterator.hasNext()) {
            String oldLine = getPaddedLineNumber(lineNumberPaddingOldConfig, ++lineNumberInOldConfig) + oldConfigIterator.next();
            appendDifferenceLine(logMessage, maxPaddingOldConfig, oldLine, "");
         }
      }

      if (newConfigIterator.hasNext()) {
         while (newConfigIterator.hasNext()) {
            String newLine = getPaddedLineNumber(lineNumberPaddingNewConfig, ++lineNumberInNewConfig) + newConfigIterator.next();
            appendDifferenceLine(logMessage, maxPaddingOldConfig, "", newLine);
         }
      }
   }

   private static String getPaddedLineNumber(int lineNumberPadding, int lineNumber) {
      return String.format(Integer.toString(lineNumber), "%" + lineNumberPadding + "d") + ": ";
   }

   private static int getRequiredLineNumberPadding(Chunk<String> chunk) {
      int highestLineNumber = chunk.getPosition() + chunk.getLines().size();
      return Integer.toString(highestLineNumber).length();
   }

   private static void appendDifferenceLine(StringBuilder logMessage, int maxPaddingOldConfig, String oldLine, String newLine) {
      logMessage.append(System.lineSeparator());
      logMessage.append(StringUtils.rightPad(oldLine, maxPaddingOldConfig));
      logMessage.append("   .   ");
      logMessage.append(newLine);
   }

   private static int determineMaxPaddingOfOldConfig(Patch<String> patch) {
      int maxPadding = "Old configuration:".length();

      for (Delta<String> delta : patch.getDeltas()) {
         maxPadding = updateMaxPaddingIfNecessary(maxPadding, delta.getOriginal());
      }

      return maxPadding;
   }

   private static int determineMaxPaddingOfNewConfig(Patch<String> patch) {
      int maxPadding = "New configuration:".length();

      for (Delta<String> delta : patch.getDeltas()) {
         maxPadding = updateMaxPaddingIfNecessary(maxPadding, delta.getRevised());
      }

      return maxPadding;
   }

   private static int updateMaxPaddingIfNecessary(int maxPadding, Chunk<String> chunk) {
      List<String> lines = chunk.getLines();
      int position = chunk.getPosition();

      for (String line : lines) {
         int length = line.length() + Integer.toString(position).length() + 2;
         if (length > maxPadding) {
            maxPadding = length;
         }

         position++;
      }

      return maxPadding;
   }

   private static void appendTitle(StringBuilder logMessage, int maxPadding) {
      logMessage.append(StringUtils.rightPad("Old configuration:", maxPadding));
      logMessage.append("   .   ");
      logMessage.append("New configuration:");
   }

   private static void appendSeparatingLine(StringBuilder logMessage, int maxPaddingOldConfig, int maxPaddingNewConfig) {
      logMessage.append(System.lineSeparator());
      logMessage.append(StringUtils.rightPad("", maxPaddingOldConfig, '.'));
      logMessage.append(".......");
      logMessage.append(StringUtils.rightPad("", maxPaddingNewConfig, '.'));
   }
}
