
package de.ivu.elect.dokumente;

import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static org.apache.commons.lang3.StringUtils.capitalize;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.isNumeric;
import static org.apache.commons.lang3.StringUtils.lowerCase;
import static org.apache.commons.lang3.StringUtils.splitByWholeSeparator;
import static org.apache.commons.lang3.StringUtils.substring;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.StringUtils.uncapitalize;
import static org.apache.commons.lang3.StringUtils.upperCase;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.document.generator.template.BaseDocument;
import de.ivu.document.generator.template.annotations.DocumentMethod;
import de.ivu.document.generator.template.annotations.DocumentParam;
import de.ivu.elect.business.DateUtil;
import de.ivu.elect.dokumente.image.BildDokument;

/**
 * Methoden zur Formatierung in Dokumenten.
 * <p>
 * Neue Methoden auch in Confluence (https://support.elect-it.com/wiki/display/ELECT/Spezielle+Objekte) und im Handbuch (L:\Wahlen\Handbücher\Dokumente\*.docx) ergänzen!
 */
public class DokumentFormatter implements Serializable {

  private static final long serialVersionUID = 1L;

  private static final List<String> ALPHABET = asList("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z");

  private static final String SEPARATOR_KOMMASEPARIERT = ", ";

  private static final Logger LOG = LoggerFactory.getLogger(DokumentFormatter.class);

  private final Locale locale;

  private final DokumentFormatterSettings settings;

  public DokumentFormatter(Locale locale) {
    this(locale, new DokumentFormatterSettings());
  }

  public DokumentFormatter(Locale locale, DokumentFormatterSettings settings) {
    super();

    this.locale = locale;

    this.settings = settings;
  }

  public Locale getLocale() {
    return locale;
  }

  public DokumentFormatterSettings getSettings() {
    return settings;
  }

  /**
   * ELECT-15373, Verwendung im Template erfordert Präfix "OOXML:", also "OOXML:formatter.drawImageBase64(<value>, <widthInCm>, <heightInCm>)".
   */
  @DocumentMethod
  public String drawImageBase64(@Nullable @DocumentParam("\"value\"") Object value, @Nullable @DocumentParam("\"widthInCm\"") Number widthInCm,
      @Nullable @DocumentParam("\"heightInCm\"") Number heightInCm) {
    if (value == null) {
      return "";
    }

    var contentBase64 = value.toString();
    Optional<Double> aspectRatio = Optional.empty();

    int roundingDecimalPlacesMax = 2;
    double roundingScale = Math.pow(10, roundingDecimalPlacesMax);

    if (value instanceof BildDokument) {
      BildDokument bildDokument = (BildDokument) value;

      contentBase64 = bildDokument.contentBase64;
      aspectRatio = bildDokument.aspectRatio;
    }

    if (isBlank(contentBase64)) {
      return "";
    }

    var name = "Bild" + System.nanoTime();

    var widthValue = "";

    if (widthInCm != null) {
      widthValue = widthInCm + "cm";
    } else if (aspectRatio.isPresent() && heightInCm != null) {
      widthValue = Math.round(heightInCm.doubleValue() * aspectRatio.get() * roundingScale) / roundingScale + "cm";
    }

    var width = "";

    if (isNotBlank(widthValue)) {
      width = "svg:width=\"" + widthValue + "\"";
    }

    var heightValue = "";

    if (heightInCm != null) {
      heightValue = heightInCm + "cm";
    } else if (aspectRatio.isPresent() && widthInCm != null) {
      heightValue = Math.round(widthInCm.doubleValue() / aspectRatio.get() * roundingScale) / roundingScale + "cm";
    }

    var height = "";

    if (isNotBlank(heightValue)) {
      height = "svg:height=\"" + heightValue + "\"";
    }

    return "" //
        + "<draw:frame draw:name=\"" + name + "\" text:anchor-type=\"paragraph\" " + width + " " + height + " draw:z-index=\"0\">" //
        + "  <draw:image xlink:type=\"simple\" xlink:show=\"embed\" xlink:actuate=\"onLoad\">" //
        + "    <office:binary-data>" + contentBase64 + "</office:binary-data>" //
        + "  </draw:image>" //
        + "</draw:frame>"; //
  }

  @DocumentMethod // ELECT-14575
  public Date getNaechsterMoBisFr(@Nullable Date value) {
    return DateUtil.getNextMoToFr(value);
  }

  @Nullable
  @DocumentMethod // ELECT-13791
  public String formatSummeGanzzahlig(@Nullable Number one, @Nullable Number another, @Nullable Number... more) {
    return formatNumber(summiereGanzzahlig(one, another, more), getLocale());
  }

  @Nullable
  @DocumentMethod // ELECT-13791
  public Long summiereGanzzahlig(@Nullable Number one, @Nullable Number another, @Nullable Number... more) {
    var result = summiereGanzzahlig(one, another);

    if (more != null) {
      for (var next : more) {
        result = summiereGanzzahlig(result, next);
      }
    }

    return result;
  }

  @Nullable
  private Long summiereGanzzahlig(@Nullable Number one, @Nullable Number another) {
    if (one == null) {
      if (another == null) {
        return null;
      }

      return another.longValue();
    }

    if (another == null) {
      return one.longValue();
    }

    return one.longValue() + another.longValue();
  }

  @DocumentMethod // ELECT-9461
  public Collection<Object> getIteratorSublisteVon(@Nullable @DocumentParam("\"liste\"") Object value, @DocumentParam("\"indexVon\"") Integer indexFromZeroBased) {
    return getIteratorSublisteVonBis(value, indexFromZeroBased, Integer.MAX_VALUE);
  }

  @DocumentMethod // ELECT-9461
  public Collection<Object> getIteratorSublisteVonBis(@Nullable @DocumentParam("\"liste\"") Object value, @DocumentParam("\"indexVon\"") Integer indexFromZeroBased,
      @DocumentParam("\"indexBis\"") Integer indexToZeroBased) {
    if (value != null && indexFromZeroBased != null && indexToZeroBased != null) {
      List<Object> result = new ArrayList<>();

      try {
        if (value.getClass().isArray()) {
          result.addAll(asList((Object[]) value));
        } else if (value instanceof List) {
          result.addAll((List< ? >) value);
        }

        var fromIndex = Math.max(indexFromZeroBased, 0);
        var toIndex = Math.min(result.size(), Math.max(indexToZeroBased, 0));

        if (fromIndex <= toIndex) {
          return result.subList(fromIndex, toIndex);
        }
      } catch (Exception ex) {
        LOG.error("Formatierung fehlgeschlagen [liste = {}, liste instanceof array = {}, liste instanceof list = {}, indexVon = {}, indexBis = {}]", value, value.getClass().isArray(),
            value instanceof List, indexFromZeroBased, indexToZeroBased, ex);
      }
    }

    return new ArrayList<>();
  }

  @DocumentMethod // ELECT-8846
  public Collection<Object> getIteratorMitDefinierterAnzahl(@DocumentParam("\"anzahl\"") Long anzahl) {
    List<Object> result = new ArrayList<>();

    if (anzahl != null) {
      for (var i = 0; i < anzahl; i++) {
        result.add("");
      }
    }

    return result;
  }

  @DocumentMethod // ELECTDD-745
  public Collection<Long> getIteratorGanzzahligBis(@DocumentParam("\"nummerBis\"") Long nummerBis) {
    return getIteratorGanzzahligVonBis(1L, nummerBis);
  }

  @DocumentMethod // ELECTDD-745
  public Collection<Long> getIteratorGanzzahligVonBis(@DocumentParam("\"nummerVon\"") Long nummerVon, @DocumentParam("\"nummerBis\"") Long nummerBis) {
    List<Long> result = new ArrayList<>();

    if (nummerVon != null && nummerBis != null) {
      for (long i = nummerVon; i <= nummerBis; i++) {
        result.add(i);
      }
    }

    return result;
  }

  public String getZeroWertZeichen() {
    return getSettings().getZeroWertZeichen();
  }

  @Nullable
  @DocumentMethod
  public String ersetzeFehlwert(@Nullable @DocumentParam("Originsl") String original, String ersatz) {
    if (original == null || original.isEmpty()) {
      if (ersatz == null) {
        return "";
      }
      return ersatz;
    }
    return original;
  }

  @DocumentMethod
  public String formatDatum(@Nullable @DocumentParam("Datum") Date date) {
    return defaultIfBlank(DokumentFormatter.formatDateMedium(date, getLocale()), "");
  }

  @DocumentMethod
  public String formatDatumKurz(@Nullable @DocumentParam("Datum") Date date) {
    return defaultIfBlank(DokumentFormatter.formatDateShort(date, getLocale()), "");
  }

  @DocumentMethod
  public String formatDatumLang(@Nullable @DocumentParam("Datum") Date date) {
    return defaultIfBlank(DokumentFormatter.formatDateLong(date, getLocale()), "");
  }

  @Nullable
  public static String formatDatePattern(@Nullable Date date, Locale locale, String pattern) {
    if (date != null) {
      try {
        return new SimpleDateFormat(pattern, locale).format(date);
      } catch (Exception ex) {
        LOG.error("Fehler bei der Datumsformatierung [date = {}, locale = {}, pattern = {}]", date, locale, pattern, ex);
      }
    }

    return null;
  }

  @Nullable
  public static String formatDateShort(@Nullable Date date, Locale locale) {
    if (date == null) {
      return null;
    }

    return DateFormat.getDateInstance(DateFormat.SHORT, locale).format(date);
  }

  @Nullable
  public static String formatDateMedium(@Nullable Date date, Locale locale) {
    if (date == null) {
      return null;
    }

    return DateFormat.getDateInstance(DateFormat.MEDIUM, locale).format(date);
  }

  @Nullable
  public static String formatDateLong(@Nullable Date date, Locale locale) {
    if (date == null) {
      return null;
    }

    return DateFormat.getDateInstance(DateFormat.LONG, locale).format(date);
  }

  @Nullable
  @DocumentMethod
  public String formatZeit(@Nullable @DocumentParam("Zeitpunkt") Date date) {
    if (date == null) {
      return "";
    }

    return defaultIfBlank(DokumentFormatter.formatTimeHHmm(date, getLocale()), "");
  }

  @Nullable
  @DocumentMethod
  public String formatZeitMitSekunden(@Nullable @DocumentParam("Zeitpunkt") Date date) {
    if (date == null) {
      return "";
    }

    return defaultIfBlank(DokumentFormatter.formatTimeHHmmss(date, getLocale()), "");
  }

  @Nullable
  public static String formatTimeHHmm(Date dateTime, Locale locale) {
    if (dateTime == null) {
      return null;
    }

    return DateFormat.getTimeInstance(DateFormat.SHORT, locale).format(dateTime);
  }

  @Nullable
  public static String formatTimeHHmmss(Date dateTime, Locale locale) {
    if (dateTime == null) {
      return null;
    }

    return DateFormat.getTimeInstance(DateFormat.MEDIUM, locale).format(dateTime);
  }

  @Nullable
  public static String formatDateTimeMedium(Date dateTime, Locale locale) {
    if (dateTime == null) {
      return null;
    }

    return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale).format(dateTime);
  }

  public String formatStimmzahl(@Nullable Long stimmzahl) {
    if (stimmzahl == null) {
      return getSettings().getFormatFuerKeineStimmenzahl();
    }

    return formatZahl(stimmzahl);
  }

  /**
   * Formatierung einer Differenz von Stimmenanzahlen (meist aktuell - Vorperiode)
   */
  public String formatStimmzahlDifferenz(@Nullable Long stimmzahl) {
    return formatStimmzahl(stimmzahl);
  }

  @DocumentMethod
  public String formatZahl(@DocumentParam("Wert") Long number) {
    return DokumentFormatter.formatNumber(number, getLocale());
  }

  @DocumentMethod
  public String formatZahlMin(@DocumentParam("Wert1") Long one, @DocumentParam("Wert2") Long another) {
    var min = one;

    if (one == null) {
      min = another;
    } else if (another != null) {
      min = Math.min(one, another);
    }

    return DokumentFormatter.formatNumber(min, getLocale());
  }

  @DocumentMethod
  public String formatZahlMax(@DocumentParam("Wert1") Long one, @DocumentParam("Wert2") Long another) {
    var max = one;

    if (one == null) {
      max = another;
    } else if (another != null) {
      max = Math.max(one, another);
    }

    return DokumentFormatter.formatNumber(max, getLocale());
  }

  public static String formatNumber(Long value, Locale locale) {
    if (value == null) {
      return "";
    }

    try {
      return NumberFormat.getInstance(locale).format(value);
    } catch (NumberFormatException ignored) { // ELECTDD-322
    }

    return "";
  }

  /**
   * @param stelle
   *          eine positiver Wert liefert die Stelle von rechts 1-basiert an (1 = Einerstelle, 2 = Zehnerstelle, 3 = Hunderterstelle usw.). Ein negativer Wert gibt die Stelle von links an (0-basiert).
   *          (Z.B. bei "4711" ist 0 = Tausenderstelle, -1 = Hunderterstelle, -2 = Zehnerstelle usw.)
   * @param number
   *          Eine String-Darstellung einer ganzen Zahl. Sollte eigentlich ein Long sein, aber da wurde manchmal fehlerhaft null zu 0, deshalb jetzt als String.
   * @return die Ziffer von <code>number</code> an der angegebenen Stelle
   */
  @DocumentMethod
  public String ziffer(int stelle, @DocumentParam("Wert") String number) {
    if (StringUtils.isEmpty(number)) {
      return "";
    }
    if (number.startsWith("-")) {
      return ziffer(stelle, number.substring(1));
    }
    var string = String.valueOf(number);
    var length = string.length();
    if (stelle > 0) {
      if (stelle <= length) {
        return string.substring(length - stelle, length - stelle + 1);
      } else {
        return "";
      }
    } else {
      if (-stelle < length) {
        return string.substring(-stelle, -stelle + 1);
      } else {
        return "";
      }
    }
  }

  public String formatProzent(@Nullable BigDecimal prozent) {
    return formatProzentStellen(prozent, getSettings().getAnzahlStellenFuerProzentwerte());
  }

  /* überladene Methoden funktionieren in Js nicht => eigener name) */
  @DocumentMethod
  public String formatProzentBigDecimal(@Nullable @DocumentParam("Wert") BigDecimal value) {
    return DokumentFormatter.formatProzent(value, getLocale());
  }

  public static String formatProzent(BigDecimal value, Locale locale) {
    if (value == null) {
      return "";
    }

    return DokumentFormatter.formatBigDecimal(value, 1, locale) + " %";
  }

  @DocumentMethod
  public String formatBigDecimal(@Nullable @DocumentParam("Wert") BigDecimal value) {
    return DokumentFormatter.formatBigDecimal(value, getLocale());
  }

  public static String formatBigDecimal(BigDecimal value, Locale locale) {
    return formatBigDecimal(value, -1, locale);
  }

  public static String formatBigDecimal(BigDecimal value, int fractionDigits, Locale locale) {
    if (value == null) {
      return "";
    }

    try {
      var numberFormat = NumberFormat.getInstance(locale);

      if (fractionDigits >= 0) {
        numberFormat.setMinimumFractionDigits(fractionDigits);
        numberFormat.setMaximumFractionDigits(fractionDigits);
      }

      return numberFormat.format(value);
    } catch (NumberFormatException ignored) { // ELECTDD-322
    }

    return "";
  }

  @DocumentMethod
  public String formatBigDecimalZweiKommastellen(@Nullable @DocumentParam("Wert") BigDecimal value) {
    return formatBigDecimalKommastellen(value, 2);
  }

  /**
   * Beispiele: formatBigDecimalKommastellen(new BigDecimal(1234L), 2) ergibt "1.234,00" formatBigDecimalKommastellen(new BigDecimal(1234.565), 2) ergibt "1.234,57"
   */
  @DocumentMethod
  public String formatBigDecimalKommastellen(@Nullable @DocumentParam("Wert") BigDecimal value, @DocumentParam("anzahlStellen") int anzahlStellen) {
    return formatNummerNachkommastellenFixMitNullen(value, anzahlStellen);
  }

  /**
   * WASDE-2086
   * <p>
   * Wie die Methode <code>formatBigDecimalKommastellen</code>, jedoch mit Berücksichtigung, dass bei knapper Unterschreitung der Prozenthürde nicht aufgerundet wird.
   */
  @DocumentMethod
  public String formatBigDecimalKommastellenMitProzenthuerde(@Nullable @DocumentParam("Wert") BigDecimal value, @DocumentParam("AnzahlStellen") int anzahlStellen,
      @Nullable @DocumentParam("Prozenthuerde") BigDecimal prozenthuerde, @DocumentParam("AnzahlStellenNaheProzenthuerde") int anzahlStellenNaheProzenthuerde) {
    if (value != null && isKnappeUnterschreitungDerProzenthuerde(value, anzahlStellen, prozenthuerde)) {
      var valueAbgerundet = value.round(new MathContext(anzahlStellenNaheProzenthuerde + 1, RoundingMode.DOWN));
      return formatBigDecimalKommastellen(valueAbgerundet, anzahlStellenNaheProzenthuerde);
    }
    return formatBigDecimalKommastellen(value, anzahlStellen);
  }

  public boolean isKnappeUnterschreitungDerProzenthuerde(@Nullable BigDecimal value, int anzahlStellen, @Nullable BigDecimal prozenthuerde) {
    if (value == null || prozenthuerde == null) {
      return false;
    }
    var schwellwertInPercentMin = prozenthuerde.doubleValue() - 0.5 / Math.pow(10d, anzahlStellen);
    var schwellwertInPercentMax = prozenthuerde.doubleValue();
    var valueInPercent = value.doubleValue();
    return valueInPercent >= schwellwertInPercentMin && valueInPercent < schwellwertInPercentMax;
  }

  @DocumentMethod
  public String formatProzentDouble(@Nullable @DocumentParam("Wert") Double value) {
    return formatProzentMitStellenIntern(value, 2);
  }

  /**
   * Beispiele: formatProzentMitStellenDouble(1234d, 2) ergibt "1.234,00 %" df.formatProzentMitStellenDouble(1234.565001d, 2) ergibt "1.234,57 %"
   */
  @DocumentMethod
  public String formatProzentMitStellenDouble(@Nullable @DocumentParam("Wert") Double value, @DocumentParam("AnzahlStellen") int anzahlStellen) {
    return formatProzentMitStellenIntern(value, anzahlStellen);
  }

  private String formatProzentMitStellenIntern(@Nullable Double value, int anzahlStellen) {
    if (value == null) {
      return "";
    }

    try {
      return formatBigDecimalKommastellen(BigDecimal.valueOf(value), anzahlStellen) + " %";
    } catch (NumberFormatException e) {
      // ELECTDD-322 ignorieren
      return "";
    }
  }

  @DocumentMethod
  public String formatDatumPattern(@DocumentParam("Datum") Date datum, @DocumentParam("Pattern") String pattern) {
    if (datum != null && isNotBlank(pattern)) {
      try {
        return new SimpleDateFormat(pattern, getLocale()).format(datum);
      } catch (Exception ex) {
        LOG.error("Fehler bei der Datumsformatierung [datum = {}, locale = {}, pattern = {}]", datum, getLocale(), pattern, ex);
      }
    }

    return formatDatumLang(datum);
  }

  public String formatStimmanteil(@Nullable BigDecimal percent) {
    if (percent == null) {
      return getSettings().getFormatFuerKeinStimmenanteil();
    }

    if (percent.equals(BigDecimal.ZERO)) {
      var formatFuerStimmenanteil0 = getSettings().getFormatFuerStimmenanteil0();
      if (formatFuerStimmenanteil0 != null) {
        return formatFuerStimmenanteil0;
      }
    }

    return formatProzentStellen(percent, 1);
  }

  /**
   * Dient der Formatierung von percent unter Berücksichtigung von anzahl. Damit kann unterschieden werden, ob eine gerundete Prozentangabe "0,0" tatsächlich auf eine 0 zurückgeht.
   */
  public String formatStimmanteil(@Nullable BigDecimal percent, @Nullable Long anzahl) {
    return formatStimmanteil(percent);
  }

  /**
   * Darstellung einer Differenz von Stimmanteilen (meist "% aktuell" - "% Vorperiode") unter Berücksichtigung von signum. Damit kann unterschieden werden, ob eine gerundete Prozentangabe "0,0"
   * tatsächlich auf eine 0 zurückgeht.
   */
  public String formatStimmanteilDifferenz(@Nullable BigDecimal percent, @Nullable Long signum) {
    return formatStimmanteil(percent);
  }

  public String formatProzentStellen(@Nullable BigDecimal prozent, int stellen) {
    if (prozent == null) {
      return getSettings().getFormatFuerKeinStimmenanteil();
    }

    return formatBigDecimalKommastellen(prozent, stellen);
  }

  public String formatMandatszahlDifferenz(@Nullable Long mandatszahl) {
    return formatMandatszahl(mandatszahl);
  }

  public String formatMandatszahl(@Nullable Long mandatszahl) {
    if (mandatszahl == null) {
      return getSettings().getFormatFuerKeineMandatszahl();
    }
    if (mandatszahl == 0L) {
      var result = getSettings().getFormatFuerMandatszahl0();
      if (result != null) {
        return result;
      }
    }
    return formatZahl(mandatszahl);
  }

  public String formatBerechnungszahl(@Nullable BigDecimal berechnungszahl) {
    if (berechnungszahl == null) {
      return getSettings().getFormatFuerKeineBerechnungszahl();
    }
    return formatBigDecimalKommastellen(berechnungszahl, getSettings().getAnzahlStellenFuerBerechnungszahl());
  }

  @DocumentMethod
  public String formatGesetzteFelderKommasepariert(@DocumentParam("\"objekt\"") Object obj, @DocumentParam("\"feldnamen\"") String... fieldNames) {
    return formatGesetzteFelderOderLeerwertKommasepariert("", obj, fieldNames);
  }

  @DocumentMethod
  public String formatGesetzteFelderSepariert(@DocumentParam("\"trennzeichen\"") String separator, @DocumentParam("\"objekt\"") Object obj, @DocumentParam("\"feldnamen\"") String... fieldNames) {
    return formatGesetzteFelderOderLeerwertSepariert(separator, "", obj, fieldNames);
  }

  @DocumentMethod
  public String formatGesetzteFelderOderLeerwertKommasepariert(@DocumentParam("\"leerwert\"") String defaultValue, @DocumentParam("\"objekt\"") Object obj,
      @DocumentParam("\"feldnamen\"") String... fieldNames) {
    return formatGesetzteFelderOderLeerwertSepariert(SEPARATOR_KOMMASEPARIERT, defaultValue, obj, fieldNames);
  }

  @DocumentMethod
  public String formatGesetzteFelderOderLeerwertSepariert(@DocumentParam("\"trennzeichen\"") String separator, @DocumentParam("\"leerwert\"") String defaultValue,
      @DocumentParam("\"objekt\"") Object obj, @DocumentParam("\"feldnamen\"") String... fieldNames) {
    try {
      String[] valuesFormatted;

      if (obj instanceof Iterable) { // ELECT-10574
        List<String> fieldValuesJoined = new ArrayList<>();

        for (Object document : (Iterable< ? >) obj) {
          var fieldValues = extractDocumentFieldValues((BaseDocument) document, fieldNames, defaultValue);

          fieldValuesJoined.add(formatGesetzteWerteOderLeerwertSepariert(" ", defaultValue, fieldValues));
        }

        valuesFormatted = fieldValuesJoined.toArray(new String[fieldValuesJoined.size()]);
      } else {
        var fieldValues = extractDocumentFieldValues((BaseDocument) obj, fieldNames, defaultValue);

        valuesFormatted = fieldValues;
      }

      return formatGesetzteWerteOderLeerwertSepariert(separator, defaultValue, valuesFormatted);
    } catch (Exception ex) {
      throw new RuntimeException("Formatierung fehlgeschlagen [" //
          + "trennzeichen = " + separator + ", " //
          + "leerwert = " + defaultValue + ", " //
          + "objekt = " + obj + ", " //
          + "feldnamen = " + Arrays.toString(fieldNames) //
          + "]", ex);
    }
  }

  private String[] extractDocumentFieldValues(BaseDocument document, String[] fieldNames, String defaultValue) {
    return stream(fieldNames) //
        .map(document::get) //
        .map(value -> value == null || value instanceof CharSequence && isBlank((CharSequence) value) ? defaultValue : value.toString()) //
        .toArray(String[]::new);
  }

  @DocumentMethod
  public String formatGesetzteWerteKommasepariert(@DocumentParam("\"werte\"") String... values) {
    return formatGesetzteWerteOderLeerwertKommasepariert("", values);
  }

  @DocumentMethod
  public String formatGesetzteWerteSepariert(@DocumentParam("\"trennzeichen\"") String separator, @DocumentParam("\"werte\"") String... values) {
    return formatGesetzteWerteOderLeerwertSepariert(separator, "", values);
  }

  @DocumentMethod
  public String formatGesetzteWerteOderLeerwertKommasepariert(@DocumentParam("\"leerwert\"") String defaultValue, @DocumentParam("\"werte\"") String... values) {
    return formatGesetzteWerteOderLeerwertSepariert(SEPARATOR_KOMMASEPARIERT, defaultValue, values);
  }

  @DocumentMethod
  public String formatGesetzteWerteOderLeerwertSepariert(@DocumentParam("\"trennzeichen\"") String separator, @DocumentParam("\"leerwert\"") String defaultValue,
      @DocumentParam("\"werte\"") String... values) {
    return joinNonBlankValues(separator == null ? " " : separator, defaultValue, values);
  }

  /**
   * @see http://www.css4you.de/example/list-style-type.html
   */
  @DocumentMethod
  public String formatAufzaehlungszeichenUpperAlpha(@Nullable @DocumentParam("wert") Number valueOneBased) {
    if (valueOneBased != null) {
      return formatStringUpperCase(formatAufzaehlungszeichenOneBased(ALPHABET, valueOneBased.intValue()));
    }

    return "";
  }

  /**
   * @see http://www.css4you.de/example/list-style-type.html
   */
  @DocumentMethod
  public String formatAufzaehlungszeichenLowerAlpha(@Nullable @DocumentParam("wert") Number valueOneBased) {
    if (valueOneBased != null) {
      return formatStringLowerCase(formatAufzaehlungszeichenOneBased(ALPHABET, valueOneBased.intValue()));
    }

    return "";
  }

  private String formatAufzaehlungszeichenOneBased(List<String> dictionary, int intValueOneBased) {
    if (intValueOneBased < 1) {
      return "";
    }

    return formatAufzaehlungszeichenZeroBased(dictionary, intValueOneBased - 1);
  }

  private String formatAufzaehlungszeichenZeroBased(List<String> dictionary, int intValueZeroBased) {
    if (intValueZeroBased < 0) {
      return "";
    }

    var result = dictionary.get(intValueZeroBased % dictionary.size());

    var quotient = intValueZeroBased / dictionary.size();

    if (quotient == 0) {
      return result;
    }

    return formatAufzaehlungszeichenZeroBased(dictionary, quotient - 1) + result;
  }

  @DocumentMethod
  public String formatStringUpperCase(@Nullable @DocumentParam("wert") String value) {
    return upperCase(value, getLocale());
  }

  @DocumentMethod
  public String formatStringLowerCase(@Nullable @DocumentParam("wert") String value) {
    return lowerCase(value, getLocale());
  }

  @DocumentMethod
  public String formatStringCapitalized(@Nullable @DocumentParam("wert") String value) {
    return capitalize(value);
  }

  @DocumentMethod
  public String formatStringUncapitalized(@Nullable @DocumentParam("wert") String value) {
    return uncapitalize(value);
  }

  @DocumentMethod
  public String formatNummerVorkommastellenFixMitNullen(@Nullable @DocumentParam("wert") Number value, @DocumentParam("anzahlStellen") int count) {
    return formatNummerVorkommastellenFix(value, "0", count);
  }

  @DocumentMethod
  public String formatNummerVorkommastellenFix(@Nullable @DocumentParam("wert") Number value, @DocumentParam("platzhalter") String placeholder, @DocumentParam("anzahlStellen") int count) {
    return formatNummerStellenFix(value, placeholder, count, "", -1);
  }

  @DocumentMethod
  public String formatNummerNachkommastellenFixMitNullen(@Nullable @DocumentParam("wert") Number value, @DocumentParam("anzahlStellen") int count) {
    return formatNummerNachkommastellenFix(value, "0", count);
  }

  @DocumentMethod
  public String formatNummerNachkommastellenFix(@Nullable @DocumentParam("wert") Number value, @DocumentParam("platzhalter") String placeholder, @DocumentParam("anzahlStellen") int count) {
    return formatNummerStellenFix(value, "", -1, placeholder, count);
  }

  @DocumentMethod
  public String formatNummerStellenFixMitNullen(@Nullable @DocumentParam("wert") Number value, @DocumentParam("anzahlVorkommastellen") int integerDigitsCount,
      @DocumentParam("anzahlNachkommastellen") int fractionDigitsCount) {
    return formatNummerStellenFix(value, "0", integerDigitsCount, "0", fractionDigitsCount);
  }

  @DocumentMethod
  public String formatNummerStellenFix(@Nullable @DocumentParam("wert") Number value, @DocumentParam("platzhalterVorkommastellen") String integerDigitPlaceholder,
      @DocumentParam("anzahlVorkommastellen") int integerDigitsCount, @DocumentParam("platzhalterNachkommastellen") String fractionDigitPlaceholder,
      @DocumentParam("anzahlNachkommastellen") int fractionDigitsCount) {
    return DokumentFormatter.formatNumberStellenFix(value, integerDigitPlaceholder, integerDigitsCount, fractionDigitPlaceholder, fractionDigitsCount, getLocale());
  }

  public static String formatNumberStellenFix(Number value, String integerDigitPlaceholder, int integerDigitsCount, String fractionDigitPlaceholder, int fractionDigitsCount, Locale locale) {
    if (value != null) {
      try {
        var decimalFormatSymbols = DecimalFormatSymbols.getInstance(locale);

        var decimalFormat = new DecimalFormat();
        decimalFormat.setRoundingMode(RoundingMode.HALF_UP);

        var decimalSeparator = Character.toString(decimalFormatSymbols.getDecimalSeparator());

        if (fractionDigitsCount >= 0) {
          decimalFormat.setMinimumFractionDigits(fractionDigitsCount);
          decimalFormat.setMaximumFractionDigits(fractionDigitsCount);
        }

        decimalFormat.setDecimalFormatSymbols(decimalFormatSymbols);

        var result = decimalFormat.format(value);
        var resultSeparated = splitByWholeSeparator(result, decimalSeparator);

        if (resultSeparated != null && resultSeparated.length > 0) {
          if (integerDigitsCount > 0) {
            var integerDigitsFormatted = resultSeparated[0];
            var integerDigitsPrefix = "";

            for (var i = 0; i < integerDigitsFormatted.length(); i++) {
              var character = Character.toString(integerDigitsFormatted.charAt(i));

              if (isNumeric(character)) {
                break;
              }

              integerDigitsPrefix += character;
            }

            result = integerDigitsPrefix
                + StringUtils.leftPad(substring(integerDigitsFormatted, integerDigitsPrefix.length(), integerDigitsFormatted.length()), Math.max(0, integerDigitsCount), integerDigitPlaceholder)
                + (resultSeparated.length > 1 ? decimalSeparator + resultSeparated[1] : "");
          }

          if (fractionDigitsCount > 0 && !"0".equals(fractionDigitPlaceholder) && resultSeparated.length > 1) {
            var resultFractionDigitsReplaced = new StringBuilder(result);

            for (var i = result.length() - 1; i >= 0; i--) {
              var character = Character.toString(result.charAt(i));

              if (!"0".equals(character)) {
                break;
              }

              resultFractionDigitsReplaced = resultFractionDigitsReplaced.replace(i, i + 1, fractionDigitPlaceholder);
            }

            result = resultFractionDigitsReplaced.toString();
          }
        }

        return result;
      } catch (Exception ex) { // ELECTDD-322 ignorieren
        LOG.error("Formatierung fehlgeschlagen [wert = {}, platzhalterVorkommastellen = {}, anzahlVorkommastellen = {}, platzhalterNachkommastellen = {}, anzahlNachkommastellen = {}]", value,
            integerDigitPlaceholder, integerDigitsCount, fractionDigitPlaceholder, fractionDigitsCount, ex);
      }
    }

    return "";
  }

  /**
   * ELECT-15413
   */
  public String leftPad(String string, int size, String padChar) {
    if (StringUtils.isBlank(string)) {
      return StringUtils.leftPad("", size, padChar);
    }
    return StringUtils.leftPad(string, size, padChar);
  }

  /**
   * ELECT-15413
   */
  public String leftPadIfNotEmpty(String string, int size, String padChar) {
    if (StringUtils.isBlank(string)) {
      return "";
    }
    return StringUtils.leftPad(string, size, padChar);
  }

  private String joinNonBlankValues(String delimiter, String defaultValue, String... values) {
    return trimToEmpty(stream(values).map(value -> (isBlank(value) ? defaultValue : value)).filter(StringUtils::isNotBlank).collect(Collectors.joining(delimiter)));
  }

  @Override
  public String toString() {
    return "DokumentFormatter{}";
  }
}
