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

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

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.SortedMap;
import java.util.function.Function;

import javax.inject.Inject;

import de.ivu.elect.business.dokumente.entity.NoDokumentExtraFileOutput;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.gesamtergebnis.ErgebnisGesamtergebnisTabellarischDocumentExportContext;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietDescriptor;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformation;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.wahl.entity.WahlartErgebnisart;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption;
import de.ivu.elect.dokumente.BaseElectContext;
import de.ivu.elect.dokumente.ergebnis.AbstractErgebnisDocumentExportMapper;
import de.ivu.elect.dokumente.ergebnis.ErgebnisDocumentDatenArt;
import de.ivu.elect.dokumente.ergebnis.ErgebnisDocumentDatenGebietDocument;
import de.ivu.elect.was.exchange.ergebnis.gesamtergebnis.ErgebnisGesamtergebnisExchange;
import de.ivu.elect.was.exchange.ergebnis.gesamtergebnis.ErgebnisGesamtergebnisTabellarischDocumentExportMapper;

/**
 * @see AbstractErgebnisGesamtergebnisTabellarischDocumentExportMapper!!!
 * @see ReferendumErgebnistabelleDocumentExportMapper!!!
 */
public class ReferendumErgebnisGesamtergebnisTabellarischDocumentExportMapper
    extends
      AbstractErgebnisDocumentExportMapper<ErgebnisGesamtergebnisTabellarischDocumentExportContext, Wahldurchfuehrungsgebiet, WahlverwaltungGebietInformation, ReferendumErgebnisGesamtergebnisTabellarischDocument>
    implements
      ErgebnisGesamtergebnisTabellarischDocumentExportMapper,
      ErgebnisGesamtergebnisExchange {

  public static final String FILE_FORMAT_NAME = "file.format.gesamtergebnis.v7.ve.document.muc";

  public static final String ERSTER_TEIL_NUMMER = "A";

  public static final String LETZTER_TEIL_NUMMER = "F";

  @Inject
  private transient ErgebnisGesamtergebnisTabellarischDocumentCreator documentCreator;

  @Inject
  private transient ReferendumErgebnisGesamtergebnisTabellarischDocumentZeileCreator documentZeileCreator;

  public ReferendumErgebnisGesamtergebnisTabellarischDocumentExportMapper() {
    super();
  }

  @Override
  protected boolean isAcceptContext(Wahldurchfuehrung wahldurchfuehrung) {
    return wahldurchfuehrung.isReferendum();
  }

  @Override
  protected String getFileFormatName() {
    return FILE_FORMAT_NAME;
  }

  @Override
  protected Function<ReferendumErgebnisGesamtergebnisTabellarischDocument, File> getTemplateFileByDocumentFinder(ErgebnisGesamtergebnisTabellarischDocumentExportContext context,
      ErgebnisDaten ergebnisDaten) {
    Map<ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt, File> templateFilePerDokumentVorlageArt = collectTemplateFilePerDokumentVorlageArt(context);

    return document -> {
      File result = null;

      if (document.hasTeilNummer(ERSTER_TEIL_NUMMER)) {
        result = templateFilePerDokumentVorlageArt.get(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.GESAMTERGEBNIS);
      } else {
        result = templateFilePerDokumentVorlageArt.get(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.DIREKTSTIMMEN_ERSTE_SEITE_ANDERS);
      }

      if (result != null) {
        return result;
      }

      throw new RuntimeException("Kein Template zum Generieren eines ODT-Dokumentes gefunden [seite = " + document.seitenNummer + ", teil = " + document.teilNummer + "]!");
    };
  }

  private Map<ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt, File> collectTemplateFilePerDokumentVorlageArt(ErgebnisGesamtergebnisTabellarischDocumentExportContext context) {
    Map<ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt, File> result = new HashMap<>();

    if (context.hasDokumentVorlageGesamtergebnis()) {
      result.put(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.GESAMTERGEBNIS, getTemplateFile(context.getDokumentVorlageGesamtergebnis()));
    }

    if (context.hasDokumentVorlageErsteSeiteDirektstimmen()) {
      result.put(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.DIREKTSTIMMEN_ERSTE_SEITE_ANDERS, getTemplateFile(context.getDokumentVorlageErsteSeiteDirektstimmen()));
    }

    if (context.hasDokumentVorlageWeitereSeitenDirektstimmen()) {
      result.put(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.DIREKTSTIMMEN, getTemplateFile(context.getDokumentVorlageWeitereSeitenDirektstimmen()));
    } else if (context.hasDokumentVorlageErsteSeiteDirektstimmen()) {
      result.put(ErgebnisGesamtergebnisTabellarischDocumentDokumentVorlageArt.DIREKTSTIMMEN, getTemplateFile(context.getDokumentVorlageErsteSeiteDirektstimmen()));
    }

    return result;
  }

  @Override
  protected List<Wahldurchfuehrungsgebiet> collectAuswertungseinheiten(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten) {
    return collectAuswertungsgebieteOderWurzelgebiet(context, ergebnisDaten);
  }

  @Override
  protected Map<WahlverwaltungGebietInformation, List<ReferendumErgebnisGesamtergebnisTabellarischDocument>> createDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context,
      ErgebnisDaten ergebnisDaten, List<Wahldurchfuehrungsgebiet> auswertungsgebiete) {
    // ELECTKOM-409, ELECTKOM-481
    return createDocuments(context, ergebnisDaten, auswertungsgebiete, context.isDocumentsCreatedPerStimmzettelgebiet(), context.isDocumentsCreatedPerGemeinde());
  }

  // Cf. AbstractErgebnisGesamtergebnisTabellarischDocumentExportMapper#createDocuments!!!
  private Map<WahlverwaltungGebietInformation, List<ReferendumErgebnisGesamtergebnisTabellarischDocument>> createDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context,
      ErgebnisDaten ergebnisDaten, List<Wahldurchfuehrungsgebiet> auswertungsgebiete, boolean documentsCreatedPerStimmzettelgebiet, boolean documentsCreatedPerGemeinde) {
    Map<WahlverwaltungGebietInformation, List<ReferendumErgebnisGesamtergebnisTabellarischDocument>> result = new HashMap<>();

    Ergebnisart ergebnisart = ergebnisDaten.getErgebnis().getErgebnisart();

    Function<Gebiet, String> auswertungsgebietSuffixFct = this::getDateiSimpleName;

    SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> erfassungsgebietePerGemeindePerStimmzettelgebiet = collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet(
        context, ergebnisDaten, auswertungsgebiete);

    for (Entry<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> entryPerStimmzettelgebiet : erfassungsgebietePerGemeindePerStimmzettelgebiet
        .entrySet()) {
      WahlverwaltungGebietInformation stimmzettelgebietInformation = entryPerStimmzettelgebiet.getKey();
      SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> erfassungsgebietePerGemeindeInStimmzettelgebiet = entryPerStimmzettelgebiet.getValue();

      info("\tErzeuge Dokumente für Stimmzettelgebiet [ergebnisart = " + ergebnisart + ", art = " + stimmzettelgebietInformation.getArt() + "/" + stimmzettelgebietInformation.getArtIndex()
          + ", nummer = " + stimmzettelgebietInformation.getNummer() + ", name = " + stimmzettelgebietInformation.getName() + ", gebiet in gebietsbaum = "
          + stimmzettelgebietInformation.getGebiet().isPresent() + ", anzahl gemeinden = " + erfassungsgebietePerGemeindeInStimmzettelgebiet.size() + ", odt-seiten erzeugt = pro"
          + (documentsCreatedPerStimmzettelgebiet ? " stimmzettelgebiet" : "") + (documentsCreatedPerGemeinde ? " gemeinde" : "") + "]");

      if (documentsCreatedPerStimmzettelgebiet) {
        WahlverwaltungGebietInformation stimmzettelgebietOdtKey = createDocumentKeyStimmzettelgebiet(stimmzettelgebietInformation, auswertungsgebiete, auswertungsgebietSuffixFct);
        List<ReferendumErgebnisGesamtergebnisTabellarischDocument> stimmzettelgebietOdtSeiten = createDocuments(context, ergebnisDaten, stimmzettelgebietInformation,
            erfassungsgebietePerGemeindeInStimmzettelgebiet);

        result.put(stimmzettelgebietOdtKey, stimmzettelgebietOdtSeiten);
      }

      if (documentsCreatedPerGemeinde) {
        for (Entry<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> entryPerGemeinde : erfassungsgebietePerGemeindeInStimmzettelgebiet.entrySet()) {
          WahlverwaltungGebietInformation gemeindeInformation = entryPerGemeinde.getKey();

          info("\tErzeuge Dokumente für Gemeinde [ergebnisart = " + ergebnisart + ", art = " + gemeindeInformation.getArt() + "/" + gemeindeInformation.getArtIndex() + ", nummer = "
              + gemeindeInformation.getNummer() + ", name = " + gemeindeInformation.getName() + ", gebiet in gebietsbaum = " + gemeindeInformation.getGebiet().isPresent()
              + ", anzahl erfassungsgebiete = " + entryPerGemeinde.getValue().size() + ", odt-seiten erzeugt = pro" + (documentsCreatedPerStimmzettelgebiet ? " stimmzettelgebiet" : "")
              + (documentsCreatedPerGemeinde ? " gemeinde" : "") + "]");

          WahlverwaltungGebietInformation gemeindeOdtKey = createDocumentKeyGemeinde(gemeindeInformation, auswertungsgebiete, auswertungsgebietSuffixFct);
          List<ReferendumErgebnisGesamtergebnisTabellarischDocument> gemeindeOdtSeiten = createDocuments(context, ergebnisDaten, stimmzettelgebietInformation, asSingletonSortedMap(entryPerGemeinde));

          result.put(gemeindeOdtKey, gemeindeOdtSeiten);
        }
      }
    }

    return result;
  }

  // Cf. AbstractErgebnisGesamtergebnisTabellarischDocumentExportMapper#collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet!!!
  private SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>>> collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet(
      ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten, List<Wahldurchfuehrungsgebiet> auswertungsgebiete) {
    boolean erfassungsgebietErfasstInAnderemGebietIgnored = false; // ELECT-12557

    return collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet(ergebnisDaten, auswertungsgebiete, erfassungsgebietErfasstInAnderemGebietIgnored,
        (stimmzettelgebietInformation, gemeindeInformation, erfassungsgebietDescriptor) -> erfassungsgebietDescriptor);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation stimmzettelgebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> erfassungsgebietePerGemeinde) {
    return createDocuments(context, ergebnisDaten, stimmzettelgebietInformation, erfassungsgebietePerGemeinde, context.isTabellenCreatedPerDatenArt(), context.isTabellenCreatedGemeindenGesamt());
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation stimmzettelgebietInformation, SortedMap<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> erfassungsgebietePerGemeinde,
      boolean tabellenCreatedPerDatenArt, boolean tabellenCreatedGemeindenGesamt) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = new ArrayList<>();

    for (Entry<WahlverwaltungGebietInformation, List<WahlverwaltungGebietDescriptor>> entryPerGemeinde : erfassungsgebietePerGemeinde.entrySet()) {
      WahlverwaltungGebietInformation gemeindeInformation = entryPerGemeinde.getKey();
      List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde = entryPerGemeinde.getValue();

      info("\t\tErzeuge Tabellen für Gemeinde [art = " + gemeindeInformation.getArt() + "/" + gemeindeInformation.getArtIndex() + ", nummer = " + gemeindeInformation.getNummer() + ", name = "
          + gemeindeInformation.getName() + ", gebiet in gebietsbaum = " + gemeindeInformation.getGebiet().isPresent() + ", anzahl zeilen = " + erfassungsgebieteInGemeinde.size() + "]");

      result.addAll(createTabellen(context, ergebnisDaten, tabellenCreatedPerDatenArt, stimmzettelgebietInformation, gemeindeInformation, erfassungsgebieteInGemeinde));
    }

    // NOTE, dass, falls mehr als eine Gemeinde existiert, die Werte aller Gemeinden/Stadtbezirke nochmals zu Pseudo-Gemeinden aggregiert werden
    // und Tabellen für diese Pseudo-Gemeinden zusätzlich ins ODT-Dokument geschrieben werden!
    if (erfassungsgebietePerGemeinde.size() > 1) {
      List<WahlverwaltungGebietDescriptor> erfassungsgebieteInStimmzettelgebiet = erfassungsgebietePerGemeinde.values().stream().flatMap(List::stream).collect(toList());

      if (tabellenCreatedGemeindenGesamt) {
        WahlverwaltungGebietInformation summenGebietGemeindenGesamt = ErgebnisDocumentDatenGebietDocument.createSummenGebietGemeindenGesamt();

        info("\t\tErzeuge Summentabellen (Pseudo-Gemeinde \"Gesamt\") [art = Pseudo-Gemeinde \"Gesamt\", nummer = " + summenGebietGemeindenGesamt.getNummer() + ", name = "
            + summenGebietGemeindenGesamt.getName() + ", tabellen = " + (tabellenCreatedPerDatenArt ? "per stimmabgabeart" : "kompakt") + "]");

        result.addAll(createSummenTabellen(context, ergebnisDaten, summenGebietGemeindenGesamt, stimmzettelgebietInformation, erfassungsgebieteInStimmzettelgebiet, tabellenCreatedPerDatenArt));
      }
    }

    postProcessSeitenUndTeile(result);

    return result;
  }

  private void postProcessSeitenUndTeile(List<ReferendumErgebnisGesamtergebnisTabellarischDocument> documents) {
    Collections.sort(documents, ReferendumErgebnisGesamtergebnisTabellarischDocument::compareByTeilNummer);

    Map<String, Long> seitenNummerPerTeilNummer = new HashMap<>(); // ELECTKOM-844, Seiten fortlaufend innerhalb eines Teils nummerieren!

    for (ReferendumErgebnisGesamtergebnisTabellarischDocument document : documents) {
      String teilNummer = document.teilNummer;

      long seitenNummer = seitenNummerPerTeilNummer.getOrDefault(teilNummer, 0L) + 1;
      document.seitenNummer = String.valueOf(seitenNummer);

      seitenNummerPerTeilNummer.put(teilNummer, seitenNummer);
    }

    for (Iterator<ReferendumErgebnisGesamtergebnisTabellarischDocument> iter = documents.iterator(); iter.hasNext();) {
      ReferendumErgebnisGesamtergebnisTabellarischDocument document = iter.next();

      document.seitenNummerMaximal = String.valueOf(seitenNummerPerTeilNummer.getOrDefault(document.teilNummer, 0L));

      document.seiteMitFortsetzung = iter.hasNext();
      document.seiteMitUnterschrift = !document.seiteMitFortsetzung; // ELECT-9048
    }
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createTabellen(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      boolean tabellenCreatedPerDatenArt, WahlverwaltungGebietInformation datenGebiet, WahlverwaltungGebietInformation gemeindeInformation,
      List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = new ArrayList<>();

    List<Gruppe> referendumVorlagen = collectReferendumVorlagenSorted(ergebnisDaten, erfassungsgebieteInGemeinde);
    BaseElectContext baseElectContext = createBaseElectContext(context, NoDokumentExtraFileOutput.INSTANCE);

    if (!tabellenCreatedPerDatenArt) {
      error("Erzeugung der Tabellen \"kompakt\" NICHT implementiert, erzeuge Tabellen \"per Stimmabgabeart\""); // Cf. ErgebnisGesamtergebnisTabellarischDocumentCreator#createDocumentAlleDatenArten!!!
    }

    result.addAll(createDocumentsUrnenwahl(context, ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, referendumVorlagen, baseElectContext));
    result.addAll(createDocumentsBriefwahl(context, ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, referendumVorlagen, baseElectContext));
    result.addAll(createDocumentsGesamt(context, ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, referendumVorlagen, baseElectContext));

    return result;
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocumentsUrnenwahl(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation datenGebiet, WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde,
      List<Gruppe> referendumVorlagen, BaseElectContext baseElectContext) {
    return createDocuments(context, ergebnisDaten, referendumVorlagen, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, baseElectContext, ErgebnisDocumentDatenArt.URNENWAHL);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocumentsBriefwahl(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation datenGebiet, WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde,
      List<Gruppe> referendumVorlagen, BaseElectContext baseElectContext) {
    return createDocuments(context, ergebnisDaten, referendumVorlagen, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, baseElectContext, ErgebnisDocumentDatenArt.BRIEFWAHL);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocumentsGesamt(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation datenGebiet, WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde,
      List<Gruppe> referendumVorlagen, BaseElectContext baseElectContext) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = createDocuments(context, ergebnisDaten, referendumVorlagen, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde,
        baseElectContext, ErgebnisDocumentDatenArt.GESAMT);

    for (ReferendumErgebnisGesamtergebnisTabellarischDocument document : result) {
      document.metadatenZeilen.clear();
      document.stimmenZeilen.clear();
    }

    return result;
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      List<Gruppe> referendumVorlagen, WahlverwaltungGebietInformation datenGebiet, WahlverwaltungGebietInformation gemeindeInformation,
      List<WahlverwaltungGebietDescriptor> erfassungsgebieteInGemeinde, BaseElectContext baseElectContext, ErgebnisDocumentDatenArt datenArt) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = new ArrayList<>();

    Ergebnisart ergebnisart = context.getErgebnisart();

    // Teil A
    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenMetadaten = new ArrayList<>();
    zeilenMetadaten.addAll(createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, datenArt));

    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenMetadatenGesamt = new ArrayList<>();
    zeilenMetadatenGesamt.addAll(zeilenMetadaten);

    String teilNummer = ERSTER_TEIL_NUMMER;

    if (ErgebnisDocumentDatenArt.GESAMT.equals(datenArt)) {
      // Summenseite
      ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, gemeindeInformation, datenArt, zeilenMetadaten, teilNummer);

      document.metadatenZeilen.addAll(zeilenMetadaten);
      document.brechneSummenMetadaten(zeilenMetadatenGesamt);

      result.add(document);
    } else {
      while (zeilenMetadaten.size() > context.getAnzahlZeilenProSeite() || !zeilenMetadaten.isEmpty()) {
        int max = context.getAnzahlZeilenProSeite();

        if (max > zeilenMetadaten.size()) {
          max = zeilenMetadaten.size();
        }

        List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> sublist = zeilenMetadaten.subList(0, max);

        ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, gemeindeInformation, datenArt, zeilenMetadaten, teilNummer);

        document.metadatenZeilen.addAll(sublist);
        zeilenMetadaten.removeAll(sublist);

        result.add(document);

        if (zeilenMetadaten.isEmpty()) {
          document.brechneSummenMetadaten(zeilenMetadatenGesamt);
        }
      }
    }

    // Teil B-F
    for (Gruppe referendumVorlage : referendumVorlagen) {
      teilNummer = getNextLetter(teilNummer);

      List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenStimmergebnisse = new ArrayList<>();
      zeilenStimmergebnisse.addAll(createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebieteInGemeinde, datenArt, referendumVorlage));

      List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenStimmergebnisseGesamt = new ArrayList<>();
      zeilenStimmergebnisseGesamt.addAll(zeilenStimmergebnisse);

      if (ErgebnisDocumentDatenArt.GESAMT.equals(datenArt)) {
        // Summenseite
        ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, gemeindeInformation, datenArt, zeilenStimmergebnisse, teilNummer);

        fillVorlagenDaten(document, referendumVorlage, referendumVorlagen, ergebnisDaten);

        document.stimmenZeilen.addAll(zeilenStimmergebnisseGesamt);
        document.brechneSummenStimmergebnis(zeilenStimmergebnisseGesamt);

        result.add(document);
      } else {
        while (zeilenStimmergebnisse.size() > context.getAnzahlZeilenProSeite() || !zeilenStimmergebnisse.isEmpty()) {
          int max = context.getAnzahlZeilenProSeite();

          if (max > zeilenStimmergebnisse.size()) {
            max = zeilenStimmergebnisse.size();
          }

          List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> sublist = zeilenStimmergebnisse.subList(0, max);

          ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, gemeindeInformation, datenArt, zeilenStimmergebnisse, teilNummer);

          fillVorlagenDaten(document, referendumVorlage, referendumVorlagen, ergebnisDaten);

          document.stimmenZeilen.addAll(sublist);
          zeilenStimmergebnisse.removeAll(sublist);

          result.add(document);

          if (zeilenStimmergebnisse.isEmpty()) {
            document.brechneSummenStimmergebnis(zeilenStimmergebnisseGesamt);
          }
        }
      }
    }

    return result;
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createSummenTabellen(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation summenGebiet, WahlverwaltungGebietInformation datenGebiet, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInDatenGebiet,
      boolean tabellenCreatedPerDatenArt) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = new ArrayList<>();

    if (!tabellenCreatedPerDatenArt) {
      error("Erzeugung der Tabellen \"kompakt\" NICHT implementiert, erzeuge Tabellen \"per Stimmabgabeart\""); // Cf. ErgebnisGesamtergebnisTabellarischDocumentCreator#createSummenDocumentAlleDatenArten!!!
    }

    result.addAll(createSummenDocumentsUrnenwahl(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet));
    result.addAll(createSummenDocumentsBriefwahl(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet));
    result.addAll(createSummenDocumentsGesamt(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet));

    for (ReferendumErgebnisGesamtergebnisTabellarischDocument document : result) {
      document.metadatenZeilen.clear();
      document.stimmenZeilen.clear();
    }

    return result;
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createSummenDocumentsUrnenwahl(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation summenGebiet, WahlverwaltungGebietInformation datenGebiet, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInDatenGebiet) {
    return createSummenDocuments(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet, ErgebnisDocumentDatenArt.URNENWAHL);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createSummenDocumentsBriefwahl(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation summenGebiet, WahlverwaltungGebietInformation datenGebiet, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInDatenGebiet) {
    return createSummenDocuments(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet, ErgebnisDocumentDatenArt.BRIEFWAHL);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createSummenDocumentsGesamt(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation summenGebiet, WahlverwaltungGebietInformation datenGebiet, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInDatenGebiet) {
    return createSummenDocuments(context, ergebnisDaten, summenGebiet, datenGebiet, erfassungsgebieteInDatenGebiet, ErgebnisDocumentDatenArt.GESAMT);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocument> createSummenDocuments(ErgebnisGesamtergebnisTabellarischDocumentExportContext context, ErgebnisDaten ergebnisDaten,
      WahlverwaltungGebietInformation summenGebiet, WahlverwaltungGebietInformation datenGebiet, List<WahlverwaltungGebietDescriptor> erfassungsgebieteInDatenGebiet,
      ErgebnisDocumentDatenArt datenArt) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocument> result = new ArrayList<>();

    Ergebnisart ergebnisart = context.getErgebnisart();

    List<Gruppe> referendumVorlagen = collectReferendumVorlagenSorted(ergebnisDaten, erfassungsgebieteInDatenGebiet);
    BaseElectContext baseElectContext = createBaseElectContext(context, NoDokumentExtraFileOutput.INSTANCE);

    // Teil A
    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenMetadaten = new ArrayList<>();
    zeilenMetadaten.addAll(createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, summenGebiet, erfassungsgebieteInDatenGebiet, datenArt));

    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenMetadatenGesamt = new ArrayList<>();
    zeilenMetadatenGesamt.addAll(zeilenMetadaten);

    String teilNummer = ERSTER_TEIL_NUMMER;

    {
      ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, datenGebiet, datenArt, zeilenMetadaten, teilNummer);
      document.metadatenZeilen.addAll(zeilenMetadaten);

      result.add(document);

      document.brechneSummenMetadaten(zeilenMetadatenGesamt);
    }

    // Teil B-F
    for (Gruppe referendumVorlage : referendumVorlagen) {
      teilNummer = getNextLetter(teilNummer);

      List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenStimmergebnisse = new ArrayList<>();
      zeilenStimmergebnisse.addAll(createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, summenGebiet, erfassungsgebieteInDatenGebiet, datenArt, referendumVorlage));

      List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenStimmergebnisseGesamt = new ArrayList<>();
      zeilenStimmergebnisseGesamt.addAll(zeilenStimmergebnisse);

      ReferendumErgebnisGesamtergebnisTabellarischDocument document = createDocument(baseElectContext, ergebnisart, datenGebiet, datenGebiet, datenArt, zeilenStimmergebnisse, teilNummer);
      fillVorlagenDaten(document, referendumVorlage, referendumVorlagen, ergebnisDaten);

      document.stimmenZeilen.addAll(zeilenStimmergebnisseGesamt);

      result.add(document);

      document.brechneSummenStimmergebnis(zeilenStimmergebnisseGesamt);
    }

    return result;
  }

  private void fillVorlagenDaten(ReferendumErgebnisGesamtergebnisTabellarischDocument document, Gruppe referendumVorlage, List<Gruppe> referendumVorlagen, ErgebnisDaten ergebnisDaten) {
    document.spaltenPrefix = getNextLetter("K", referendumVorlagen.indexOf(referendumVorlage));

    document.referendumVorlageSchluessel = referendumVorlage.getSchluessel();
    document.referendumVorlagePosition = referendumVorlage.getRelativePosition();
    document.referendumVorlageName = referendumVorlage.getName();
    document.referendumVorlageKurzname = referendumVorlage.getKurzname();
    document.volksentscheidNummer = "" + (referendumVorlagen.indexOf(referendumVorlage) + 1);

    int referendumOptionNummer = 0;

    for (ReferendumOption referendumOption : ergebnisDaten.getReferendumOptionenSortiertNachPosition(referendumVorlage)) {
      referendumOptionNummer++;

      document.kuerzelDerOptionenQueue.add(document.spaltenPrefix + referendumOptionNummer);
      document.labelDerOptionenQueue.add(referendumOption.getLabel());
      document.kennworteDerOptionenQueue.add(referendumOption.getKennwort());
    }

    document.kuerzelGueltigeStimmen = document.spaltenPrefix + (referendumOptionNummer + 1);
    document.kuerzelUngueltigeStimmen = document.spaltenPrefix + (referendumOptionNummer + 2);
    document.kuerzelGesamtStimmen = document.spaltenPrefix;
  }

  private ReferendumErgebnisGesamtergebnisTabellarischDocument createDocument(BaseElectContext baseElectContext, WahlartErgebnisart ergebnisart, WahlverwaltungGebietInformation datenGebiet,
      WahlverwaltungGebietInformation gebietUebergeordnetInformation, ErgebnisDocumentDatenArt datenArt, List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenFuerDatenArt,
      String teilNummer) {
    ReferendumErgebnisGesamtergebnisTabellarischDocument result = documentCreator.createReferendumDocument(baseElectContext, ergebnisart, datenGebiet, gebietUebergeordnetInformation, datenArt,
        zeilenFuerDatenArt);
    result.teilNummer = teilNummer;

    return result;
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> createDocumentZeilenFuerDatenArt(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation datenGebiet,
      WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebiete, ErgebnisDocumentDatenArt datenArt) {
    return createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebiete, datenArt, Optional.empty());
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> createDocumentZeilenFuerDatenArt(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation datenGebiet,
      WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebiete, ErgebnisDocumentDatenArt datenArt, Gruppe referendumVorlage) {
    return createDocumentZeilenFuerDatenArt(ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebiete, datenArt, Optional.of(referendumVorlage));
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> createDocumentZeilenFuerDatenArt(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation datenGebiet,
      WahlverwaltungGebietInformation gemeindeInformation, List<WahlverwaltungGebietDescriptor> erfassungsgebiete, ErgebnisDocumentDatenArt datenArt, Optional<Gruppe> referendumVorlageIfPresent) {
    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilen = new ArrayList<>();

    for (WahlverwaltungGebietDescriptor erfassungsgebiet : erfassungsgebiete) {
      zeilen.add(createDocumentZeile(ergebnisDaten, datenGebiet, gemeindeInformation, erfassungsgebiet, referendumVorlageIfPresent));
    }

    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilenFuerDatenArt = collectZeilenFuerDatenArt(datenArt, zeilen);

    for (ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile zeile : zeilenFuerDatenArt) {
      zeile.nummer = (long) (1 + zeilenFuerDatenArt.indexOf(zeile));
    }

    return zeilenFuerDatenArt;
  }

  private ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile createDocumentZeile(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation datenGebiet,
      WahlverwaltungGebietInformation gemeindeInformation, WahlverwaltungGebietDescriptor erfassungsgebietDescriptor, Optional<Gruppe> referendumVorlageIfPresent) {
    return documentZeileCreator.createZeile(ergebnisDaten, getErfassungsgebietGebietsreferenzFormat(), datenGebiet, gemeindeInformation, erfassungsgebietDescriptor, referendumVorlageIfPresent);
  }

  private List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> collectZeilenFuerDatenArt(ErgebnisDocumentDatenArt datenArt,
      List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> zeilen) {
    if (ErgebnisDocumentDatenArt.GESAMT.equals(datenArt)) {
      return zeilen;
    }

    List<ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile> result = new ArrayList<>();

    for (ReferendumErgebnisGesamtergebnisTabellarischDocumentZeile zeile : zeilen) {
      if (datenArt.equals(zeile.datenArt)) {
        result.add(zeile);
      }
    }

    return result;
  }

  private List<Gruppe> collectReferendumVorlagenSorted(ErgebnisDaten ergebnisDaten, List<WahlverwaltungGebietDescriptor> erfassungsgebiete) {
    List<Gruppe> result = new ArrayList<>();

    if (erfassungsgebiete.isEmpty()) {
      for (Gruppe gruppe : ergebnisDaten.getGruppen()) {
        if (gruppe.isReferendumVorlage()) {
          result.add(gruppe);
        }
      }
    } else { // ELECTKOM-844
      Wahldurchfuehrungsgebiet erfassungsgebietBeliebig = erfassungsgebiete.stream().findAny() //
          .map(WahlverwaltungGebietDescriptor::getGebiet) //
          .map(gebiet -> castTo(gebiet, Wahldurchfuehrungsgebiet.class)) //
          .orElse(null);

      result.addAll(ergebnisDaten.getReferendumVorlagenSortiertNachPositionAufGebiet(erfassungsgebietBeliebig));
    }

    return result;
  }

  private String getNextLetter(String letter) {
    return getNextLetter(letter, 1);
  }

  private String getNextLetter(String letter, int offset) {
    int volksentscheidKennungCodePoint = letter.toCharArray()[0] + offset;

    if (Character.isLetter(volksentscheidKennungCodePoint) && Character.isUpperCase(volksentscheidKennungCodePoint)) {
      return new StringBuilder().appendCodePoint(volksentscheidKennungCodePoint).toString();
    }

    throw new IllegalStateException();
  }
}
