
package de.ivu.elect.business.stimmzettel.boundary;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.kandidatur.control.ListenkandidaturByRelativePositionComparator;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.kandidatur.entity.ListenkandidaturStimmzettelPosition;
import de.ivu.elect.business.kandidatur.entity.ListenkandidaturStimmzettelPositionFormatter;
import de.ivu.elect.business.kandidatur.entity.ListenkandidaturStimmzettelPositionFormatterKonfiguration;
import de.ivu.elect.business.pruefung.boundary.ZulassungPruefer;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.control.WahlvorschlagReihenfolgeVorsortierenComparator;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagStimmzettelPosition;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagStimmzettelPositionFormatter;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagStimmzettelPositionFormatterKonfiguration;
import de.ivu.elect.util.TriFunction;


import static de.ivu.elect.jee.entity.EntityUtil.castTo;
import static java.util.stream.Collectors.toList;

public class EntityAufStimmzettelDatenStimmzettelPositionBerechnung implements Serializable {

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

  private static final long serialVersionUID = 1L;

  @Inject
  private transient AnzahlStimmenProWaehlerBestimmer anzahlStimmenProWaehlerBestimmer;

  public EntityAufStimmzettelDatenStimmzettelPositionBerechnung() {
    super();
  }

  public WahlvorschlagStimmzettelPosition calculateWahlvorschlagStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, @Nullable Gebiet gebiet,
                                                                                    @Nullable Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag == null || wahlvorschlag.getWahl() == null) {
      var result = emptyWahlvorschlagStimmzettelPosition();
      info("Betrachteter Wahlvorschlag ist NICHT gültig", result, wahlvorschlag);

      return result;
    }

    var gebietBetrachtet = gebiet;

    if (gebietBetrachtet == null) {
      gebietBetrachtet = wahlvorschlag.getGebiet(); // Alles wie vorher...
    }

    // (!)  Ungeprüfte sollen erstmal eine Stimmzettelposition haben, bis sie explizit abgelehnt werden, bei Änderungen UND informierem(!)
    // ELECT-12000 (Stimmzettelposition anzeigen)
    // ELECT-9421, ungeprüfte Wahlvorschläge landen nicht auf dem Stimmzettel...
    //    if (wahlvorschlag.isUngeprueft()) {
    //      return emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
    //    }

    // Nicht zugelassene Wahlvorschläge landen nicht auf dem Stimmzettel...
    if (wahlvorschlag.isNichtZugelassenImSinneAbgelehnt()) {
      var result = emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
      info("Betrachteter Wahlvorschlag ist NICHT zugelassen (abgelehnt)", result, wahlvorschlag);

      return result;
    }

    // Nicht zulassungswürdige Wahlvorschläge landen nicht auf dem Stimmzettel...
    //    if (!entityAufStimmzettelDaten.getZulassungPruefer().isZulassenErlaubt(wahlvorschlag)) {
    //      return emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
    //    }

    // Stimmzettelpositionen oberhalb des Gebietes des Wahlvorschlages ist nicht sinnvoll...
    if (!gebietBetrachtet.equals(wahlvorschlag.getGebiet()) && gebietBetrachtet.isInGebietshierarchieUebergeordnetesGebietVon(wahlvorschlag.getGebiet())) {
      var result = emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
      info("Betrachtetes Gebiet ist in übergeordneter Gebietshierarchie des betrachteten Wahlvorschlages", result, wahlvorschlag);

      return result;
    }

    // Bei vorgegebener Position immer diese nehmen (unabhängig von der potentiellen Anzahl der Listenwahlgebiete) ELECT-16140
    var vorgegebenePosition = wahlvorschlag.getGruppeImGebiet().getVorgegebenePosition();
    if (vorgegebenePosition != null) {
      return createWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag, vorgegebenePosition);
    }

    // Wenn Direktkandidat und falls es einen Wahlvorschlag (Liste) derselben Gruppe in einem höheren Gebiet gibt, nimm die Stimmzettelposition von diesem Wahlvorschlag (konkret: Position des Direktkandidaten = Position der Liste)
    if (wahlvorschlag.isDirektkandidat()) {
      var listeInGebietshierarchieUebergeordnet = bestimmeListeImGleichenOderHoeremGebiet(entityAufStimmzettelDaten, wahlvorschlag);

      if (listeInGebietshierarchieUebergeordnet != null) {
        var result = listeInGebietshierarchieUebergeordnet.getStimmzettelPosition(gebietBetrachtet, entityAufStimmzettelDaten);

        if (result != null && result.hasPosition()) { // WASDE-2152
          info("Betrachteter Direktkandidat hat Liste MIT Stimmzettelposition in übergeordneter Gebietshierarchie", result, wahlvorschlag);

          return result;
        } else {
          LOG.warn("Betrachteter Direktkandidat hat Liste OHNE Stimmzettelposition in übergeordneter Gebietshierarchie [kennwort = {}, wahlvorschlag = {}]",
              wahlvorschlag.getKurznameOderKennwort(),
              wahlvorschlag);
        }
      }
    }

    var gebietStimmzettelPositionBerechenbar = wahlvorschlag.getGebiet();

    if (wahlvorschlag.isListe() && wahlvorschlag.getWahl().isWahlMitMehrerenListenGebietsarten()) {
      var liste = castTo(wahlvorschlag, Liste.class);

      gebietStimmzettelPositionBerechenbar = liste.getGebietStimmzettelPositionBerechenbar(gebietBetrachtet);

      if (gebietStimmzettelPositionBerechenbar == null) {
        gebietStimmzettelPositionBerechenbar = wahlvorschlag.getGebiet();
      }

      // Wenn es Listen unterhalb des zu betrachtenden Gebietes gibt...
      if (isListenPositionenInGebietsHierachieVariabel(wahlvorschlag.getWahl())
          && existsGruppenUnabhaengigListenInGebietshierarchieUntergeordnet(entityAufStimmzettelDaten, gebietStimmzettelPositionBerechenbar, wahlvorschlag)) {
        var result = emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
        info("Betrachtete Liste hat Liste in untergeordneter Gebietshierarchie", result, wahlvorschlag);

        return result;
      }
    }

    if (gebietStimmzettelPositionBerechenbar.equals(wahlvorschlag.getGebiet())) {
      // Wenn Position fixiert, dann diese zurückgeben, aber nur, wenn betrachtendes Gebiet dem Gebiet des Wahlvorschlages entspricht...Destatis Europawahl...
      var berechnetePosition = wahlvorschlag.getGruppeImGebiet().getVorgegebenePosition();

      if (berechnetePosition != null) {
        var result = createWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag, berechnetePosition);
        info("Betrachteter Wahlvorschlag hat vorgegebene Position an GruppeImGebiet (3)", result, wahlvorschlag);

        return result;
      }
    }

    if (!gebietStimmzettelPositionBerechenbar.equals(wahlvorschlag.getGebiet())) {
      var gruppeImGebietStimmzettelPositionBerechenbar = WahlvorschlagReihenfolgeVorsortierenComparator.bestimmeGruppeImGebietFuerGebiet(
          wahlvorschlag,
          gebietStimmzettelPositionBerechenbar);

      // Wenn Position fixiert, dann diese zurückgeben
      var berechnetePosition = gruppeImGebietStimmzettelPositionBerechenbar.getVorgegebenePosition();

      if (berechnetePosition != null) {
        var result = createWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag, berechnetePosition);
        info("Betrachteter Wahlvorschlag hat vorgegebene Position an GruppeImGebiet (4)", result, wahlvorschlag);

        return result;
      }
    }

    var wahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault = collectWahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault(
        entityAufStimmzettelDaten, gebietStimmzettelPositionBerechenbar, wahlvorschlag);

    Map<Wahlvorschlag, Long> stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet = new HashMap<>();
    var stimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet = collectStimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet(
        entityAufStimmzettelDaten,
        gebietBetrachtet, wahlvorschlag);

    for (var wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet : wahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault) {
      if (!wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet.equals(wahlvorschlag)) {
        var wahlvorschlagStimmzettelPosition = wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet.getGruppeImGebiet().getVorgegebenePosition();

        if (wahlvorschlagStimmzettelPosition != null) {
          stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet.put(wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet,
              wahlvorschlagStimmzettelPosition);
        }

        if (wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet.isDirektkandidat()) {
          var listeInGebietshierarchieUebergeordnet = bestimmeListeImGleichenOderHoeremGebiet(entityAufStimmzettelDaten,
              wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet);

          if (listeInGebietshierarchieUebergeordnet != null) {
            if (stimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet.containsKey(listeInGebietshierarchieUebergeordnet)) {
              var listePosition = stimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet.get(listeInGebietshierarchieUebergeordnet);

              stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet.put(wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet,
                  listePosition);
            } else {
              var listePosition = listeInGebietshierarchieUebergeordnet.getStimmzettelPosition(gebietBetrachtet, entityAufStimmzettelDaten).getPosition();

              if (listePosition != null) {
                stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet.put(wahlvorschlagAufStimmzettelInGebietshierarchieUebergeordnet,
                    listePosition);
              }
            }
          }
        }
      }
    }

    Set<Long> stimmzettelPositionenVergeben = new HashSet<>();
    stimmzettelPositionenVergeben.addAll(stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet.values());
    stimmzettelPositionenVergeben.addAll(stimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet.values());

    List<Wahlvorschlag> wahlvorschlaegeOhneStimmzettelPositionSortedByDefault = new ArrayList<>();
    wahlvorschlaegeOhneStimmzettelPositionSortedByDefault.addAll(wahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault);
    wahlvorschlaegeOhneStimmzettelPositionSortedByDefault.removeAll(stimmzettelPositionVergebenPerWahlvorschlagInGebietshierarchieUebergeordnet.keySet());
    wahlvorschlaegeOhneStimmzettelPositionSortedByDefault.removeAll(stimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet.keySet());

    long berechnetePosition = 0;

    for (var wahlvorschlagOhneStimmzettelPosition : wahlvorschlaegeOhneStimmzettelPositionSortedByDefault) {
      berechnetePosition++;

      while (stimmzettelPositionenVergeben.contains(berechnetePosition)) {
        berechnetePosition++;
      }

      if (wahlvorschlagOhneStimmzettelPosition.equals(wahlvorschlag)) {
        var result = createWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag, berechnetePosition);
        info("Betrachteter Wahlvorschlag hat KEINE vorgegebene Position und wird EINSORTIERT", result, wahlvorschlag);

        return result;
      }
    }

    LOG.error(
        "Kann nicht passieren, dieser Wahlvorschlag muss in der übergebenen Liste vorhanden sein! [gebiet = " + gebiet + ", wahlvorschlag = " + wahlvorschlag
            + ", wahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnet = " + wahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault
            + ", wahlvorschlaegeOhneStimmzettelPosition = " + wahlvorschlaegeOhneStimmzettelPositionSortedByDefault + "]");

    return emptyWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag);
  }

  protected boolean isListenPositionenInGebietsHierachieVariabel(Wahl wahl) {
    return true; // TODO ELECT-13235 wie bisher => variabel
  }

  private boolean existsGruppenUnabhaengigListenInGebietshierarchieUntergeordnet(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Gebiet gebiet, Wahlvorschlag wahlvorschlag) {
    var wahlvorschlaegeInWahl = collectWahlvorschlaegeInWahl(entityAufStimmzettelDaten, wahlvorschlag);

    for (var another : wahlvorschlaegeInWahl) {
      if (another.isListe() && !another.getGebiet().equals(gebiet) && another.getGebiet().isUntergebietVon(gebiet)) {
        return true;
      }
    }

    return false;
  }

  @Nullable
  private Liste bestimmeListeImGleichenOderHoeremGebiet(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Wahlvorschlag template) {
    return collectWahlvorschlaegeInWahl(entityAufStimmzettelDaten, template).stream() //
        .filter(wahlvorschlag -> wahlvorschlag.isListe() && wahlvorschlag.getGruppe().equals(template.getGruppe()) && !wahlvorschlag.equals(template)) //
        .filter(wahlvorschlag -> template.getGebiet().isEqualOderUntergebietVon(wahlvorschlag.getGebiet())) //
        .findFirst() //
        .map(wahlvorschlag -> wahlvorschlag.castTo(Liste.class).getEntity()) //
        .orElse(null);
  }

  private Map<Liste, Long> collectStimmzettelPositionVergebenPerListeInGebietshierarchieUebergeordnet(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Gebiet gebiet, Wahlvorschlag wahlvorschlag) {
    Map<Liste, Long> result = new HashMap<>();

    if (wahlvorschlag.isDirektkandidat()) {
      for (var another : collectWahlvorschlaegeInWahl(entityAufStimmzettelDaten, wahlvorschlag)) {
        if (another.isListe() && another.getGebiet().isInGebietshierarchieUebergeordnetesGebietVon(wahlvorschlag.getGebiet())) {
          var stimmzettelPosition = another.getStimmzettelPosition(gebiet, entityAufStimmzettelDaten).getPosition();

          if (stimmzettelPosition != null) {
            result.put(another.castTo(Liste.class).getEntity(), stimmzettelPosition);
          }
        }
      }
    }

    return result;
  }

  private List<Wahlvorschlag> collectWahlvorschlaegeAufStimmzettelInGebietshierarchieUebergeordnetSortedByDefault(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Gebiet gebiet,
                                                                                                                  Wahlvorschlag wahlvorschlag) {
    var zulassungPruefer = entityAufStimmzettelDaten.getZulassungPruefer();

    List<Wahlvorschlag> result = new ArrayList<>(
        collectWahlvorschlaegeInGebietshierarchieUebergeordnet(entityAufStimmzettelDaten, gebiet, wahlvorschlag).stream() //
            .filter(another -> isWahlvorschlagAufStimmzettel(zulassungPruefer, another)) //
            .collect(toList()));

    // WASDE-1184 alle laden...
    var wahlvorschlaegeInWahl = collectWahlvorschlaegeInWahl(entityAufStimmzettelDaten, wahlvorschlag);

    if (wahlvorschlaegeInWahl == null) {
      wahlvorschlaegeInWahl = Collections.emptyList();
    }

    result.sort(new WahlvorschlagReihenfolgeVorsortierenComparator(entityAufStimmzettelDaten.getGruppeImGebietReihenfolgeVorsortierer(), wahlvorschlaegeInWahl,
        entityAufStimmzettelDaten.getStimmenPoolFactory(), gebiet));

    return result;
  }

  private boolean isWahlvorschlagAufStimmzettel(ZulassungPruefer zulassungPruefer, Wahlvorschlag wahlvorschlag) {
    // Nur Wahlvorschläge, die nicht bereits abgelehnt sind /*und auch noch potentiell zugelassen werden können*/
    return !wahlvorschlag.isNichtZugelassenImSinneAbgelehnt()/* && zulassungPruefer.isZulassenErlaubt(wahlvorschlag) */;
  }

  private Collection<Wahlvorschlag> collectWahlvorschlaegeInWahl(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Wahlvorschlag wahlvorschlag) {
    return entityAufStimmzettelDaten.collectWahlvorschlaegeInWahl(wahlvorschlag.getWahl());
  }

  private Collection<Wahlvorschlag> collectWahlvorschlaegeInGebietshierarchieUebergeordnet(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Gebiet gebiet, Wahlvorschlag wahlvorschlag) {
    var wahlvorschlaegePerGebiet = entityAufStimmzettelDaten.collectWahlvorschlaegeInWahlPerGebiet(wahlvorschlag.getWahl());

    if (wahlvorschlag.isDirektkandidat()) {
      return wahlvorschlaegePerGebiet.get(wahlvorschlag.getGebiet());
    }

    Set<Wahlvorschlag> result = new HashSet<>();

    var gebietInGebietshierarchieUebergeordnet = gebiet;

    do {
      var wahlvorschlaegeImGebiet = wahlvorschlaegePerGebiet.get(gebietInGebietshierarchieUebergeordnet);

      if (wahlvorschlaegeImGebiet != null) {
        for (var wahlvorschlagImGebiet : wahlvorschlaegeImGebiet) {
          if (wahlvorschlagImGebiet.isListe()) {
            result.add(wahlvorschlagImGebiet);
          }
        }
      }

      gebietInGebietshierarchieUebergeordnet = gebietInGebietshierarchieUebergeordnet.getUebergeordnetesGebiet();
    } while (gebietInGebietshierarchieUebergeordnet != null);

    return result;
  }

  private WahlvorschlagStimmzettelPosition emptyWahlvorschlagStimmzettelPosition() {
    return WahlvorschlagStimmzettelPosition.EMPTY;
  }

  private WahlvorschlagStimmzettelPosition emptyWahlvorschlagStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Wahlvorschlag wahlvorschlag) {
    return createWahlvorschlagStimmzettelPosition(entityAufStimmzettelDaten, wahlvorschlag, null); // ELECTMUC-2790
  }

  private WahlvorschlagStimmzettelPosition createWahlvorschlagStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Wahlvorschlag wahlvorschlag, Long berechnetePosition) {
    if (wahlvorschlag != null) {
      var gruppeImGebiet = wahlvorschlag.getGruppeImGebiet();

      if (gruppeImGebiet != null) {
        return new WahlvorschlagStimmzettelPosition(
            wahlvorschlag.getPk(),
            gruppeImGebiet,
            berechnetePosition,
            new WahlvorschlagStimmzettelPositionFormatter(
                formatWahlvorschlagPositionFct(wahlvorschlag),
                formatWahlvorschlagKennzahlFct(wahlvorschlag),
                formatWahlvorschlagKennzahlAufStimmzettelFct(wahlvorschlag),
                isWaehlerBenannteWahlvorschlaegeErfassbar(entityAufStimmzettelDaten, wahlvorschlag)
            ));
      }
    }

    return emptyWahlvorschlagStimmzettelPosition();
  }

  public BiFunction<WahlvorschlagStimmzettelPositionFormatterKonfiguration, Long, String> formatWahlvorschlagPositionFct(Wahlvorschlag wahlvorschlag) {
    return this::formatWahlvorschlagPositionDefault;
  }

  protected String formatWahlvorschlagPositionDefault(WahlvorschlagStimmzettelPositionFormatterKonfiguration formatterKonfiguration, Long wahlvorschlagPosition) {
    if (wahlvorschlagPosition != null) {
      return String.format("%d", wahlvorschlagPosition);
    }

    return "";
  }

  public BiFunction<WahlvorschlagStimmzettelPositionFormatterKonfiguration, Long, String> formatWahlvorschlagKennzahlFct(Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag.isListe()) {
      return this::formatWahlvorschlagKennzahlDefaultListenwahl;
    }

    return this::formatWahlvorschlagKennzahlDefault;
  }

  public BiFunction<WahlvorschlagStimmzettelPositionFormatterKonfiguration, Long, String> formatWahlvorschlagKennzahlAufStimmzettelFct(Wahlvorschlag wahlvorschlag) {
    return formatWahlvorschlagKennzahlFct(wahlvorschlag);
  }

  protected String formatWahlvorschlagKennzahlDefaultListenwahl(WahlvorschlagStimmzettelPositionFormatterKonfiguration formatterKonfiguration, Long listePosition) {
    if (listePosition != null) {
      if (formatterKonfiguration.isWaehlerBenannteWahlvorschlaegeErfassbar()) {// ELECTKOM-871
        return String.format("%02d", listePosition);
      }

      return String.format("%d00", listePosition);
    }

    return formatWahlvorschlagKennzahlDefault(formatterKonfiguration, listePosition);
  }

  protected String formatWahlvorschlagKennzahlDefault(WahlvorschlagStimmzettelPositionFormatterKonfiguration formatterKonfiguration, Long wahlvorschlagPosition) {
    if (wahlvorschlagPosition != null) {
      return String.format("%d", wahlvorschlagPosition);
    }

    return "";
  }

  public ListenkandidaturStimmzettelPosition calculateListenkandidaturStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, @Nullable Gebiet gebiet,
                                                                                          @Nullable Listenkandidatur listenkandidatur) {
    if (listenkandidatur == null) {
      return emptyListenkandidaturStimmzettelPosition();
    }

    var liste = listenkandidatur.getListe();
    var listenkandidaturenAufStimmzettelSortedByDefault = collectListenkandidaturenAufStimmzettelSortedByDefault(entityAufStimmzettelDaten,
        liste);

    if (!listenkandidaturenAufStimmzettelSortedByDefault.contains(listenkandidatur)) {
      return emptyListenkandidaturStimmzettelPosition(entityAufStimmzettelDaten, liste, listenkandidatur);
    }

    var gebietBetrachtet = gebiet;

    if (gebietBetrachtet == null) {
      gebietBetrachtet = listenkandidatur.getGebiet();
    }

    return calculateListenkandidaturStimmzettelPosition(entityAufStimmzettelDaten, gebietBetrachtet, liste, listenkandidatur,
        listenkandidaturenAufStimmzettelSortedByDefault);
  }

  protected List<Listenkandidatur> collectListenkandidaturenAufStimmzettelSortedByDefault(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Liste liste) {
    var listenkandidaturenAufListe = liste.getListenkandidaturen();

    return collectListenkandidaturenAufStimmzettel(entityAufStimmzettelDaten, listenkandidaturenAufListe,
        ListenkandidaturByRelativePositionComparator.ASCENDING);
  }

  public List<Listenkandidatur> collectListenkandidaturenAufStimmzettel(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Collection<Listenkandidatur> listenkandidaturen,
                                                                        Comparator<Listenkandidatur> comparator) {
    var zulassungPruefer = entityAufStimmzettelDaten.getZulassungPruefer();

    return listenkandidaturen.stream() //
        .filter(listenkandidatur -> isListenkandidaturAufStimmzettel(zulassungPruefer, listenkandidatur)) //
        .sorted(comparator) //
        .collect(toList());
  }

  private boolean isListenkandidaturAufStimmzettel(ZulassungPruefer zulassungPruefer, Listenkandidatur listenkandidatur) {
    return listenkandidatur.isAufStimmzettel(); // ELECT-8948, ELECT-9421
  }

  protected boolean isListenkandidaturStimmzettelPositionNennungenIncluded(Wahl wahl) {
    return false;
  }

  private ListenkandidaturStimmzettelPosition calculateListenkandidaturStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Gebiet gebiet, Liste liste,
                                                                                           Listenkandidatur listenkandidatur, List<Listenkandidatur> listenkandidaturenAufStimmzettelSortedByDefault) {
    long berechnetePositionFortlaufend = 0;

    long berechnetePositionNennungenIncluded = 0;
    long berechnetePositionNennungenExcluded = 0;

    for (var listenkandidaturAufStimmzettel : listenkandidaturenAufStimmzettelSortedByDefault) {
      berechnetePositionFortlaufend += 1L;

      var berechnetePositionNennungenIncludedIncremented = berechnetePositionNennungenIncluded + 1;
      var berechnetePositionNennungenExcludedIncremented = berechnetePositionNennungenExcluded + 1;

      var vorgegebenePosition = listenkandidaturAufStimmzettel.getVorgegebenePosition();

      var naechstePositionNennungenIncluded = fixNullToDefault(vorgegebenePosition, berechnetePositionNennungenIncludedIncremented);
      var naechstePositionNennungenExcluded = fixNullToDefault(vorgegebenePosition, berechnetePositionNennungenExcludedIncremented);

      berechnetePositionNennungenIncluded = Math.max(naechstePositionNennungenIncluded, berechnetePositionNennungenIncludedIncremented);
      berechnetePositionNennungenExcluded = naechstePositionNennungenExcluded; // Wenn Position vorgegeben, dann immer verwenden!!!

      if (listenkandidaturAufStimmzettel.equals(listenkandidatur)) {
        break; // Done!
      }

      var anzahlNennungen = fixNullToDefault(listenkandidaturAufStimmzettel.getAnzahlNennungen(), 1L);

      berechnetePositionNennungenIncluded += Math.max(1L, anzahlNennungen) - 1;
    }

    return createListenkandidaturStimmzettelPosition(entityAufStimmzettelDaten, liste, listenkandidatur, berechnetePositionFortlaufend,
        berechnetePositionNennungenExcluded,
        berechnetePositionNennungenIncluded);
  }

  private ListenkandidaturStimmzettelPosition emptyListenkandidaturStimmzettelPosition() {
    return ListenkandidaturStimmzettelPosition.EMPTY;
  }

  private ListenkandidaturStimmzettelPosition emptyListenkandidaturStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Liste liste, Listenkandidatur listenkandidatur) {
    return createListenkandidaturStimmzettelPosition(entityAufStimmzettelDaten, liste, listenkandidatur, null, null, null); // ELECTMUC-2790
  }

  protected ListenkandidaturStimmzettelPosition createListenkandidaturStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Liste liste, Listenkandidatur listenkandidatur,
                                                                                          Long berechnetePositionFortlaufend, Long berechnetePositionNennungenExcluded, Long berechnetePositionNennungenIncluded) {
    var berechnetePosition = berechnetePositionNennungenExcluded;

    if (isListenkandidaturStimmzettelPositionNennungenIncluded(liste.getWahl())) { // ELECT-8784
      berechnetePosition = berechnetePositionNennungenIncluded;
    }

    return createListenkandidaturStimmzettelPosition(entityAufStimmzettelDaten, liste, listenkandidatur, berechnetePosition, berechnetePositionFortlaufend,
        berechnetePositionNennungenExcluded,
        berechnetePositionNennungenIncluded);
  }

  protected ListenkandidaturStimmzettelPosition createListenkandidaturStimmzettelPosition(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Liste liste, Listenkandidatur listenkandidatur,
                                                                                          Long berechnetePosition, Long berechnetePositionFortlaufend, Long berechnetePositionNennungenExcluded, Long berechnetePositionNennungenIncluded) {
    return new ListenkandidaturStimmzettelPosition(
        listenkandidatur,
        berechnetePosition,
        fixNullToDefault(listenkandidatur.getAnzahlNennungen(), 1L),
        berechnetePositionFortlaufend,
        berechnetePositionNennungenExcluded, berechnetePositionNennungenIncluded,
        new ListenkandidaturStimmzettelPositionFormatter(
            formatListenkandidaturPositionFct(listenkandidatur),
            formatListenkandidaturKennzahlFct(listenkandidatur),
            formatListenkandidaturKennzahlAufStimmzettelFct(listenkandidatur),
            isWaehlerBenannteWahlvorschlaegeErfassbar(entityAufStimmzettelDaten, liste)));
  }

  public TriFunction<ListenkandidaturStimmzettelPositionFormatterKonfiguration, Long, Long, String> formatListenkandidaturPositionFct(Listenkandidatur listenkandidatur) {
    return this::formatListenkandidaturPositionDefault;
  }

  protected String formatListenkandidaturPositionDefault(ListenkandidaturStimmzettelPositionFormatterKonfiguration formatterKonfiguration, @Nullable Long listePosition,
                                                         Long listenkandidaturPosition) {
    if (listenkandidaturPosition != null) {
      return String.format("%d", listenkandidaturPosition);
    }

    return "";
  }

  public TriFunction<ListenkandidaturStimmzettelPositionFormatterKonfiguration, Long, Long, String> formatListenkandidaturKennzahlFct(Listenkandidatur listenkandidatur) {
    return this::formatListenkandidaturKennzahlDefault;
  }

  public TriFunction<ListenkandidaturStimmzettelPositionFormatterKonfiguration, Long, Long, String> formatListenkandidaturKennzahlAufStimmzettelFct(Listenkandidatur listenkandidatur) {
    return formatListenkandidaturKennzahlFct(listenkandidatur);
  }

  protected String formatListenkandidaturKennzahlDefault(ListenkandidaturStimmzettelPositionFormatterKonfiguration formatterKonfiguration, @Nullable Long listePosition,
                                                         Long listenkandidaturPosition) {
    if (listenkandidaturPosition != null) {
      if (listePosition == null || formatterKonfiguration.isWaehlerBenannteListenkandidaturenErfassbar()) { // ELECTKOM-871
        return String.format("%02d", listenkandidaturPosition);
      }

      return String.format("%d%02d", listePosition, listenkandidaturPosition);
    }

    return formatListenkandidaturKennzahlLeer(formatterKonfiguration, listePosition, listenkandidaturPosition);
  }

  protected String formatListenkandidaturKennzahlLeer(ListenkandidaturStimmzettelPositionFormatterKonfiguration formatterKonfiguration, @Nullable Long listePosition, Long listenkandidaturPosition) {
    return "";
  }

  private boolean isWaehlerBenannteWahlvorschlaegeErfassbar(EntityAufStimmzettelDaten entityAufStimmzettelDaten, Wahlvorschlag wahlvorschlag) {
    if (wahlvorschlag != null) {
      var wahlvorschlagGebiet = wahlvorschlag.getGebiet();

      return getAnzahlStimmenProWaehlerBestimmer().isWaehlerBenannteWahlvorschlaegeErfassbar(wahlvorschlagGebiet,
          () -> entityAufStimmzettelDaten.collectWahlvorschlaegeInWahlPerGebiet(wahlvorschlag.getWahl()).getOrDefault(wahlvorschlagGebiet, new ArrayList<>()));
    }

    return false;
  }

  protected AnzahlStimmenProWaehlerBestimmer getAnzahlStimmenProWaehlerBestimmer() {
    return anzahlStimmenProWaehlerBestimmer;
  }

  private long fixNullToDefault(Long value, long defaultValue) {
    if (value == null) {
      return defaultValue;
    }

    return value;
  }

  private void info(String cause, WahlvorschlagStimmzettelPosition position, @Nullable Wahlvorschlag wahlvorschlag) {
    // String format = cause + " [position = {}, kennwort = {}, wahlvorschlag = {}]";

    // LOG.info(format, position == null || !position.hasPosition() ? "KEINE" : position.getPosition(), wahlvorschlag == null ? "" :
    // wahlvorschlag.getKurznameOderKennwort(), wahlvorschlag);
  }
}
