
package de.ivu.elect.util;

import static java.util.Arrays.stream;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.splitByWholeSeparator;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

// Bei BugFix/Verbesserung auch CaseAndDiacriticIgnoringComparator in de.ivu.bundeswahlleiter:bundeswahlleiter-meta anpassen!
public class CaseAndDiacriticIgnoringNormalizer {

  public static final CaseAndDiacriticIgnoringNormalizer DEFAULT_INSTANCE = new CaseAndDiacriticIgnoringNormalizer();

  private static final String COLLATOR_RULES_UMLAUTE_DIN_5007_VAR_1 = "" //
      + "& A , Ä" //
      + "& A , A\u0308" //
      + "& a , ä" //
      + "& a , a\u0308" //
      + "& O , Ö" //
      + "& O , O\u0308" //
      + "& o , ö" //
      + "& o , o\u0308" //
      + "& U , Ü" //
      + "& U , U\u0308" //
      + "& u , ü" //
      + "& u , u\u0308" //
  ;
  private static final String COLLATOR_RULES_UMLAUTE_DIN_5007_VAR_2 = "" //
      + "& Ae , Ä" //
      + "& Ae , A\u0308" //
      + "& ae , ä" //
      + "& ae , a\u0308" //
      + "& Oe , Ö" //
      + "& Oe , O\u0308" //
      + "& oe , ö" //
      + "& oe , o\u0308" //
      + "& Ue , Ü" //
      + "& Ue , U\u0308" //
      + "& ue , ü" //
      + "& ue , u\u0308" //
  ;

  private static final String COLLATOR_RULES_SHARP_S = "" // Identisch für DIN_5007_VAR_1 und DIN_5007_VAR_2!!!
      + "& ss , \u00DF" // ß
      + "& ss , \u1E9E" //
  ;

  private static final String COLLATOR_RULES_AE = "" //
      + "& AE , \u00C6" // Æ
      + "& ae , \u00E6" //
  ;

  private static final String COLLATOR_RULES_OE = "" // (LIGATURE OE)
      + "& OE , \u0152" // Œ
      + "& oe , \u0153" //
  ;

  private static final String COLLATOR_RULES_THORN = "" //
      + "& TH , \u00DE" // Þ
      + "& th , \u00FE" //
  ;

  // ELECTMUC-1495, ELECTMUC-1745, spezielle Ausnahmen von den Regeln.
  private static final String COLLATOR_RULES_DIACRITIC_REPLACEMENTS = "" //
      + "& A , \u01DE" // Ǟ
      + "& a , \u01DF" //
      + "& D , \u00D0" // Đ (ETH)
      + "& d , \u00F0" //
      + "& D , \u0110" // Đ (D WITH STROKE)
      + "& d , \u0111" //
      + "& E , \u018F" // Ə (SCHWA)
      + "& e , \u0259" //
      + "& G , \u01E4" // Ǥ
      + "& g , \u01E5" //
      + "& H , \u0126" // Ħ
      + "& h , \u0127" //
      + "& i , \u0131" // ı (DOTLESS I)
      + "& k , \u0138" // ĸ (KRA)
      + "& L , \u013F" // Ŀ
      + "& l , \u0140" //
      + "& L , \u0141" // Ł
      + "& l , \u0142" //
      + "& n , \u0149" // ŉ
      + "& N , \u014A" // Ŋ (ENG)
      + "& n , \u014B" //
      + "& O , \u00D8" // Ø
      + "& o , \u00F8" //
      + "& O , \u022A" // Ȫ
      + "& o , \u022B" //
      + "& T , \u0166" // Ŧ
      + "& t , \u0167" //
      + "& Y , \u01B7" // Ʒ (EZH)
      + "& y , \u0292" //
  ;

  public static final String COLLATOR_RULES_DEFAULT = "" //
      + COLLATOR_RULES_AE //
      + COLLATOR_RULES_OE //
      + COLLATOR_RULES_THORN //
      + COLLATOR_RULES_SHARP_S //
      + COLLATOR_RULES_DIACRITIC_REPLACEMENTS //
  ;

  /**
   * Ä == A
   */
  public static final String COLLATOR_RULES_DIN_5007_VAR_1 = COLLATOR_RULES_DEFAULT //
      + COLLATOR_RULES_UMLAUTE_DIN_5007_VAR_1 //
  ;

  /**
   * Ä == A
   */
  public static final Collection<Entry<Pattern, String>> COLLATOR_REPLACEMENTS_DIN_5007_VAR_1 = extractCollatorRulesReplacementPerAlias( //
      COLLATOR_RULES_DIN_5007_VAR_1 //
  );

  /**
   * Ä == Ae
   */
  public static final String COLLATOR_RULES_DIN_5007_VAR_2 = COLLATOR_RULES_DEFAULT //
      + COLLATOR_RULES_UMLAUTE_DIN_5007_VAR_2 //
  ;

  /**
   * Ä == Ae
   */
  public static final Collection<Entry<Pattern, String>> COLLATOR_REPLACEMENTS_DIN_5007_VAR_2 = extractCollatorRulesReplacementPerAlias( //
      COLLATOR_RULES_DIN_5007_VAR_2 //
  );

  private static final Collection<Entry<Pattern, String>> COLLATOR_DIACRITIC_REPLACEMENTS = extractCollatorRulesReplacementPerAlias( //
      COLLATOR_RULES_DIACRITIC_REPLACEMENTS //
  );

  private static final Collection<Entry<Pattern, String>> NORMALIZER_REPLACEMENTS = new ArrayList<>();

  public static final Pattern COMPILE = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");

  static {
    NORMALIZER_REPLACEMENTS.addAll(extractCollatorRulesReplacementPerAlias( //
        COLLATOR_RULES_AE, //
        COLLATOR_RULES_OE, //
        COLLATOR_RULES_THORN, //
        COLLATOR_RULES_SHARP_S //
    ));

    // Bindestriche...
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u002D", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2010", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2011", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2012", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2013", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2014", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
    NORMALIZER_REPLACEMENTS.add(new SimpleEntry<>(Pattern.compile("\u2015", Pattern.LITERAL), Matcher.quoteReplacement(" ")));
  }

  private CaseAndDiacriticIgnoringNormalizer() {
    super();
  }

  /**
   * @param value
   * @param indexZeroBased
   *
   * @return Das normalisierte Zeichen an der angegebenen Stelle im String oder einen leeren String. BEACHTE, dass ein normalisiertes Zeichen nicht zwangsläufig ein einzelnes Zeichen ist! So gilt
   *         bspw. ß -> SS.
   */
  public String getCharAtNormalized(String value, int indexZeroBased) {
    return getCharAtNormalizedOrDefault(value, indexZeroBased, "");
  }

  /**
   * @param value
   * @param indexZeroBased
   * @param defaultValue
   *
   * @return Das normalisierte Zeichen an der angegebenen Stelle im String oder den angegebenen Standardwert. BEACHTE, dass ein normalisiertes Zeichen nicht zwangsläufig ein einzelnes Zeichen ist! So
   *         gilt bspw. ß -> SS.
   */
  public String getCharAtNormalizedOrDefault(String value, int indexZeroBased, String defaultValue) {
    return getStringNormalized(getCharAtOrDefault(value, indexZeroBased, defaultValue));
  }

  private static final ConcurrentHashMap<String, String> CACHE = new ConcurrentHashMap<>();

  public String getStringNormalized(String value) {
    //    if (value==null){
    //      return null;
    //    }
    //    return CACHE.computeIfAbsent(value, x->getStringNormalized(value, NORMALIZER_REPLACEMENTS));
    return getStringNormalized(value, NORMALIZER_REPLACEMENTS);
  }

  public String getStringNormalized(String value, Collection<Entry<Pattern, String>> replacementPerAlias) {
    if (isNotBlank(value)) {
      String result = normalizeDiacritics(value);

      for (Entry<Pattern, String> entry : replacementPerAlias) {
        // result = result.replace(entry.getKey(), entry.getValue());

        result = entry.getKey().matcher(result).replaceAll(entry.getValue());

      }

      return result.toUpperCase();
    }

    return value;
  }

  public String normalizeDiacritics(String value) {
    if (isNotBlank(value)) {
      String result = value;

      result = Normalizer.normalize(result, Normalizer.Form.NFD);
      // result = result.replaceAll("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+", "");
      result = COMPILE.matcher(result).replaceAll("");

      for (Entry<Pattern, String> entry : COLLATOR_DIACRITIC_REPLACEMENTS) {
        // result = result.replace(entry.getKey(), entry.getValue());
        result = entry.getKey().matcher(result).replaceAll(entry.getValue());
      }

      return result;
    }

    return value;
  }

  public String normalizeDiacriticsNotInCharset(String value, Charset targetEncoding) {
    return normalizeDiacriticsNotInCharset(value, StandardCharsets.UTF_8, targetEncoding);
  }

  public String normalizeDiacriticsNotInCharset(String value, Charset sourceEncoding, Charset targetEncoding) {
    if (isNotBlank(value)) {
      String valueSourceEncoded = value;
      String valueTargetEncoded = getStringEncoded(valueSourceEncoded, targetEncoding);

      if (!valueSourceEncoded.equals(valueTargetEncoded)) {
        String targetEncodingReplacement = getStringEncoded(targetEncoding.newDecoder().replacement(), targetEncoding);

        if (!valueTargetEncoded.contains(targetEncodingReplacement)) {
          return valueSourceEncoded;
        }

        int valueSourceEncodedCodePointCount = valueSourceEncoded.codePointCount(0, valueSourceEncoded.length());
        int valueTargetEncodedCodePointCount = valueTargetEncoded.codePointCount(0, valueTargetEncoded.length());

        if (valueSourceEncodedCodePointCount == valueTargetEncodedCodePointCount) {
          StringBuilder resultSourceEncoded = new StringBuilder();

          for (char i = 0; i < valueTargetEncodedCodePointCount; i++) {
            String characterTargetEncoded = getCharAt(valueTargetEncoded, i);

            if (targetEncodingReplacement.equals(characterTargetEncoded)) {
              resultSourceEncoded.append(normalizeDiacritics(getCharAt(valueSourceEncoded, i)));
            } else {
              resultSourceEncoded.append(getStringEncoded(characterTargetEncoded, sourceEncoding));
            }
          }

          return resultSourceEncoded.toString();
        }
      }
    }

    return value;
  }

  private String getCharAt(String value, int indexZeroBased) {
    return getCharAtOrDefault(value, indexZeroBased, "");
  }

  private String getCharAtOrDefault(String value, int indexZeroBased, String defaultValue) {
    if (nonNull(value) && indexZeroBased >= 0 && indexZeroBased < value.codePointCount(0, value.length())) {
      return new String(Character.toChars(value.codePointAt(indexZeroBased)));
    }

    return defaultValue;
  }

  private String getStringEncoded(String value, Charset to) {
    return new String(value.getBytes(to), to);
  }

  private static Collection<Entry<Pattern, String>> extractCollatorRulesReplacementPerAlias(String... values) {
    return stream(values) //
        .flatMap(collatorRules -> stream(splitByWholeSeparator(collatorRules, "&"))) //
        .map(collatorRule -> splitByWholeSeparator(collatorRule, ",")) //
        .map(replacementAndAlias -> new SimpleEntry<>(Pattern.compile(trimToEmpty(replacementAndAlias[1]), Pattern.LITERAL), Matcher.quoteReplacement(trimToEmpty(replacementAndAlias[0])))) //
        .collect(Collectors.toList());
  }
}
