
package de.ivu.elect.dokumente.ergebnis.zusammenstellung;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.function.Function;

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

import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.util.ListUtil;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungDirektkandidatur;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungListe;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungListenkandidatur;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungZeile;

public class ErgebnisTabellarischAufteilung {

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

  public static final Stimmenart STIMMENART_KOPFZAHLEN = Stimmenart.GESAMT;

  public static final Stimmenart STIMMENART_STIMMZETTEL = Stimmenart.GESAMT_EINGENOMMEN;

  public static final Stimmenart STIMMENART_DIREKTSTIMMEN = Stimmenart.DIREKTSTIMME;

  public static final Stimmenart STIMMENART_LISTENSTIMMEN = Stimmenart.LISTENSTIMME;

  public static final Stimmenart STIMMENART_GESAMTSTIMMEN = Stimmenart.GESAMT;

  public static final int NUMMER_HORIZONTAL_INITIAL = 1;

  public static final int NUMMER_VERTIKAL_INITIAL = 1;

  public ErgebnisTabellarischAufteilung() {
    super();
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurKopfzahlen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetter(context, collectAufteilungHorizontal(context, STIMMENART_KOPFZAHLEN, tabelle, this::collectAufteilungHorizontalNurKopfzahlen));
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurDirektstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetter(context, collectAufteilungHorizontal(context, STIMMENART_DIREKTSTIMMEN, tabelle, this::collectAufteilungHorizontalNurDirektstimmen));
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurDirektstimmenStimmzettel(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetterNurStimmenStimmzettel(context, STIMMENART_DIREKTSTIMMEN, tabelle, this::collectAufteilungHorizontalNurDirektstimmen);
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurListenstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetter(context, collectAufteilungHorizontal(context, STIMMENART_LISTENSTIMMEN, tabelle, this::collectAufteilungHorizontalNurListenstimmen));
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurListenstimmenStimmzettel(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetterNurStimmenStimmzettel(context, STIMMENART_LISTENSTIMMEN, tabelle, this::collectAufteilungHorizontalNurListenstimmen);
  }

  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurListenkandidaturstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetter(context, collectAufteilungHorizontal(context, STIMMENART_LISTENSTIMMEN, tabelle, this::collectAufteilungHorizontalNurListenkandidaturstimmen));
  }

  private List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurStimmenStimmzettel(ErgebnisTabellarischContext context, Stimmenart stimmenart, ErgebnisTabellarischTabelle tabelle,
      BiFunction<ErgebnisTabellarischContext, ErgebnisTabellarischZeile, Map<Integer, List<ErgebnisTabellarischZeile>>> zeilenAufSeiteMapper) {
    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontalNurStimmen = collectAufteilungHorizontal(context, stimmenart, tabelle, zeilenAufSeiteMapper);

    // Für die Stimmzettel nochmals genauso viele Spalten wie für die Stimmen erzeugen...
    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontalNurStimmzettel = collectAufteilungHorizontal(context, STIMMENART_STIMMZETTEL, tabelle, zeilenAufSeiteMapper);

    if (context.isErsteSeiteNurKopfzahlen() && !aufteilungHorizontalNurStimmzettel.isEmpty()) {
      aufteilungHorizontalNurStimmzettel = aufteilungHorizontalNurStimmzettel.subList(1, aufteilungHorizontalNurStimmzettel.size()); // Blatt mit Kopfzahlen ist schon bei den Stimmen enthalten...
    }

    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontal = new ArrayList<>();

    int nummerHorizontalMax = aufteilungHorizontalNurStimmen.size() + aufteilungHorizontalNurStimmzettel.size();
    int nummerHorizontal = NUMMER_HORIZONTAL_INITIAL;

    for (ErgebnisTabellarischAufteilungHorizontal entry : aufteilungHorizontalNurStimmen) {
      aufteilungHorizontal.add(new ErgebnisTabellarischAufteilungHorizontal(nummerHorizontal++, nummerHorizontalMax, entry));
    }

    for (ErgebnisTabellarischAufteilungHorizontal entry : aufteilungHorizontalNurStimmzettel) {
      aufteilungHorizontal.add(new ErgebnisTabellarischAufteilungHorizontal(nummerHorizontal++, nummerHorizontalMax, entry));
    }

    return collectBlaetter(context, aufteilungHorizontal);
  }

  /**
   * @return Direktstimmen + Listenstimmen auf gemeinsamen Blatt (NICHT GETRENNT)...
   */
  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterNurGesamtstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    return collectBlaetter(context, collectAufteilungHorizontal(context, STIMMENART_GESAMTSTIMMEN, tabelle, this::collectAufteilungHorizontalNurGesamtstimmen));
  }

  /**
   * @return Direktstimmen, dann Listenstimmen auf hintereinanderfolgenden Blättern (GETRENNT)...
   */
  public List<ErgebnisTabellarischAufteilungBlatt> collectBlaetterDirektstimmenListenstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontalNurDirektstimmen = collectAufteilungHorizontal(context, STIMMENART_DIREKTSTIMMEN, tabelle,
        this::collectAufteilungHorizontalNurDirektstimmen);

    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontalNurListenstimmen = collectAufteilungHorizontal(context, STIMMENART_LISTENSTIMMEN, tabelle,
        this::collectAufteilungHorizontalNurListenstimmen);

    if (context.isErsteSeiteNurKopfzahlen() && !aufteilungHorizontalNurListenstimmen.isEmpty()) {
      aufteilungHorizontalNurListenstimmen = aufteilungHorizontalNurListenstimmen.subList(1, aufteilungHorizontalNurListenstimmen.size()); // Blatt mit Kopfzahlen ist schon bei den Direktstimmen enthalten...
    }

    List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontal = new ArrayList<>();

    int nummerHorizontalMax = aufteilungHorizontalNurDirektstimmen.size() + aufteilungHorizontalNurListenstimmen.size();
    int nummerHorizontal = NUMMER_HORIZONTAL_INITIAL;

    for (ErgebnisTabellarischAufteilungHorizontal entry : aufteilungHorizontalNurDirektstimmen) {
      aufteilungHorizontal.add(new ErgebnisTabellarischAufteilungHorizontal(nummerHorizontal++, nummerHorizontalMax, entry));
    }

    for (ErgebnisTabellarischAufteilungHorizontal entry : aufteilungHorizontalNurListenstimmen) {
      aufteilungHorizontal.add(new ErgebnisTabellarischAufteilungHorizontal(nummerHorizontal++, nummerHorizontalMax, entry));
    }

    return collectBlaetter(context, aufteilungHorizontal);
  }

  private List<ErgebnisTabellarischAufteilungBlatt> collectBlaetter(ErgebnisTabellarischContext context, List<ErgebnisTabellarischAufteilungHorizontal> aufteilungHorizontal) {
    List<ErgebnisTabellarischAufteilungBlatt> result = new ArrayList<>();

    Stimmenart stimmenartBisher = null;

    /*
     * "Horizontal" meint die Aufteilung einer Tabelle, falls die Tabelle mehr Spalten enthält als auf ein (ODT-) Blatt passen...
     */
    for (ErgebnisTabellarischAufteilungHorizontal horizontal : aufteilungHorizontal) {
      int nummerHorizontal = horizontal.getNummer();
      int nummerHorizontalMax = horizontal.getNummerMax();

      Stimmenart stimmenart = horizontal.getStimmenart();
      boolean erstesBlattMitStimmenart = !Objects.equals(stimmenart, stimmenartBisher);

      /*
       * "Vertikal" meint die Aufteilung einer Tabelle, falls die Tabelle mehr Zeilen enthält als auf ein (ODT-) Blatt passen...
       */
      for (ErgebnisTabellarischAufteilungVertikal vertikal : collectAufteilungVertikal(context, horizontal.getTabelle())) {
        int nummerVertikal = vertikal.getNummer();
        int nummerVertikalMax = vertikal.getNummerMax();

        result.add(new ErgebnisTabellarischAufteilungBlatt(nummerHorizontal, nummerHorizontalMax, nummerVertikal, nummerVertikalMax, erstesBlattMitStimmenart, stimmenart, vertikal.getTabelle(),
            vertikal.getZeileUebertragsummen()));
      }

      stimmenartBisher = stimmenart;
    }

    // Sortierung der (ODT-) Blätter -> SEITE 1/TEIL 1, SEITE 2/TEIL 1, ..., SEITE 1/TEIL 2, SEITE 2/TEIL 2, ..., SEITE 1/TEIL 3, SEITE 2/TEIL 3, ...
    result.sort(Comparator.comparing(ErgebnisTabellarischAufteilungBlatt::getNummerierung));

    return result;
  }

  private List<ErgebnisTabellarischAufteilungVertikal> collectAufteilungVertikal(ErgebnisTabellarischContext context, ErgebnisTabellarischTabelle tabelle) {
    List<ErgebnisTabellarischAufteilungVertikal> result = new ArrayList<>();

    int anzahlZeilenMaxAufErsterSeite = context.getAnzahlZeilenMaxAufErsterSeite();
    int anzahlZeilenMaxAufWeitererSeite = context.getAnzahlZeilenMaxAufWeitererSeite();

    List<ErgebnisTabellarischZeile> zeilen = tabelle.getZeilen();
    int anzahlZeilen = zeilen.size();

    List<ErgebnisTabellarischZeile> zeilenAufErsterSeite = new ArrayList<>();
    List<List<ErgebnisTabellarischZeile>> groupsOfZeilenAufWeitererSeite = new ArrayList<>();

    if (anzahlZeilen > 0) {
      zeilenAufErsterSeite.addAll(subList(zeilen, anzahlZeilenMaxAufErsterSeite));

      if (anzahlZeilen > anzahlZeilenMaxAufErsterSeite) {
        List<ErgebnisTabellarischZeile> zeilenAufWeiterenTeilen = subList(zeilen, anzahlZeilenMaxAufErsterSeite, anzahlZeilen);

        groupsOfZeilenAufWeitererSeite.addAll(groupsOfSubList(zeilenAufWeiterenTeilen, anzahlZeilenMaxAufWeitererSeite));
      }
    }

    if (!zeilenAufErsterSeite.isEmpty()) {
      int nummerVertikalMax = 1 /* == Anzahl erste Seite */ + groupsOfZeilenAufWeitererSeite.size() /* == Anzahl weitere Seiten */;

      int nummerVertikalAufErsterSeite = NUMMER_VERTIKAL_INITIAL;
      ErgebnisTabellarischAufteilungVertikal ersteSeite = createAufteilungVertikal(nummerVertikalAufErsterSeite, nummerVertikalMax, tabelle, zeilenAufErsterSeite, Optional.empty());
      result.add(ersteSeite);

      Optional<ErgebnisTabellarischZeile> zeileUebertragsummen = ersteSeite.getZeileUebertragsummen();

      Collection<ErgebnisTabellarischZeile> zeilenAufSeiten = new ArrayList<>();
      zeilenAufSeiten.addAll(zeilenAufErsterSeite);

      for (int i = 0; i < groupsOfZeilenAufWeitererSeite.size(); i++) {
        int nummerVertikalAufWeitererSeite = nummerVertikalAufErsterSeite + 1 + i;

        List<ErgebnisTabellarischZeile> zeilenAufWeitererSeite = groupsOfZeilenAufWeitererSeite.get(i);
        zeilenAufSeiten.addAll(zeilenAufWeitererSeite);

        if (zeileUebertragsummen.isPresent()) {
          ErgebnisTabellarischZeile template = zeileUebertragsummen.get();

          zeileUebertragsummen = ErgebnisTabellarischZeile.sum(template.getDatengebiet(), template.getStimmabgabeart(), template.getZwischensummen(), true, zeilenAufSeiten);
        }

        result.add(createAufteilungVertikal(nummerVertikalAufWeitererSeite, nummerVertikalMax, tabelle, zeilenAufWeitererSeite, zeileUebertragsummen));
      }
    }

    return result;
  }

  private List<ErgebnisTabellarischAufteilungHorizontal> collectAufteilungHorizontal(ErgebnisTabellarischContext context, Stimmenart stimmenart, ErgebnisTabellarischTabelle tabelle,
      BiFunction<ErgebnisTabellarischContext, ErgebnisTabellarischZeile, Map<Integer, List<ErgebnisTabellarischZeile>>> zeilenAufSeiteMapper) {
    List<ErgebnisTabellarischAufteilungHorizontal> result = new ArrayList<>();

    SortedMap<Integer, List<ErgebnisTabellarischZeile>> zeilenAufSeitePerNummerHorizontal = new TreeMap<>();

    for (ErgebnisTabellarischZeile zeile : tabelle.getZeilen()) {
      for (Entry<Integer, List<ErgebnisTabellarischZeile>> entry : zeilenAufSeiteMapper.apply(context, zeile).entrySet()) {
        zeilenAufSeitePerNummerHorizontal.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).addAll(entry.getValue());
      }
    }

    int nummerHorizontalMax = zeilenAufSeitePerNummerHorizontal.size();
    int nummerHorizontal = NUMMER_HORIZONTAL_INITIAL;

    for (Entry<Integer, List<ErgebnisTabellarischZeile>> entry : zeilenAufSeitePerNummerHorizontal.entrySet()) {
      Stimmenart stimmenartAufSeite = stimmenart;

      if (context.isErsteSeiteNurKopfzahlen() && nummerHorizontal == NUMMER_HORIZONTAL_INITIAL) {
        stimmenartAufSeite = STIMMENART_KOPFZAHLEN;
      }

      result.add(createAufteilungHorizontal(nummerHorizontal++, nummerHorizontalMax, stimmenartAufSeite, tabelle, entry.getValue()));
    }

    return result;
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalNurKopfzahlen(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile) {
    Map<Integer, List<ErgebnisTabellarischZeile>> result = new HashMap<>();
    result.put(NUMMER_HORIZONTAL_INITIAL, singletonList(createZeile(zeile, emptyList(), emptyList()))); // Keine Wahlvorschläge, nur Kopfzahlen...

    return result;
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalNurDirektstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile) {
    return collectAufteilungHorizontalWahlvorschlaege(context, zeile, emptyList(), zeile.getDirektkandidaturen());
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalNurListenstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile) {
    return collectAufteilungHorizontalWahlvorschlaege(context, zeile, zeile.getListen(), emptyList());
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalNurGesamtstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile) {
    return collectAufteilungHorizontalWahlvorschlaege(context, zeile, zeile.getListen(), zeile.getDirektkandidaturen());
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalWahlvorschlaege(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile,
      List<ErgebnisZusammenstellungListe> listen, List<ErgebnisZusammenstellungDirektkandidatur> direktkandidaturen) {
    Map<Integer, List<ErgebnisTabellarischZeile>> result = new HashMap<>();

    int nummerHorizontalOffset = 0;

    if (context.isErsteSeiteNurKopfzahlen()) {
      result.put(NUMMER_HORIZONTAL_INITIAL, singletonList(createZeile(zeile, emptyList(), emptyList()))); // Keine Wahlvorschläge, nur Kopfzahlen...

      nummerHorizontalOffset = NUMMER_HORIZONTAL_INITIAL;
    }

    Map<Long, ErgebnisZusammenstellungListe> listePerStimmzettelPosition = listen.stream() //
        .filter(ErgebnisZusammenstellungListe::hasStimmzettelPosition) //
        .collect(toMap(wahlvorschlag -> wahlvorschlag.getStimmzettelPosition().getPosition(), Function.identity()));

    Map<Long, ErgebnisZusammenstellungDirektkandidatur> direktkandidaturPerStimmzettelPosition = direktkandidaturen.stream() //
        .filter(ErgebnisZusammenstellungDirektkandidatur::hasStimmzettelPosition) //
        .collect(toMap(wahlvorschlag -> wahlvorschlag.getStimmzettelPosition().getPosition(), Function.identity()));

    SortedSet<Long> stimmzettelPositionenInZeile = new TreeSet<>();
    stimmzettelPositionenInZeile.addAll(listePerStimmzettelPosition.keySet());
    stimmzettelPositionenInZeile.addAll(direktkandidaturPerStimmzettelPosition.keySet());

    int anzahlStimmzettelPositionenInZeile = stimmzettelPositionenInZeile.size();

    int anzahlStimmzettelPositionenMaxAufErsterSeite = context.getAnzahlWahlvorschlaegeMaxAufErsterSeite();
    int anzahlStimmzettelPositionenMaxAufWeitererSeite = context.getAnzahlWahlvorschlaegeMaxAufWeitererSeite();
    int anzahlStimmzettelPositionenMaxAufLetzterSeite = context.getAnzahlWahlvorschlaegeMaxAufLetzterSeite();

    List<List<Long>> groupsOfStimmzettelPositionenAufSeite = new ArrayList<>();

    if (anzahlStimmzettelPositionenInZeile > 0) {
      List<Long> stimmzettelPositionenAufErsterSeite = subList(stimmzettelPositionenInZeile, anzahlStimmzettelPositionenMaxAufErsterSeite);
      groupsOfStimmzettelPositionenAufSeite.add(stimmzettelPositionenAufErsterSeite);

      if (anzahlStimmzettelPositionenInZeile > anzahlStimmzettelPositionenMaxAufErsterSeite) {
        List<Long> stimmzettelPositionenAufWeiterenSeiten = subList(stimmzettelPositionenInZeile, anzahlStimmzettelPositionenMaxAufErsterSeite, anzahlStimmzettelPositionenInZeile);
        groupsOfStimmzettelPositionenAufSeite.addAll(groupsOfSubList(stimmzettelPositionenAufWeiterenSeiten, anzahlStimmzettelPositionenMaxAufWeitererSeite));
      }

      List<Long> stimmzettelPositionenAufLetzterSeite = groupsOfStimmzettelPositionenAufSeite.get(groupsOfStimmzettelPositionenAufSeite.size() - 1);
      int anzahlStimmzettelPositionenAufLetzterSeite = stimmzettelPositionenAufLetzterSeite.size();

      if (anzahlStimmzettelPositionenAufLetzterSeite > anzahlStimmzettelPositionenMaxAufLetzterSeite) {
        groupsOfStimmzettelPositionenAufSeite.add(new ArrayList<>()); // Weitere Seite hinzufügen, bspw. zur Anzeige von Gültige/Ungültige HINTER den Wahlvorschlägen...
      }
    }

    for (int i = 0; i < groupsOfStimmzettelPositionenAufSeite.size(); i++) {
      int nummerHorizontal = nummerHorizontalOffset + 1 + i;

      List<ErgebnisZusammenstellungListe> listenAufSeite = new ArrayList<>();
      List<ErgebnisZusammenstellungDirektkandidatur> direktkandidaturenAufSeite = new ArrayList<>();

      for (Long stimmzettelPosition : groupsOfStimmzettelPositionenAufSeite.get(i)) {
        if (listePerStimmzettelPosition.containsKey(stimmzettelPosition)) {
          listenAufSeite.add(listePerStimmzettelPosition.get(stimmzettelPosition));
        }

        if (direktkandidaturPerStimmzettelPosition.containsKey(stimmzettelPosition)) {
          direktkandidaturenAufSeite.add(direktkandidaturPerStimmzettelPosition.get(stimmzettelPosition));
        }
      }

      result.computeIfAbsent(nummerHorizontal, ignored -> new ArrayList<>()).add(createZeile(zeile, listenAufSeite, direktkandidaturenAufSeite));
    }

    return result;
  }

  private Map<Integer, List<ErgebnisTabellarischZeile>> collectAufteilungHorizontalNurListenkandidaturstimmen(ErgebnisTabellarischContext context, ErgebnisTabellarischZeile zeile) {
    Map<Integer, List<ErgebnisTabellarischZeile>> result = new HashMap<>();

    int nummerHorizontalOffset = 0;

    if (context.isErsteSeiteNurKopfzahlen()) {
      result.put(NUMMER_HORIZONTAL_INITIAL, singletonList(createZeile(zeile, emptyList(), emptyList()))); // Keine Wahlvorschläge, nur Kopfzahlen...

      nummerHorizontalOffset = NUMMER_HORIZONTAL_INITIAL;
    }

    List<ErgebnisZusammenstellungDirektkandidatur> direktkandidaturen = zeile.getDirektkandidaturen(); // ELECT-15337
    Set<PersonendatenPK> direktkandidaturPKs = direktkandidaturen.stream().map(ErgebnisZusammenstellungDirektkandidatur::getPersonendatenPK).collect(toSet());

    int anzahlListenkandidaturenMaxAufErsterSeite = context.getAnzahlWahlvorschlaegeMaxAufErsterSeite();
    int anzahlListenkandidaturenMaxAufWeitererSeite = context.getAnzahlWahlvorschlaegeMaxAufWeitererSeite();

    List<ErgebnisZusammenstellungListe> listenAufErsterSeite = new ArrayList<>();
    List<List<ErgebnisZusammenstellungListe>> groupsOfListenAufWeitererSeite = new ArrayList<>();

    for (ErgebnisZusammenstellungListe liste : zeile.getListen()) { // Iteriere über alle Listen der aktuellen Zeile...
      List<ErgebnisZusammenstellungListenkandidatur> listenkandidaturenInZeile = liste.getListenkandidaturen();

      if (!direktkandidaturPKs.isEmpty() && /* ELECT-16754 */ zeile.isUnterhalbVonDirektwahlgebiet()) {
        listenkandidaturenInZeile = listenkandidaturenInZeile.stream()
            .filter(listenkandidatur -> listenkandidatur.getIdentischVerknuepfteDirektkandidaturPK().filter(direktkandidaturPKs::contains).isEmpty()) //
            .collect(toList());
      }

      int anzahlListenkandidaturenInZeile = listenkandidaturenInZeile.size();

      /*
       * Erzeuge neue Dummy-Listen, die nur die Listenkandidaturen der ersten/jeder weiteren Seite enthalten...
       *
       * ... NOTE ELECTKOM-2621, dass die Dummy-Listen (für die erste Seite) IMMER angelegt werden müssen, auch wenn sie bspw. keine Listenkandidaturen haben. Ansonsten wird die Tabelle gar nicht
       * ausgegeben, obwohl Werte für Kopfzahlen, Direkt- und Listenstimmen vorhanden sind!!!
       */

      List<ErgebnisZusammenstellungListenkandidatur> listenkandidaturenAufErsterSeite = subList(listenkandidaturenInZeile, anzahlListenkandidaturenMaxAufErsterSeite);
      listenAufErsterSeite.add(createListe(liste, listenkandidaturenAufErsterSeite));

      if (anzahlListenkandidaturenInZeile > anzahlListenkandidaturenMaxAufErsterSeite) {
        List<ErgebnisZusammenstellungListenkandidatur> listenkandidaturenAufWeiterenSeiten = subList(listenkandidaturenInZeile, anzahlListenkandidaturenMaxAufErsterSeite,
            anzahlListenkandidaturenInZeile);

        List<List<ErgebnisZusammenstellungListenkandidatur>> groupsOfListenkandidaturenAufWeitererSeite = groupsOfSubList(listenkandidaturenAufWeiterenSeiten,
            anzahlListenkandidaturenMaxAufWeitererSeite);

        for (List<ErgebnisZusammenstellungListenkandidatur> listenkandidaturenAufWeitererSeite : groupsOfListenkandidaturenAufWeitererSeite) {
          List<ErgebnisZusammenstellungListe> listenAufWeitererSeite = new ArrayList<>();
          listenAufWeitererSeite.add(createListe(liste, listenkandidaturenAufWeitererSeite));

          groupsOfListenAufWeitererSeite.add(listenAufWeitererSeite);
        }
      }
    }

    if (!listenAufErsterSeite.isEmpty()) {
      int nummerHorizontalAufErsterSeite = nummerHorizontalOffset + 1;

      /*
       * Erzeuge neue Zeilen für die neuen Dummy-Listen (mit den Listenkandidaturen der ersten/jeder weiteren Seite)...
       */

      result.computeIfAbsent(nummerHorizontalAufErsterSeite, ignored -> new ArrayList<>()).add(createZeile(zeile, listenAufErsterSeite, direktkandidaturen));

      for (int i = 0; i < groupsOfListenAufWeitererSeite.size(); i++) {
        int nummerHorizontalAufWeitererSeite = nummerHorizontalAufErsterSeite + 1 + i;

        result.computeIfAbsent(nummerHorizontalAufWeitererSeite, ignored -> new ArrayList<>()).add(createZeile(zeile, groupsOfListenAufWeitererSeite.get(i), direktkandidaturen));
      }
    }

    return result;
  }

  private <T> List<T> subList(Collection<T> elements, int anzahlMax) {
    return subList(elements, 0, anzahlMax);
  }

  private <T> List<T> subList(Collection<T> elements, int fromIndex, int anzahlMax) {
    return new ArrayList<>(elements).subList(fromIndex, Math.min(anzahlMax, elements.size()));
  }

  private <T> List<List<T>> groupsOfSubList(Collection<T> elements, int anzahlMax) {
    if (elements == null) {
      return emptyList();
    }

    return ListUtil.bildeBloeckeAlsListen(elements, anzahlMax);
  }

  private ErgebnisZusammenstellungListe createListe(ErgebnisZusammenstellungListe template, List<ErgebnisZusammenstellungListenkandidatur> listenkandidaturen) {
    return new ErgebnisZusammenstellungListe(template, listenkandidaturen);
  }

  private ErgebnisTabellarischZeile createZeile(ErgebnisTabellarischZeile template, List<ErgebnisZusammenstellungListe> listen, List<ErgebnisZusammenstellungDirektkandidatur> direktkandidaturen) {
    return new ErgebnisTabellarischZeile(
        new ErgebnisZusammenstellungZeile(template.getDatengebiet(), template.isUnterhalbVonDirektwahlgebiet(), template.isUnterhalbVonListenwahlgebiet(), template.getStimmabgabeart(),
            template.getErfassung(), template.getWaehler(), template.getWahlberechtigte(), template.getStimmzettel(), template.getStimmen(), listen, direktkandidaturen),
        template.getZwischensummen(), false);
  }

  private ErgebnisTabellarischTabelle createTabelle(ErgebnisTabellarischTabelle template, List<ErgebnisTabellarischZeile> zeilen) {
    return new ErgebnisTabellarischTabelle(template, zeilen);
  }

  private ErgebnisTabellarischAufteilungHorizontal createAufteilungHorizontal(int nummer, int nummerMax, Stimmenart stimmenart, ErgebnisTabellarischTabelle template,
      List<ErgebnisTabellarischZeile> zeilen) {
    ErgebnisTabellarischTabelle tabelle = createTabelle(template, zeilen);

    info("Erzeuge neue Seite zur horizontalen Aufteilung (zu viele Spalten) [nummer = {}/{}, stimmenart = {}, anzahl zeilen = {}]", nummer, nummerMax, stimmenart, tabelle.getZeilen().size());

    return new ErgebnisTabellarischAufteilungHorizontal(nummer, nummerMax, stimmenart, tabelle);
  }

  private ErgebnisTabellarischAufteilungVertikal createAufteilungVertikal(int nummer, int nummerMax, ErgebnisTabellarischTabelle template, List<ErgebnisTabellarischZeile> zeilen,
      Optional<ErgebnisTabellarischZeile> zeileUebertragsummen) {
    ErgebnisTabellarischTabelle tabelle = createTabelle(template, zeilen);

    info("Erzeuge neue Seite zur vertikalen Aufteilung (zu viele Zeilen) [nummer = {}/{}, anzahl zeilen = {}, uebertragsummen vorhanden = {}]", nummer, nummerMax, tabelle.getZeilen().size(),
        zeileUebertragsummen.isPresent());

    return new ErgebnisTabellarischAufteilungVertikal(nummer, nummerMax, tabelle, zeileUebertragsummen.or(tabelle::getZeileSpaltensummenGesamt));
  }

  private void info(String format, Object... params) {
    LOG.info(format, params);
  }

  private static class ErgebnisTabellarischAufteilungHorizontal {

    private final int nummer;

    private final int nummerMax;

    private final Stimmenart stimmenart;

    private final ErgebnisTabellarischTabelle tabelle;

    public ErgebnisTabellarischAufteilungHorizontal(int nummer, int nummerMax, ErgebnisTabellarischAufteilungHorizontal template) {
      this(nummer, nummerMax, template.getStimmenart(), template.getTabelle());
    }

    public ErgebnisTabellarischAufteilungHorizontal(int nummer, int nummerMax, Stimmenart stimmenart, ErgebnisTabellarischTabelle tabelle) {
      super();

      this.nummer = nummer;
      this.nummerMax = nummerMax;

      this.stimmenart = stimmenart;

      this.tabelle = tabelle;
    }

    public int getNummer() {
      return nummer;
    }

    public int getNummerMax() {
      return nummerMax;
    }

    public Stimmenart getStimmenart() {
      return stimmenart;
    }

    public ErgebnisTabellarischTabelle getTabelle() {
      return tabelle;
    }
  }

  private static class ErgebnisTabellarischAufteilungVertikal {

    private final int nummer;

    private final int nummerMax;

    private final ErgebnisTabellarischTabelle tabelle;

    private final Optional<ErgebnisTabellarischZeile> zeileUebertragsummen;

    public ErgebnisTabellarischAufteilungVertikal(int nummer, int nummerMax, ErgebnisTabellarischTabelle tabelle, Optional<ErgebnisTabellarischZeile> zeileUebertragsummen) {
      super();

      this.nummer = nummer;
      this.nummerMax = nummerMax;

      this.tabelle = tabelle;

      this.zeileUebertragsummen = zeileUebertragsummen;
    }

    public int getNummer() {
      return nummer;
    }

    public int getNummerMax() {
      return nummerMax;
    }

    public ErgebnisTabellarischTabelle getTabelle() {
      return tabelle;
    }

    public Optional<ErgebnisTabellarischZeile> getZeileUebertragsummen() {
      return zeileUebertragsummen;
    }
  }
}
