
package de.ivu.elect.dokumente.ergebnis;

import static de.ivu.elect.jee.entity.EntityUtil.castTo;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

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

import de.ivu.elect.business.ergebnis.control.AbstractErgebnisDatenWahlberechtigte;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenGueltige;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenStimmzettel;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenWaehler;
import de.ivu.elect.business.ergebnis.control.ErgebnisdokumentTypExportContext;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietDescriptor;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformation;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolver;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolverContext;
import de.ivu.elect.business.gebietsbaum.control.GebietByBreadthFirstComparator;
import de.ivu.elect.business.gebietsbaum.entity.GebietPK;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.internet.control.GebietUndStimmabgabeart;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingabestatus;
import de.ivu.elect.business.stimmen.boundary.DirektstimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.GruppeStimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.ListenkandidaturstimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.ListenstimmeLabelGenerator;
import de.ivu.elect.business.wahl.entity.CommonWahlart;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlartErgebnisart;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.dokumente.BaseElectContext;
import de.ivu.elect.dokumente.ergebnis.zusammenstellung.ErgebnisTabellarischZeile;
import de.ivu.elect.dokumente.ergebnis.zusammenstellung.ErgebnisTabellarischZwischensummen;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.presentation.gebietsbaum.GebietNummerFormatter;
import de.ivu.elect.presentation.person.PersonendatenFormatter;
import de.ivu.elect.was.exchange.ergebnis.ErgebnisExportGebietFormatter;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.AbstractErgebnisZusammenstellungKandidatur;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungDirektkandidatur;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungErfassung;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungListe;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungStimmen;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungStimmzettel;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungWaehler;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungWahlberechtigte;
import de.ivu.elect.was.exchange.ergebnis.zusammenstellung.ErgebnisZusammenstellungZeileDatengebiet;

public abstract class AbstractErgebnisTabellarischDocumentZeileCreator<T extends AbstractErgebnisTabellarischDocumentZeile> {

  @Inject
  private transient Messages messages;

  @Inject
  private transient PersonendatenFormatter personendatenFormatter;

  @Inject
  private transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO;

  @Inject
  private transient WahlverwaltungGebietInformationResolver wahlverwaltungGebietInformationResolver;

  @Inject
  private transient ErgebnisExportGebietFormatter ergebnisExportGebietFormatter;

  @Inject
  private transient GebietNummerFormatter gebietNummerFormatter;

  @Inject
  private transient GruppeStimmeLabelGenerator gruppeStimmeLabelGenerator;

  private final Stimmabgabeart stimmabgabeart;

  protected AbstractErgebnisTabellarischDocumentZeileCreator() {
    super();

    this.stimmabgabeart = Stimmabgabeart.GESAMT;
  }

  // ELECT-15337
  public T mapDocumentZeile(BaseElectContext baseElectContext, ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      WahlverwaltungGebietInformation tabelleDatenGebietInformation, String erfassungsgebietGebietsreferenzFormat, ErgebnisTabellarischZeile zeile) {
    Wahl wahl = ergebnisDaten.getWahl();
    WahlartErgebnisart ergebnisart = ergebnisDaten.getErgebnis().getErgebnisart();

    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = zeile.getGebietPK().map(this::attach).orElse(null);

    T result = createZeile(ergebnisart, wahldurchfuehrungsgebiet);

    if (wahldurchfuehrungsgebiet == null) {
      ErgebnisZusammenstellungZeileDatengebiet zeileDatenGebiet = zeile.getDatengebiet();

      String zeileDatenGebietName = zeileDatenGebiet.getName();
      String zeileDatenGebietNummer = zeileDatenGebiet.getNummer();

      WahlverwaltungGebietInformation wahlkreisInformation = zeileDatenGebiet.getWahlkreis();
      WahlverwaltungGebietInformation stimmkreisInformation = zeileDatenGebiet.getStimmkreis();
      WahlverwaltungGebietInformation wahlbereichInformation = zeileDatenGebiet.getWahlbereich();

      WahlverwaltungGebietInformation bundeslandInformation = zeileDatenGebiet.getBundesland();
      WahlverwaltungGebietInformation kreisInformation = zeileDatenGebiet.getKreis();
      WahlverwaltungGebietInformation gemeindeverbandInformation = zeileDatenGebiet.getGemeindeverband();
      WahlverwaltungGebietInformation gemeindeInformation = zeileDatenGebiet.getGemeinde();
      WahlverwaltungGebietInformation ortsbezirkInformation = zeileDatenGebiet.getOrtsbezirk();
      WahlverwaltungGebietInformation stadtbezirkInformation = zeileDatenGebiet.getStadtbezirk();

      result.datenGebiet = new ErgebnisDocumentDatenGebietDocument(tabelleDatenGebietInformation); // LEGACY -> Gebiet der Tabelle, nicht der Gebiet der Zeile...

      result.name = zeileDatenGebietName;

      result.datenArt = ErgebnisDocumentDatenArt.GESAMT;

      if (zeile.isStimmabgabeartUrnenwahl()) {
        result.datenArt = ErgebnisDocumentDatenArt.URNENWAHL;
      } else if (zeile.isStimmabgabeartBriefwahl()) {
        result.datenArt = ErgebnisDocumentDatenArt.BRIEFWAHL;
      }

      result.containsZwischensumme = zeile.containsZwischensummen();
      result.datenArtZwischensumme = zeile.getZwischensummen().map(ErgebnisTabellarischZwischensummen::isZwischensummenStimmabgabeart).orElse(false);

      result.wahlbezirk.name = zeileDatenGebietName;
      result.wahlbezirk.nummer = zeileDatenGebietNummer;
      result.wahlbezirk.nummerFormatiert = zeileDatenGebietNummer;
      result.wahlbezirk.kuerzel = zeileDatenGebietName;

      result.wahlbezirkGebietsreferenz10Stellig = zeileDatenGebietNummer;
      result.wahlbezirkGebietsreferenz10StelligFormatiert = zeileDatenGebietNummer;
      result.wahlbezirkGebietsreferenz13Stellig = zeileDatenGebietNummer;
      result.wahlbezirkGebietsreferenz13StelligFormatiert = zeileDatenGebietNummer;

      ErgebnisZusammenstellungErfassung erfassung = zeile.getErfassung();
      result.wahlbezirk.isErgebniseingabestatusEingabeBeendet = erfassung.isEingabeBeendet();
      result.wahlbezirk.isErgebniseingabestatusGesperrt = erfassung.isGesperrt();
      result.wahlbezirk.isErgebniseingabestatusAbgeschlossen = erfassung.isAbgeschlossen();
      result.wahlbezirk.isErgebniseingabestatusFreigegeben = erfassung.isFreigegeben();
      result.wahlbezirk.isErgebniseingabestatusAusbleibend = erfassung.isAusbleibend();

      result.wahlkreis = new ErgebnisDocumentGebietDocument(wahlkreisInformation);
      result.stimmkreis = new ErgebnisDocumentGebietDocument(stimmkreisInformation);
      result.wahlbereich = new ErgebnisDocumentGebietDocument(wahlbereichInformation);

      result.bundesland = new ErgebnisDocumentGebietDocument(bundeslandInformation);
      result.landkreis = new ErgebnisDocumentGebietDocument(kreisInformation);
      result.gemeindeverband = new ErgebnisDocumentGebietDocument(gemeindeverbandInformation);
      result.gemeinde = new ErgebnisDocumentGebietDocument(gemeindeInformation);
      result.ortsbezirk = new ErgebnisDocumentGebietDocument(ortsbezirkInformation);
      result.stadtbezirk = new ErgebnisDocumentGebietDocument(stadtbezirkInformation);
    } else {
      Wahldurchfuehrungsgebiet erfassungsgebiet = wahldurchfuehrungsgebiet;

      fillInGebietInformationen(result, erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformation, tabelleDatenGebietInformation,
          createErfassungsgebietDescriptor(ergebnisDaten, erfassungsgebiet));
    }

    if (!result.isDatenArtZwischensumme() && result.wahlbezirk.isSonderstimmbezirk) {
      result.name = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, result.name);

      result.wahlbezirk.nummerFormatiert = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, defaultIfBlank(result.wahlbezirk.nummerFormatiert, result.wahlbezirk.nummer));

      result.wahlbezirkGebietsreferenz10Stellig = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, result.wahlbezirkGebietsreferenz10Stellig);
      result.wahlbezirkGebietsreferenz10StelligFormatiert = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, result.wahlbezirkGebietsreferenz10StelligFormatiert);
      result.wahlbezirkGebietsreferenz13Stellig = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, result.wahlbezirkGebietsreferenz13Stellig);
      result.wahlbezirkGebietsreferenz13StelligFormatiert = addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(baseElectContext, result.wahlbezirkGebietsreferenz13StelligFormatiert);
    }

    fillInErgebnisDatenWahlberechtigte(wahl, result, zeile.getWahlberechtigte());
    fillInErgebnisDatenWaehler(wahl, result, zeile.getWaehler());

    fillInErgebnisDatenGueltige(wahl, result, zeile.getStimmzettel(), zeile.getStimmen());

    fillInErgebnisDatenStimmen(wahl, result, zeile.getDirektkandidaturen(), zeile.getListen());

    return result;
  }

  protected String addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(BaseElectContext baseElectContext, String value) {
    return trimToEmpty(trimToEmpty(value) + " " + baseElectContext.getMessage("ergebnis.gesamtergebnis.tabellarisch.exportieren.dokument.zeile.suffix.SONDERSTIMMBEZIRK"));
  }

  /*
   *
   *
   *
   *
   *
   * ELECT-15337, LEGACY-Methoden -> NICHT mehr erweitern!!!
   *
   *
   *
   *
   *
   */

  protected Stimmabgabeart getStimmabgabeart() {
    return stimmabgabeart;
  }

  public List<T> createZeilenFuerAlleErfassungsgebiete(ErgebnisdokumentTypExportContext context, ErgebnisDaten ergebnisDaten, String erfassungsgebietGebietsreferenzFormat,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> stimmzettelgebietInformationFct,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> gebietUebergeordnetInformationFct) {
    List<T> result = new ArrayList<>();

    for (Wahldurchfuehrungsgebiet erfassungsgebiet : collectErfassungsgebiete(ergebnisDaten)) {
      result.add(createZeile(context, ergebnisDaten, erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformationFct, gebietUebergeordnetInformationFct, erfassungsgebiet));
    }

    return result;
  }

  private T createZeile(ErgebnisdokumentTypExportContext context, ErgebnisDaten ergebnisDaten, String erfassungsgebietGebietsreferenzFormat,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> stimmzettelgebietInformationFct,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> gebietUebergeordnetInformationFct, Wahldurchfuehrungsgebiet erfassungsgebiet) {
    WahlverwaltungGebietDescriptor erfassungsgebietDescriptor = createErfassungsgebietDescriptor(ergebnisDaten, erfassungsgebiet);

    return createZeile(ergebnisDaten, erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformationFct.apply(erfassungsgebietDescriptor),
        gebietUebergeordnetInformationFct.apply(erfassungsgebietDescriptor), erfassungsgebietDescriptor);
  }

  public T createZeile(ErgebnisDaten ergebnisDaten, String erfassungsgebietGebietsreferenzFormat, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      WahlverwaltungGebietInformation gebietUebergeordnetInformation, WahlverwaltungGebietDescriptor erfassungsgebietDescriptor) {
    T result = createZeile(ergebnisDaten.getErgebnis().getErgebnisart(), castTo(erfassungsgebietDescriptor.getGebiet(), Wahldurchfuehrungsgebiet.class));
    fillInZeileFuerErfassungsgebiet(result, ergebnisDaten, erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);

    return result;
  }

  private void fillInZeileFuerErfassungsgebiet(T zeile, ErgebnisDaten ergebnisDaten, String erfassungsgebietGebietsreferenzFormat, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      WahlverwaltungGebietInformation gebietUebergeordnetInformation, WahlverwaltungGebietDescriptor erfassungsgebietDescriptor) {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = castTo(erfassungsgebietDescriptor.getGebiet(), Wahldurchfuehrungsgebiet.class);

    fillInGebietInformationen(zeile, erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);

    fillInErgebnisDatenKopfzahlen(zeile, ergebnisDaten, wahldurchfuehrungsgebiet, getStimmabgabeart());
    fillInErgebnisDatenStimmen(zeile, ergebnisDaten, wahldurchfuehrungsgebiet, getStimmabgabeart());
  }

  protected abstract void fillInErgebnisDatenStimmen(T zeile, ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart);

  protected void fillInErgebnisDatenStimmen(Wahl wahl, T zeile, List<ErgebnisZusammenstellungDirektkandidatur> direktkandidaturen, List<ErgebnisZusammenstellungListe> listen) {
    // ELECT-15337
  }

  protected void fillInGebietInformationen(T zeile, String erfassungsgebietGebietsreferenzFormat, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      WahlverwaltungGebietInformation gebietUebergeordnetInformation, WahlverwaltungGebietDescriptor erfassungsgebietDescriptor) {
    WahlartErgebnisart ergebnisart = zeile.ergebnisart;
    Wahldurchfuehrungsgebiet erfassungsgebiet = castTo(erfassungsgebietDescriptor.getGebiet(), Wahldurchfuehrungsgebiet.class);

    WahlverwaltungGebietInformation wahlkreisInformation = erfassungsgebietDescriptor.getWahlkreis();
    WahlverwaltungGebietInformation stimmkreisInformation = erfassungsgebietDescriptor.getStimmkreis();
    WahlverwaltungGebietInformation wahlbereichInformation = erfassungsgebietDescriptor.getWahlbereich();

    WahlverwaltungGebietInformation erfassungsgebietInformation = erfassungsgebietDescriptor.getErfassungsgebiet();

    WahlverwaltungGebietInformation bundeslandInformation = erfassungsgebietDescriptor.getBundesland();
    WahlverwaltungGebietInformation kreisInformation = erfassungsgebietDescriptor.getKreis();
    WahlverwaltungGebietInformation gemeindeverbandInformation = erfassungsgebietDescriptor.getGemeindeverband();
    WahlverwaltungGebietInformation gemeindeInformation = erfassungsgebietDescriptor.getGemeinde();
    WahlverwaltungGebietInformation ortsbezirkInformation = erfassungsgebietDescriptor.getOrtsbezirk();
    WahlverwaltungGebietInformation stadtbezirkInformation = erfassungsgebietDescriptor.getStadtbezirk();

    zeile.name = formatGebietsreferenz(erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);

    zeile.datenGebiet = new ErgebnisDocumentDatenGebietDocument(gebietUebergeordnetInformation);
    zeile.datenArt = erfassungsgebiet.isBriefwahlgebiet() ? ErgebnisDocumentDatenArt.BRIEFWAHL : ErgebnisDocumentDatenArt.URNENWAHL;

    zeile.wahlkreis = new ErgebnisDocumentGebietDocument(wahlkreisInformation);
    zeile.stimmkreis = new ErgebnisDocumentGebietDocument(stimmkreisInformation);
    zeile.wahlbereich = new ErgebnisDocumentGebietDocument(wahlbereichInformation);

    zeile.wahlbezirk = new ErgebnisDocumentErfassungsgebietDocument(erfassungsgebietInformation);

    // ELECT-9400
    zeile.wahlbezirk.isErgebniseingabestatusEingabeBeendet = erfassungsgebiet.matchesAnyErgebniseingabestatusInUebergeordneterGebietshierarchie(ergebnisart, Ergebniseingabestatus::isEingabeBeendet);
    zeile.wahlbezirk.isErgebniseingabestatusGesperrt = erfassungsgebiet.matchesAnyErgebniseingabestatusInUebergeordneterGebietshierarchie(ergebnisart, Ergebniseingabestatus::isGesperrt);
    zeile.wahlbezirk.isErgebniseingabestatusAbgeschlossen = erfassungsgebiet.matchesAnyErgebniseingabestatusInUebergeordneterGebietshierarchie(ergebnisart, Ergebniseingabestatus::isAbgeschlossen);
    zeile.wahlbezirk.isErgebniseingabestatusFreigegeben = erfassungsgebiet.matchesAnyErgebniseingabestatusInUebergeordneterGebietshierarchie(ergebnisart, Ergebniseingabestatus::isFreigegeben);
    zeile.wahlbezirk.isErgebniseingabestatusAusbleibend = erfassungsgebiet.matchesAnyErgebniseingabestatusInUebergeordneterGebietshierarchie(ergebnisart, Ergebniseingabestatus::isAusbleibend);

    // ELECT-12557
    Function<Wahldurchfuehrungsgebiet, String> gebietFormatter = gebiet -> gebietNummerFormatter.getNummer(gebiet);

    zeile.wahlbezirk.isWirdInAnderemGebietErfasst = erfassungsgebiet.getErfasstIn() != null;
    zeile.wahlbezirk.wirdErfasstInGebiet = Optional.ofNullable(erfassungsgebiet.getErfasstIn()).map(gebietFormatter).orElse("");

    List<Wahldurchfuehrungsgebiet> erfasstFuer = wahldurchfuehrungsgebietDAO.findByErfasstFuerGebiete(erfassungsgebiet).stream().sorted(GebietByBreadthFirstComparator.INSTANCE).collect(toList());

    zeile.wahlbezirk.isErfasstAndereGebiet = !erfasstFuer.isEmpty();
    zeile.wahlbezirk.erfasstAuchFuerGebiete = erfasstFuer.stream().map(gebietFormatter).collect(Collectors.joining(", "));

    zeile.wahlbezirkBriefwahlbezirk = zeile.wahlbezirk.isBriefwahlbezirk;

    zeile.wahlbezirkGebietsreferenz10Stellig = formatGebietsreferenz("%2$s%3$s", stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);
    zeile.wahlbezirkGebietsreferenz10StelligFormatiert = formatGebietsreferenz("%2$s %3$s", stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);
    zeile.wahlbezirkGebietsreferenz13Stellig = formatGebietsreferenz("%1$s%2$s%3$s", stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);
    zeile.wahlbezirkGebietsreferenz13StelligFormatiert = formatGebietsreferenz("%1$s %2$s %3$s", stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);

    zeile.bundesland = new ErgebnisDocumentGebietDocument(bundeslandInformation);
    zeile.landkreis = new ErgebnisDocumentGebietDocument(kreisInformation);
    zeile.gemeindeverband = new ErgebnisDocumentGebietDocument(gemeindeverbandInformation);
    zeile.gemeinde = new ErgebnisDocumentGebietDocument(gemeindeInformation);
    zeile.ortsbezirk = new ErgebnisDocumentGebietDocument(ortsbezirkInformation);
    zeile.stadtbezirk = new ErgebnisDocumentGebietDocument(stadtbezirkInformation);
  }

  private void fillInErgebnisDatenKopfzahlen(T zeile, ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart) {
    fillInErgebnisDatenWahlberechtigte(zeile, ergebnisDaten.getErgebnisDatenWahlberechtigte(), wahldurchfuehrungsgebiet, stimmabgabeart);
    fillInErgebnisDatenWaehler(zeile, ergebnisDaten.getErgebnisDatenWaehler(), wahldurchfuehrungsgebiet, stimmabgabeart);
    fillInErgebnisDatenGueltige(zeile, ergebnisDaten, wahldurchfuehrungsgebiet, stimmabgabeart);
  }

  // Cf. https://support.elect-it.com/wiki/display/ELECT/Kopfzahlenkonfiguration
  protected void fillInErgebnisDatenWahlberechtigte(T zeile, AbstractErgebnisDatenWahlberechtigte ergebnisDatenWahlberechtigte, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet,
      Stimmabgabeart stimmabgabeart) {
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(wahldurchfuehrungsgebiet, stimmabgabeart);

    long anzahlWahlberechtigteOhneWahlschein = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteOhneWahlschein(gebietUndStimmabgabeart);
    long anzahlWahlberechtigteMitWahlschein = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteMitWahlschein(gebietUndStimmabgabeart);
    long anzahlWahlberechtigteNichtImWaehlerverzeichnis = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteNichtImWaehlerverzeichnis(gebietUndStimmabgabeart);
    long anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis(gebietUndStimmabgabeart);
    long anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis(gebietUndStimmabgabeart);
    long anzahlWahlberechtigteGesamt = ergebnisDatenWahlberechtigte.getSummeWahlberechtigteGesamt(gebietUndStimmabgabeart);

    fillInErgebnisDatenWahlberechtigte(wahldurchfuehrungsgebiet.getWahl(), zeile, anzahlWahlberechtigteOhneWahlschein, anzahlWahlberechtigteMitWahlschein,
        anzahlWahlberechtigteNichtImWaehlerverzeichnis, anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis, anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis,
        anzahlWahlberechtigteGesamt);
  }

  protected void fillInErgebnisDatenWahlberechtigte(Wahl wahl, T zeile, ErgebnisZusammenstellungWahlberechtigte wahlberechtigte) {
    long anzahlWahlberechtigteOhneWahlschein = wahlberechtigte.getAnzahlOhneWahlschein().getAnzahl();
    long anzahlWahlberechtigteMitWahlschein = wahlberechtigte.getAnzahlMitWahlschein().getAnzahl();
    long anzahlWahlberechtigteNichtImWaehlerverzeichnis = wahlberechtigte.getAnzahlNichtImWaehlerverzeichnis().getAnzahl();
    long anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis = wahlberechtigte.getAnzahlGesamtInklusiveNichtImWaehlerverzeichnis().getAnzahl();
    long anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis = wahlberechtigte.getAnzahlGesamtExklusiveNichtImWaehlerverzeichnis().getAnzahl();
    long anzahlWahlberechtigteGesamt = wahlberechtigte.getAnzahlGesamt().getAnzahl();

    fillInErgebnisDatenWahlberechtigte(wahl, zeile, anzahlWahlberechtigteOhneWahlschein, anzahlWahlberechtigteMitWahlschein, anzahlWahlberechtigteNichtImWaehlerverzeichnis,
        anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis, anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis, anzahlWahlberechtigteGesamt);
  }

  protected void fillInErgebnisDatenWahlberechtigte(Wahl wahl, T zeile, long anzahlWahlberechtigteOhneWahlschein, long anzahlWahlberechtigteMitWahlschein,
      long anzahlWahlberechtigteNichtImWaehlerverzeichnis, long anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis, long anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis,
      long anzahlWahlberechtigteGesamt) {
    zeile.wahlberechtigteOhneWahlschein = anzahlWahlberechtigteOhneWahlschein;
    zeile.wahlberechtigteMitWahlschein = anzahlWahlberechtigteMitWahlschein;
    zeile.wahlberechtigteNichtImWaehlerverzeichnis = anzahlWahlberechtigteNichtImWaehlerverzeichnis;
    zeile.wahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis = anzahlWahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis;
    zeile.wahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis = anzahlWahlberechtigteGesamtExklusiveNichtImWaehlerverzeichnis;
    zeile.wahlberechtigteGesamt = anzahlWahlberechtigteGesamt;

    if (!zeile.isZwischensumme() /* == bereits kumuliert!!! */ && zeile.wahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis > anzahlWahlberechtigteGesamt) { // ELECTMUC-281, ELECTKOM-2241
      zeile.wahlberechtigteGesamt = zeile.wahlberechtigteGesamtInklusiveNichtImWaehlerverzeichnis;
    }

    // ELECT-8018, LEGACY!!!
    zeile.a1 = zeile.wahlberechtigteOhneWahlschein;
    zeile.a2 = zeile.wahlberechtigteMitWahlschein;
    zeile.a3 = zeile.wahlberechtigteNichtImWaehlerverzeichnis;
    zeile.a = zeile.wahlberechtigteGesamt;
  }

  // Cf. https://support.elect-it.com/wiki/display/ELECT/Kopfzahlenkonfiguration
  protected void fillInErgebnisDatenWaehler(T zeile, ErgebnisDatenWaehler ergebnisDatenWaehler, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart) {
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(wahldurchfuehrungsgebiet, stimmabgabeart);

    long anzahlWaehlerOhneWahlschein = ergebnisDatenWaehler.getSummeWaehlerOhneWahlschein(gebietUndStimmabgabeart);
    long anzahlWaehlerMitWahlschein = ergebnisDatenWaehler.getSummeWaehlerMitWahlschein(gebietUndStimmabgabeart);
    long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl = ergebnisDatenWaehler.getSummeWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl(gebietUndStimmabgabeart);
    long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl = ergebnisDatenWaehler.getSummeWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl(gebietUndStimmabgabeart);
    long anzahlWaehlerGesamt = ergebnisDatenWaehler.getSummeWaehlerGesamt(gebietUndStimmabgabeart);

    fillInErgebnisDatenWaehler(wahldurchfuehrungsgebiet.getWahl(), zeile, anzahlWaehlerOhneWahlschein, anzahlWaehlerMitWahlschein, anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl,
        anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl, anzahlWaehlerGesamt);
  }

  protected void fillInErgebnisDatenWaehler(Wahl wahl, T zeile, ErgebnisZusammenstellungWaehler waehler) {
    long anzahlWaehlerOhneWahlschein = waehler.getAnzahlOhneWahlschein().getAnzahl();
    long anzahlWaehlerMitWahlschein = waehler.getAnzahlMitWahlschein().getAnzahl();
    long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl = waehler.getAnzahlMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl().getAnzahl();
    long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl = waehler.getAnzahlMitWahlscheinInUrnenwahlAnzahlAusBriefwahl().getAnzahl();
    long anzahlWaehlerGesamt = waehler.getAnzahlGesamt().getAnzahl();

    fillInErgebnisDatenWaehler(wahl, zeile, anzahlWaehlerOhneWahlschein, anzahlWaehlerMitWahlschein, anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl,
        anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl, anzahlWaehlerGesamt);
  }

  protected void fillInErgebnisDatenWaehler(Wahl wahl, T zeile, long anzahlWaehlerOhneWahlschein, long anzahlWaehlerMitWahlschein, long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl,
      long anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl, long anzahlWaehlerGesamt) {
    zeile.waehlerOhneWahlschein = anzahlWaehlerOhneWahlschein;
    zeile.waehlerMitWahlschein = anzahlWaehlerMitWahlschein;
    zeile.waehlerMitWahlscheinAnteilAusUrnenwahl = anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusUrnenwahl;
    zeile.waehlerMitWahlscheinAnteilAusBriefwahl = anzahlWaehlerMitWahlscheinInUrnenwahlAnzahlAusBriefwahl;
    zeile.waehlerGesamt = anzahlWaehlerGesamt;

    if (Boolean.TRUE.equals(zeile.wahlbezirkBriefwahlbezirk)) {
      zeile.waehlerOhneWahlschein = 0L;
      zeile.waehlerMitWahlschein = anzahlWaehlerGesamt;
      zeile.waehlerMitWahlscheinAnteilAusUrnenwahl = 0L;
      zeile.waehlerMitWahlscheinAnteilAusBriefwahl = anzahlWaehlerGesamt;
    }

    // ELECT-8018, LEGACY!!!
    zeile.b1 = zeile.waehlerMitWahlschein;
    zeile.b2 = zeile.waehlerOhneWahlschein;
    zeile.b = zeile.waehlerGesamt;

    // ELECTMUC-282, ELECT-7835, dass standardmäßig B1 == "Wähler mit Wahlschein" gilt und (bisher) nur bei MUC/BY-KOM B1 == "Wähler laut Wählerverzeichnis"
    // und B2 == "Wähler mit Wahlschein" bei landesweiten/kommunalen Wahlen gilt!!!
    if (!CommonWahlart.BUNDESTAGSWAHL.equalsWahlart(wahl) && !CommonWahlart.EUROPAWAHL.equalsWahlart(wahl)) {
      zeile.b1 = zeile.waehlerOhneWahlschein;
      zeile.b2 = zeile.waehlerMitWahlschein;
    }
  }

  protected void fillInErgebnisDatenGueltige(T zeile, ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart) {
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(wahldurchfuehrungsgebiet, stimmabgabeart);

    ErgebnisDatenStimmzettel ergebnisDatenStimmzettel = ergebnisDaten.getErgebnisDatenStimmzettel();

    Long anzahlStimmzettelGueltig = ergebnisDatenStimmzettel.getStimmzettelAktuellGueltig(gebietUndStimmabgabeart);
    Long anzahlStimmzettelUngueltig = ergebnisDatenStimmzettel.getStimmzettelAktuellUngueltig(gebietUndStimmabgabeart);
    Long anzahlStimmzettelUnbestimmt = ergebnisDatenStimmzettel.getStimmzettelAktuellUnbestimmt(gebietUndStimmabgabeart);
    Long anzahlStimmzettelGesamt = ergebnisDatenStimmzettel.getStimmzettelAktuellGesamt(gebietUndStimmabgabeart);

    ErgebnisDatenGueltige ergebnisDatenGueltige = ergebnisDaten.getErgebnisDatenGueltige();

    Long direktstimmenGueltig = ergebnisDatenGueltige.getErststimmenAktuellGueltig(gebietUndStimmabgabeart);
    Long direktstimmenUngueltig = ergebnisDatenGueltige.getErststimmenAktuellUngueltig(gebietUndStimmabgabeart);
    Long direktstimmenGesamt = ergebnisDatenGueltige.getErststimmenAktuellGesamt(gebietUndStimmabgabeart);

    Long listenstimmenGueltig = ergebnisDatenGueltige.getZweitstimmenAktuellGueltig(gebietUndStimmabgabeart);
    Long listenstimmenUngueltig = ergebnisDatenGueltige.getZweitstimmenAktuellUngueltig(gebietUndStimmabgabeart);
    Long listenstimmenGesamt = ergebnisDatenGueltige.getZweitstimmenAktuellGesamt(gebietUndStimmabgabeart);

    fillInErgebnisDatenGueltige(wahldurchfuehrungsgebiet.getWahl(), zeile, anzahlStimmzettelGueltig, anzahlStimmzettelUngueltig, anzahlStimmzettelUnbestimmt, anzahlStimmzettelGesamt,
        direktstimmenGueltig, direktstimmenUngueltig, direktstimmenGesamt, listenstimmenGueltig, listenstimmenUngueltig, listenstimmenGesamt);
  }

  protected void fillInErgebnisDatenGueltige(Wahl wahl, T zeile, ErgebnisZusammenstellungStimmzettel stimmzettel, ErgebnisZusammenstellungStimmen stimmen) {
    long anzahlStimmzettelGueltig = stimmzettel.getDirektstimmenGueltig().getAnzahl();
    long anzahlStimmzettelUngueltig = stimmzettel.getDirektstimmenUngueltig().getAnzahl();
    long anzahlStimmzettelUnbestimmt = stimmzettel.getDirektstimmenUnbestimmt().getAnzahl();
    long anzahlStimmzettelGesamt = stimmzettel.getDirektstimmenGesamt().getAnzahl();

    if (wahl.isWahlMitListengebieten()) {
      anzahlStimmzettelGueltig = Math.max(anzahlStimmzettelGueltig, stimmzettel.getListenstimmenGueltig().getAnzahl());
      anzahlStimmzettelUngueltig = Math.max(anzahlStimmzettelUngueltig, stimmzettel.getListenstimmenUngueltig().getAnzahl());
      anzahlStimmzettelUnbestimmt = Math.max(anzahlStimmzettelUnbestimmt, stimmzettel.getListenstimmenUnbestimmt().getAnzahl());
      anzahlStimmzettelGesamt = Math.max(anzahlStimmzettelGesamt, stimmzettel.getListenstimmenGesamt().getAnzahl());
    }

    Long direktstimmenGueltig = stimmen.getDirektstimmenGueltig().getAnzahl();
    Long direktstimmenUngueltig = stimmen.getDirektstimmenUngueltig().getAnzahl();
    Long direktstimmenGesamt = stimmen.getDirektstimmenGesamt().getAnzahl();

    Long listenstimmenGueltig = stimmen.getListenstimmenGueltig().getAnzahl();
    Long listenstimmenUngueltig = stimmen.getListenstimmenUngueltig().getAnzahl();
    Long listenstimmenGesamt = stimmen.getListenstimmenGesamt().getAnzahl();

    fillInErgebnisDatenGueltige(wahl, zeile, anzahlStimmzettelGueltig, anzahlStimmzettelUngueltig, anzahlStimmzettelUnbestimmt, anzahlStimmzettelGesamt, direktstimmenGueltig, direktstimmenUngueltig,
        direktstimmenGesamt, listenstimmenGueltig, listenstimmenUngueltig, listenstimmenGesamt);
  }

  protected void fillInErgebnisDatenGueltige(Wahl wahl, T zeile, Long anzahlStimmzettelGueltig, Long anzahlStimmzettelUngueltig, Long anzahlStimmzettelUnbestimmt, Long anzahlStimmzettelGesamt,
      Long direktstimmenGueltig, Long direktstimmenUngueltig, Long direktstimmenGesamt, Long listenstimmenGueltig, Long listenstimmenUngueltig, Long listenstimmenGesamt) {
    zeile.stimmzettelGueltig = anzahlStimmzettelGueltig;
    zeile.stimmzettelUngueltig = anzahlStimmzettelUngueltig;
    zeile.stimmzettelUnbestimmt = anzahlStimmzettelUnbestimmt;
    zeile.stimmzettelGesamt = anzahlStimmzettelGesamt;
  }

  private String formatGebietsreferenz(String erfassungsgebietGebietsreferenzFormat, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      WahlverwaltungGebietInformation gebietUebergeordnetInformation, WahlverwaltungGebietDescriptor erfassungsgebietDescriptor) {
    return ergebnisExportGebietFormatter.formatGebietsreferenz(erfassungsgebietGebietsreferenzFormat, stimmzettelgebietInformation, gebietUebergeordnetInformation, erfassungsgebietDescriptor);
  }

  protected abstract T createZeile(WahlartErgebnisart ergebnisart, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet);

  @Nullable
  protected Wahlvorschlag extractWahlvorschlag(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, ErgebnisDaten ergebnisDaten, PolitischeGruppe politischeGruppe) {
    Direktkandidatur direktkandidatur = extractDirektkandidatur(wahldurchfuehrungsgebiet, ergebnisDaten, politischeGruppe, null);

    if (direktkandidatur != null) {
      return direktkandidatur.getDirektkandidat();
    }

    return extractListe(wahldurchfuehrungsgebiet, ergebnisDaten, politischeGruppe, null);
  }

  @Nullable
  protected Direktkandidatur extractDirektkandidatur(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, ErgebnisDaten ergebnisDaten, PolitischeGruppe politischeGruppe,
      @Nullable Wahlvorschlag wahlvorschlag) {
    Collection<Direktkandidatur> direktkandidaturen = new HashSet<>();

    if (wahlvorschlag != null && wahlvorschlag.isInstanceOf(Direktkandidat.class)) {
      direktkandidaturen.add(castTo(wahlvorschlag, Direktkandidat.class).getDirektkandidatur());
    } else {
      direktkandidaturen.addAll(ergebnisDaten.getErgebnisDatenDirektkandidaturen().getDirektkandidaturenAufStimmzettelFuerGruppeAufGebiet(politischeGruppe, wahldurchfuehrungsgebiet));
    }

    if (direktkandidaturen.size() > 1) {
      throw new IllegalStateException("Nur eine Direktkandidatur für eine Gruppe auf einem Gebiet erwartet! [gebiet = " + wahldurchfuehrungsgebiet.getNameMitNummer() + ", gruppe = "
          + politischeGruppe.getName() + " direktkandidaturen = " + direktkandidaturen + "]");
    }

    return direktkandidaturen.stream().filter(direktkandidatur -> !direktkandidatur.isNichtZugelassenImSinneAbgelehnt()).findFirst().orElse(null); // ELECT-14707
  }

  @Nullable
  protected Liste extractListe(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, ErgebnisDaten ergebnisDaten, PolitischeGruppe politischeGruppe, @Nullable Wahlvorschlag wahlvorschlag) {
    Collection<Liste> listen = new HashSet<>();

    if (wahlvorschlag != null && wahlvorschlag.isInstanceOf(Liste.class)) {
      listen.add(castTo(wahlvorschlag, Liste.class));
    } else {
      listen.addAll(ergebnisDaten.getListenInGebietshierarchie(politischeGruppe, wahldurchfuehrungsgebiet));
    }

    if (listen.size() > 1) {
      throw new IllegalStateException("Nur eine Liste für eine Gruppe auf einem Gebiet erwartet! [gebiet = " + wahldurchfuehrungsgebiet.getNameMitNummer() + ", gruppe = " + politischeGruppe.getName()
          + " listen = " + listen + "]");
    }

    return listen.stream().filter(liste -> !liste.isNichtZugelassenImSinneAbgelehnt()).findFirst().orElse(null); // ELECT-14707
  }

  protected List<Wahldurchfuehrungsgebiet> collectErfassungsgebiete(ErgebnisDaten ergebnisDaten) {
    // NOTE, dass die Gebiete sortiert nach ihrer Position ausgegeben werden
    return ergebnisDaten.getErfassungsgebieteErgebnisRelevantUnterhalbVon(ergebnisDaten.getWurzelgebiet());
  }

  // Cf. AbstractErgebnisDocumentExportMapper#collectPolitischeGruppenRelevantSorted!
  protected List<PolitischeGruppe> collectPolitischeGruppenRelevantSorted(ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet erfassungsgebiet) {
    return ergebnisDaten.getPolitischeGruppenZugelassenSortiertNachPositionAufGebiet(erfassungsgebiet);
  }

  protected WahlverwaltungGebietDescriptor createErfassungsgebietDescriptor(ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet erfassungsgebiet) {
    return wahlverwaltungGebietInformationResolver.createDescriptor(new WahlverwaltungGebietInformationResolverContext(ergebnisDaten), erfassungsgebiet);
  }

  protected GruppeStimmeLabelGenerator getGruppeStimmeLabelGenerator() {
    return gruppeStimmeLabelGenerator;
  }

  protected DirektstimmeLabelGenerator getDirektstimmeLabelGenerator() {
    return getGruppeStimmeLabelGenerator().getDirektstimmeLabelGenerator();
  }

  protected ListenstimmeLabelGenerator getListenstimmeLabelGenerator() {
    return getGruppeStimmeLabelGenerator().getListenstimmeLabelGenerator();
  }

  protected ListenkandidaturstimmeLabelGenerator getListenkandidaturstimmeLabelGenerator() {
    return getListenstimmeLabelGenerator().getListenkandidaturstimmeLabelGenerator();
  }

  protected Wahldurchfuehrungsgebiet attach(GebietPK gebietPK) {
    return wahldurchfuehrungsgebietDAO.findByPrimaryKey(gebietPK);
  }

  protected String formatKandidaturName(AbstractErgebnisZusammenstellungKandidatur kandidatur) {
    return kandidatur.getFamilienname();
  }

  protected String formatKandidaturNameVollstaendig(AbstractErgebnisZusammenstellungKandidatur kandidatur) {
    String kandidaturTitel = kandidatur.getTitel();
    String kandidaturFamilienname = kandidatur.getFamilienname();
    String kandidaturFamiliennameBestandteil = kandidatur.getFamiliennameBestandteil();

    return formatKandidaturNameVollstaendig(kandidaturTitel, kandidaturFamiliennameBestandteil, kandidaturFamilienname);
  }

  protected String formatKandidaturNameVollstaendig(String kandidaturTitel, String kandidaturFamiliennameBestandteil, String kandidaturFamilienname) {
    return personendatenFormatter.bestimmeTitelNamensbestandteileUndNachname(kandidaturTitel, kandidaturFamiliennameBestandteil, kandidaturFamilienname);
  }

  protected String formatKandidaturVornamen(AbstractErgebnisZusammenstellungKandidatur kandidatur) {
    return defaultIfBlank(kandidatur.getStimmzettelPersonendaten().getVornamen(), kandidatur.getRufname());
  }

  protected String getMessage(String msgKey, Object... params) {
    return getMessages().getMessageForMessageKey(msgKey, params);
  }

  protected Messages getMessages() {
    return messages;
  }

  protected long fixNullToZero(Long value) {
    if (value == null) {
      return 0;
    }

    return value;
  }
}
