
package de.ivu.elect.presentation.stimmeingabe;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BooleanSupplier;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.inject.Inject;

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

import de.ivu.elect.business.CurrentCustomer;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.gebietsbaum.boundary.GebietExtendedDAO;
import de.ivu.elect.business.gebietsbaum.boundary.GebietSelectChangeEvent;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietExtendedDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietSessionSelect;
import de.ivu.elect.business.gebietsbaum.control.GebietByBreadthFirstComparator;
import de.ivu.elect.business.gebietsbaum.entity.AbstractGebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietConstants;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppeEigenschaften;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietExtendedDAO;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietWahlvorschlaegeInHierarchiePreloader;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.hochrechnung.entity.HochrechneralgorithmusAusfuehrbarkeitStatus;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerAusgabedaten;
import de.ivu.elect.business.konfiguration.entity.CustomParameter;
import de.ivu.elect.business.security.Customer;
import de.ivu.elect.business.security.UseCasePermission;
import de.ivu.elect.business.security.UseCaseSecure;
import de.ivu.elect.business.security.control.ElectCommonPrincipal;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangExtendedDAO;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangKonfiguration;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangStatus;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassenDescription;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassungLoeschen;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeExtendedDAO;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeFiktivewerteCreate;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeGruppeStimmenErfassenModelBuilder;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeGruppeStimmenErfassenModelContext;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeKopfzahlErfassenModelBuilder;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeListenkandidaturStimmenErfassenModelBuilder;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeReferendumOptionStimmenErfassenModelBuilder;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabefehlerExtendedDAO;
import de.ivu.elect.business.stimmeingabe.boundary.WasStimmeingabeBoundaryCreate;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeGruppeStimmenErfassenLabelGenerator;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeKopfzahlErfassenLabelGenerator;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeListenkandidaturStimmenErfassenLabelGenerator;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabePlausiBuilder;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeReferendumOptionStimmenErfassenLabelGenerator;
import de.ivu.elect.business.stimmeingabe.control.plausi.WahlvorschlaegeSupplier;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingabestufe;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangErfasser;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangExceptionContext;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangProperties;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsherkunft;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabe;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeGruppeStimmenErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeKopfzahlErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeListenkandidaturStimmenErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeReferendumVorlageStimmenErfassenModel;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabefehler;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabefehlerart;
import de.ivu.elect.business.stimmzettel.boundary.AnzahlStimmenProWaehlerBestimmer;
import de.ivu.elect.business.stimmzettel.boundary.EntityAufStimmzettelDatenExtendedDAOFactory;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.entity.AbstractWahlParameterLoader;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlErgebniseingangUebernahmeManuell;
import de.ivu.elect.business.wahl.entity.WahlParameterExtendedLoader;
import de.ivu.elect.business.wahl.entity.WahlParameterStimmeingabeErfassungsmaskeLayoutStimmeingabeGruppeStimmen;
import de.ivu.elect.business.wahl.entity.WahlParameterStimmeingabeErfassungsmaskeTabKeys;
import de.ivu.elect.business.wahl.entity.WahlParameterStimmeingabeErfassungsmaskeTabOrder;
import de.ivu.elect.business.wahl.entity.WahlParameterStimmeingabeErfassungsmaskeUmgangBlank;
import de.ivu.elect.business.wahl.entity.WahlParameterStimmeingabeErfassungsmaskeUmgangKennung;
import de.ivu.elect.business.wahl.entity.WahlParameterWerte;
import de.ivu.elect.business.wahl.entity.WahlartConstants;
import de.ivu.elect.business.wahl.entity.WahlartErgebnisartVorlaeufigErfassungsart;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahlvorschlag.boundary.DirektkandidatExtendedDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.ListeExtendedDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.WahlvorschlagExtendedDAO;
import de.ivu.elect.jee.doubleclick.PreventDoubleClick;
import de.ivu.elect.jee.doubleclick.PreventDoubleClickLockProvider;
import de.ivu.elect.jee.entity.EntityUtil;
import de.ivu.elect.jee.entity.tk.EntityTK;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.faces.FacesContextUtil;
import de.ivu.elect.jee.faces.FacesMessageUtil;
import de.ivu.elect.jee.faces.PreConditionCheck;
import de.ivu.elect.jee.presentation.dynamicform.AdvancedDynaFormModel;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.jee.presentation.dynamicform.ReadOnlyStringProperty;
import de.ivu.elect.jee.presentation.redirect.Redirect;
import de.ivu.elect.jee.presentation.security.StartViewResolver;
import de.ivu.elect.jee.presentation.view.AbstractView;
import de.ivu.elect.presentation.ergebnis.ErgebnisStatusInfo;
import de.ivu.elect.presentation.ergebnis.WasErgebnisStatusInfo;
import de.ivu.elect.presentation.gebietsbaum.GebietNummerFormatter;
import de.ivu.elect.presentation.gebietsbaum.tree.AbstractNoStoreGebietsbaumView;
import de.ivu.elect.presentation.niederschrift.NiederschriftKorrekturCreateView;
import de.ivu.elect.util.ElectRandom;

@PreventDoubleClick // ELECTMUC-892
public abstract class AbstractStimmeingabeErfassenView extends AbstractView implements PreventDoubleClickLockProvider, StimmeingabeErfassenViewHavingHeader, StimmeingabeErfassenViewHavingParameter {

  private static final long serialVersionUID = 1L;

  public static final Ergebniseingangsherkunft ERGEBNISEINGANGSHERKUNFT_MANUELLE_ERFASSUNG = Ergebniseingangsherkunft.BENUTZER_EINGABE;

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

  private static final long MAX_TRIES = 10L; // willkürlich

  private final ReentrantLock lock = new ReentrantLock();

  @Inject
  protected transient WahlParameterExtendedLoader wahlParameterExtendedLoader;

  @Inject
  protected transient CurrentCustomer currentCustomer;

  @Inject
  protected transient ElectCommonPrincipal principal;

  @Inject
  protected transient SystemGruppeEigenschaften systemGruppeEigenschaften;

  @Inject
  protected transient GebietNummerFormatter gebietNummerFormatter;

  @Inject
  protected transient WahldurchfuehrungsgebietExtendedDAO wahldurchfuehrungsgebietExtendedDAO;

  @Inject
  protected transient GruppeImGebietExtendedDAO gruppeImGebietExtendedDAO;

  @Inject
  protected transient GruppeImGebietWahlvorschlaegeInHierarchiePreloader gruppeImGebietWahlvorschlaegeInHierarchiePreloader;

  @Inject
  protected transient WahlvorschlagExtendedDAO wahlvorschlagExtendedDAO;

  @Inject
  protected transient DirektkandidatExtendedDAO direktkandidatExtendedDAO;

  @Inject
  protected transient ListeExtendedDAO listeExtendedDAO;

  @Inject
  protected transient StimmeingabefehlerExtendedDAO stimmeingabefehlerExtendedDAO;

  @Inject
  protected transient StimmeingabeExtendedDAO stimmeingabeExtendedDAO;

  @Inject
  protected transient ErgebniseingangExtendedDAO ergebniseingangExtendedDAO;

  @Inject
  protected transient WasStimmeingabeBoundaryCreate stimmeingabeBoundaryCreate;

  @Inject
  protected transient StimmeingabeFiktivewerteCreate stimmeingabeFiktivewerteCreate;

  @Inject
  protected transient StimmeingabeSecurity stimmeingabeSecurity;

  @Inject
  protected transient StimmeingabeErfassungLoeschen stimmeingabeErfassungLoeschen;

  @Inject
  protected transient WahldurchfuehrungsgebietSessionSelect wahldurchfuehrungsgebietSessionSelect;

  @Inject
  protected transient StimmeingabeErfassenViewViewAccessScopedStateStore viewAccessScopedState;

  @Inject
  protected transient StimmeingabeErfassenViewRequestScopedStateStore requestScopedState;

  @Inject
  protected transient AbstractNoStoreGebietsbaumView gebietsbaumView;

  @Inject
  protected transient StimmeingabeErfassenViewResolver stimmeingabeErfassenViewResolver;

  @Inject
  protected transient ErgebniseingangStatus ergebniseingangStatus;

  @Inject
  protected transient StartViewResolver startViewResolver;

  @Inject
  protected transient GebietExtendedDAO gebietExtendedDAO;

  @Inject
  protected transient LetzterErgebniseingangCustomInformationResolver letzterErgebniseingangCustomInformationResolver;

  @Inject
  protected transient StimmeingabeErfassenViewErgebniseingangsartStore stimmeingabeErfassenViewErgebniseingangsartStore;

  @Inject
  protected transient EntityAufStimmzettelDatenExtendedDAOFactory entityAufStimmzettelDatenExtendedDAOFactory;

  @Inject
  protected transient AnzahlStimmenProWaehlerBestimmer anzahlStimmenProWaehlerBestimmer;

  @Inject
  protected transient ErgebniseingangKonfiguration ergebniseingangKonfiguration;

  private StimmeingabeErfassenViewStatus stimmeingabeErfassenViewStatus;

  private WahlParameterStimmeingabeErfassungsmaskeLayoutStimmeingabeGruppeStimmen layoutStimmeingabeGruppeStimmen;

  private WahlParameterStimmeingabeErfassungsmaskeTabKeys stimmeingabeErfassenTabKeys;

  private WahlParameterStimmeingabeErfassungsmaskeUmgangBlank stimmeingabeErfassenUmgangBlank;

  private WahlParameterStimmeingabeErfassungsmaskeUmgangBlank stimmeingabeAnzeigenUmgangBlank;

  protected AbstractStimmeingabeErfassenView() {
    super();
  }

  public abstract String getViewId();

  protected abstract boolean isErfassungAufUnterseite();

  @PreConditionCheck
  public String checkPreCondition() {
    if (isGebietSelected()) {
      if (isApplicable(getWahl())) {
        if (!getStimmeingabeErfassenViewResolver().isErfassungsgebietSelected()) {
          return getStimmeingabeErfassenViewId();
        }

        return checkPreConditionInternal();
      } else {
        return getStimmeingabeErfassenViewId(); // ELECT-6297 => zur passenden Eingabemaske Wechseln
      }
    } else {
      return startViewResolver.getStartViewId();
    }
  }

  protected abstract String checkPreConditionInternal();

  public void preRenderView() {
    if (!isApplicable(getWahl())) { // ELECT-7291
      redirectToStimmeingabeErfassenView();
    }

    if (isLetzterErgebniseingangAktuellVorhandenHavingStatusFiktiv() && hasPermissionLetzterErgebniseingangAktuellVorhandenHavingStatusFiktivErkennen()) {
      addMessageAktuellerErgebniseingangFiktiv();
    }

    // ELECT-2240
    if (!isGebietLaesstEingabeZu()) {
      addMessageAktuellerErgebniseingangErfassungGesperrtByGebietErgebniseingabestatus();
    } else if (!isAktuellerErgebniseingangErgebniseingabestufeLaesstEingabeZu()) {
      addMessageAktuellerErgebniseingangErfassungGesperrtByErgebniseingabestufe();
    }

    if (isErfasstFuerAndereGebiete()) {
      addMessageErfasstFuerAndereGebiete();
    }

    Wahldurchfuehrungsgebiet gebiet = getGebiet();

    // ELECTKOM-101
    if (!isErfassungGesperrt()) {
      viewAccessScopedState.setEntity(gebiet);
    }

    // ELECT-8960
    getStimmeingabePlausiBuilder().checkPreConditions(gebiet, getErgebniseingangsart(), direktkandidatExtendedDAO, listeExtendedDAO);

    getStimmeingabeSecurity().preRenderView(getStimmeingabeErfassenContext(), getMessages());
  }

  protected void addMessageAktuellerErgebniseingangFiktiv() {
    addWarningMessage("stimmeingabe.erfassen.message.fiktive.werte");
  }

  protected void addMessageAktuellerErgebniseingangErfassungGesperrtByStatusUngueltigKannFehlerKorrigierbar() {
    addWarningMessage("stimmeingabe.erfassen.pruefen.eingabe.auf.gebiet.gesperrt.kann.fehler.korrigierbar", getGebietName());
  }

  protected void addMessageAktuellerErgebniseingangErfassungGesperrtByGebietErgebniseingabestatus() {
    Wahldurchfuehrungsgebiet gebietDasEingabeNichtZulaesst = getGebiet().getErstesGebietInHierarchieDasEingabeNichtZulaesst(getErgebniseingangsart(), getErgebniseingangsherkunft(),
        getEingangsherkunftIgnorieren());

    if (gebietDasEingabeNichtZulaesst != null) {
      gebietDasEingabeNichtZulaesst.getErgebniseingabestatus(getErgebniseingangsart())
          .addWarnMessageEingabeNichtMoeglich(new ErgebniseingangExceptionContext(gebietDasEingabeNichtZulaesst, getErgebniseingangsherkunft(), gebietNummerFormatter, getMessages()));
    }
  }

  protected void addMessageAktuellerErgebniseingangErfassungGesperrtByErgebniseingabestufe() {
    // To be overridden...
  }

  protected void addMessageErfasstFuerAndereGebiete() {
    String gebiete = getErfasstFuerGebiete().stream().sorted(GebietByBreadthFirstComparator.INSTANCE).map(g -> gebietNummerFormatter.getDarstellung(g)).collect(Collectors.joining(", "));

    addWarningMessage("stimmeingabe.erfassen.erfasst.fuer.gebiete", gebiete);
  }

  @Override
  public boolean isTabIndexPerColumn() {
    return WahlParameterStimmeingabeErfassungsmaskeTabOrder.PER_STIMMENART.equals(resolveWahlParameterWertEnumValueOrDefault(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_TAB_ORDER,
        WahlParameterStimmeingabeErfassungsmaskeTabOrder.class, WahlParameterStimmeingabeErfassungsmaskeTabOrder.defaultValue()));
  }

  private WahlParameterStimmeingabeErfassungsmaskeLayoutStimmeingabeGruppeStimmen getLayoutStimmeingabeGruppeStimmen() {
    if (layoutStimmeingabeGruppeStimmen == null) {
      layoutStimmeingabeGruppeStimmen = resolveWahlParameterWertEnumValueOrDefault(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_LAYOUT_STIMMEINGABE_GRUPPE_STIMMEN,
          WahlParameterStimmeingabeErfassungsmaskeLayoutStimmeingabeGruppeStimmen.class, WahlParameterStimmeingabeErfassungsmaskeLayoutStimmeingabeGruppeStimmen.defaultValue());
    }

    return layoutStimmeingabeGruppeStimmen;
  }

  @Override
  public boolean isTabIndexPerColumnLeftToRight() {
    return true;
  }

  @Override
  public boolean isTabIndexPerColumnIncludesSpaltensumme() {
    return true;
  }

  public WahlParameterStimmeingabeErfassungsmaskeTabKeys getStimmeingabeErfassenTabKeys() {
    if (stimmeingabeErfassenTabKeys == null) {
      stimmeingabeErfassenTabKeys = resolveWahlParameterWertEnumValueOrDefault(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_TAB_KEYS, WahlParameterStimmeingabeErfassungsmaskeTabKeys.class,
          WahlParameterStimmeingabeErfassungsmaskeTabKeys.defaultValue());
    }

    return stimmeingabeErfassenTabKeys;
  }

  protected WahlParameterStimmeingabeErfassungsmaskeUmgangKennung getStimmeingabeErfassenUmgangKennungDefault() {
    return WahlParameterStimmeingabeErfassungsmaskeUmgangKennung.VOR_STIMMEINGABE;
  }

  @Override
  public boolean isStimmeingabeErfassenAnzahlDefaultRequired() {
    return getStimmeingabeErfassenUmgangBlank().hasDefaultAnzahl();
  }

  public String getStimmeingabeErfassenAnzahlDefault() {
    return getStimmeingabeErfassenUmgangBlank().getDefaultAnzahl();
  }

  public String getStimmeingabeErfassenUmgangBlankDisplayValue() {
    return getMessage(getStimmeingabeErfassenUmgangBlank());
  }

  private WahlParameterStimmeingabeErfassungsmaskeUmgangBlank getStimmeingabeErfassenUmgangBlank() {
    if (stimmeingabeErfassenUmgangBlank == null) {
      stimmeingabeErfassenUmgangBlank = resolveUmgangBlankValue(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_UMGANG_BLANK_ERFASSEN);
    }

    return stimmeingabeErfassenUmgangBlank;
  }

  public boolean isStimmeingabeAnzeigenAnzahlDefaultRequired() {
    return getStimmeingabeAnzeigenUmgangBlank().hasDefaultAnzahl();
  }

  @Override
  public String getStimmeingabeAnzeigenAnzahlDefault() {
    return getStimmeingabeAnzeigenUmgangBlank().getDefaultAnzahl();
  }

  public String getStimmeingabeAnzeigenUmgangBlankDisplayValue() {
    return getMessage(getStimmeingabeAnzeigenUmgangBlank());
  }

  private WahlParameterStimmeingabeErfassungsmaskeUmgangBlank getStimmeingabeAnzeigenUmgangBlank() {
    if (stimmeingabeAnzeigenUmgangBlank == null) {
      stimmeingabeAnzeigenUmgangBlank = resolveUmgangBlankValue(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_UMGANG_BLANK_ANZEIGEN);
    }

    return stimmeingabeAnzeigenUmgangBlank;
  }

  protected boolean isGebietWaehlerBenannteWahlvorschlaegeErfassbar() {
    return getStimmeingabeGruppeStimmenErfassenModel() != null && getStimmeingabeGruppeStimmenErfassenModel().isWaehlerBenannteWahlvorschlaegeErfassbar();
  }

  protected boolean isGebietLaesstEingabeZu() {
    return isGebietLaesstEingabeZu(getEingangsherkunftIgnorieren());
  }

  protected boolean isErfasstFuerAndereGebiete() {
    return !getErfasstFuerGebiete().isEmpty();
  }

  protected List<Wahldurchfuehrungsgebiet> getErfasstFuerGebiete() {
    return getFindByErfasstFuerGebiete().apply(getGebiet());
  }

  private boolean isGebietLaesstEingabeZu(BooleanSupplier eingangsherkunftIgnorieren) {
    return isGebietSelected() && getGebiet().isEingabeMoeglich(getErgebniseingangsart(), getErgebniseingangsherkunft(), eingangsherkunftIgnorieren);
  }

  public void onGebietSelectChangeEvent(@Observes(notifyObserver = Reception.IF_EXISTS) GebietSelectChangeEvent event) {
    // Mit ELECTKOM-212 wird die Wechselmöglichkeit eingeschränkt => Beim Gebietswechsel immer zur Eingangsart der Wahl wechseln
    //  führt zur Verwirrung => wieder raus
    // stimmeingabeErfassenViewErgebniseingangsartStore.reset();

    gebietWechselt(event.getNeueEntity());
  }

  protected abstract void gebietWechselt(Gebiet neuesGebiet);

  protected boolean isGebietSelected() {
    return wahldurchfuehrungsgebietSessionSelect.isSelected();
  }

  protected Wahldurchfuehrungsgebiet getGebiet() {
    return wahldurchfuehrungsgebietSessionSelect.getEntity();
  }

  @Override
  public String getGebietName() {
    if (isGebietSelected()) {
      return getStimmeingabeErfassenContext().getGebietName();
    }

    return "";
  }

  // ELECT-2918
  public int getGebietInView() {
    if (isGebietSelected()) {
      return getGebiet().hashCode();
    }

    return 0;
  }

  // ELECT-2918
  public void setGebietInView(int gebietInViewHashCode) {
    if (gebietInViewHashCode != getGebiet().hashCode()) {
      throw new ElectCommonException(ElectCommonException.RedirectDestination.NONE, "stimmeingabe.erfassen.gebiet.geaendert.message");
    }
  }

  protected Collection<GruppeImGebiet> getGruppenImGebiet() {
    if (requestScopedState.getGruppenImGebiet() == null) {
      preloadGruppenImGebiet();
    }

    return requestScopedState.getGruppenImGebiet();
  }

  private void preloadGruppenImGebiet() {
    Collection<GruppeImGebiet> gigAufGebiet = findGruppenImGebiet(getGebiet());
    requestScopedState.setGruppenImGebiet(gigAufGebiet);
    gruppeImGebietWahlvorschlaegeInHierarchiePreloader.preloadGruppeImGebietWahlvorschlaegeInHierarchie(gigAufGebiet, getGebiet(), wahlvorschlagExtendedDAO);

    preloadGruppenImGebietInGebietshierachieAnGruppen();
  }

  protected void preloadGruppenImGebietInGebietshierachieAnGruppen() {
    // ELECT-12101
    Gebiet gebiet = getGebiet();
    while (gebiet != null) {
      for (GruppeImGebiet gruppeImGebiet : findGruppenImGebiet(gebiet)) {
        Gruppe gruppe = gruppeImGebiet.getGruppe();
        if (!gruppe.isGruppenImGebietLoaded()) {
          gruppe.setGruppenImGebietLoaded(new HashSet<>());
        }
        gruppe.getGruppenImGebiet().add(gruppeImGebiet);
      }

      gebiet = gebiet.getUebergeordnetesGebiet();
    }
  }

  protected Collection<GruppeImGebiet> findGruppenImGebiet(Gebiet gebiet) {
    return gruppeImGebietExtendedDAO.findByGebiet(gebiet);
  }

  protected boolean hasPermissionLetzterErgebniseingangAktuellVorhandenHavingStatusFiktivErkennen() {
    // ELECTMUC-121, ELECT-9794, sonst könnte ein Benutzer einen fiktiven Ergebniseingang überspeichern, ohne sich dessen bewusst zu sein...
    return getStimmeingabeSecurity().hasPermissionLetzterErgebniseingangOhneMussFehlerUeberschreiben() || stimmeingabeSecurity.hasPermissionLetzterErgebniseingangFiktivErkennen();
  }

  public abstract boolean isApplicable(@Nullable Wahl wahl);

  @Nullable
  protected Wahl getWahl() {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getGebiet();

    if (wahldurchfuehrungsgebiet != null) {
      return wahldurchfuehrungsgebiet.getWahl();
    }

    return null;
  }

  protected StimmeingabeErfassenViewStatus getStimmeingabeErfassenViewStatus() {
    if (stimmeingabeErfassenViewStatus == null) {
      stimmeingabeErfassenViewStatus = createStimmeingabeErfassenViewStatus();
    }

    return stimmeingabeErfassenViewStatus;
  }

  protected StimmeingabeErfassenViewStatus createStimmeingabeErfassenViewStatus() {
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getGebiet();

    if (wahldurchfuehrungsgebiet != null) {
      Wahldurchfuehrung wahldurchfuehrung = wahldurchfuehrungsgebiet.getWahldurchfuehrung();

      if (wahldurchfuehrung != null) {
        try { // ELECTKOM-472 Fehler anzeigen, wenn Fall eintritt
          if (wahldurchfuehrung.getBerichtetAn() != null) {
            Wahl berichtetAn = wahldurchfuehrung.getBerichtetAn();

            if (AbstractGebiet.findeGebietBeiAufteilungHandleWithCare(wahldurchfuehrungsgebiet, berichtetAn, gebietExtendedDAO) == null) {
              String berichtetAnWahl = getMessage("wahl.status.berichtet.an.message", berichtetAn.getName(), berichtetAn.getMandant().getName(), berichtetAn.getMandant().getNummer());

              addErrorMessage("wahl.aufgeteilt.gebiete.nicht.zugeordnet.erfassung", berichtetAnWahl, gebietNummerFormatter.getDarstellung(wahldurchfuehrungsgebiet));
            }
          }
        } catch (Exception ex) {
          // ignorieren
        }

        // ELECTKOM-264
        if (getStimmeingabeErfassenViewErgebniseingangsartStore().isErgebniseingangsartVerschiedenVonWahl(wahldurchfuehrung)) {
          if (isErgebniseingangsartEndgueltig(wahldurchfuehrung)) {
            Ergebniseingang ergebniseingangVorlaeufig = wahldurchfuehrungsgebiet.getLetzterErgebniseingangAktuell(ergebniseingangExtendedDAO, Ergebniseingangsart.VORLAEUFIG);

            if (ergebniseingangVorlaeufig == null || !ergebniseingangVorlaeufig.isInErgebnisUebernehmbar()) {
              if (hasPermission(UseCasePermission.STIMMEINGABE_ENDGUELTIG_OHNE_VORLAEUFIG_ERFASSEN)) {
                if (ergebniseingangVorlaeufig == null) {
                  addWarningMessage("stimmeingabe.erfassen.endgueltig.ohne.vorlaeufig.warnung");
                } else {
                  addWarningMessage("stimmeingabe.erfassen.endgueltig.ungueltige.vorlaeufig.warnung");
                }
              } else {
                addErrorMessage("stimmeingabe.erfassen.endgueltig.ohne.vorlaeufig");

                return StimmeingabeErfassenViewStatus.NUR_LESEN;
              }
            }
          }
        }
      }
    }

    return StimmeingabeErfassenViewStatus.EINGABE;
  }

  protected void setStimmeingabeErfassenViewStatus(StimmeingabeErfassenViewStatus stimmeingabeErfassenViewStatus) {
    this.stimmeingabeErfassenViewStatus = stimmeingabeErfassenViewStatus;
  }

  protected boolean isStimmeingabeErfassenViewStatusFehlerBestaetigen() {
    return StimmeingabeErfassenViewStatus.FEHLER_BESTAETIGEN.equals(getStimmeingabeErfassenViewStatus());
  }

  protected boolean isStimmeingabeErfassenViewStatusEingabe() {
    return StimmeingabeErfassenViewStatus.EINGABE.equals(getStimmeingabeErfassenViewStatus());
  }

  protected boolean isStimmeingabeErfassenViewStatusNurLesen() {
    return StimmeingabeErfassenViewStatus.NUR_LESEN.equals(getStimmeingabeErfassenViewStatus());
  }

  @Override
  public boolean isErfassungGesperrt() {
    boolean result = !isStimmeingabeErfassenViewStatusEingabe() || !isGebietLaesstEingabeZu() || !isAktuellerErgebniseingangErgebniseingabestufeLaesstEingabeZu();

    return getStimmeingabeSecurity().isErfassungGesperrt(getStimmeingabeErfassenContext(), result);
  }

  protected Collection<Stimmeingabe> getStimmeingabenLetzterErgebniseingang() {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangAktuellStimmeingaben();
  }

  protected List<Stimmeingabefehler> getStimmeingabefehlerLetzterErgebniseingang() {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangAktuellStimmeingabefehler();
  }

  protected Ergebniseingang getAktuellerErgebniseingang() {
    return getStimmeingabeErfassenContext().getErgebniseingang();
  }

  protected Ergebniseingang getAktuellerErgebniseingangOverrideErgebniseingangsart() {
    Ergebniseingang ergebniseingang = getAktuellerErgebniseingang();
    ergebniseingang.setOverrideErgebniseingangsart(isErgebniseingangsartVerschiedenVonWahl());

    return ergebniseingang;
  }

  protected boolean isAktuellerErgebniseingangErgebniseingabestufeLaesstEingabeZu() {
    return true;
  }

  protected Ergebniseingabestufe getAktuellerErgebniseingangErgebniseingabestufe() {
    return getAktuellerErgebniseingang().getErgebniseingabestufe();
  }

  protected Ergebnis getSelectedErgebnis() {
    return getStimmeingabeErfassenViewErgebniseingangsartStore().getSelectedErgebnis();
  }

  protected boolean isErgebniseingangsartWechselnUnterstuetzt() {
    if (getWahl() == null) {
      return false;
    }

    return getStimmeingabeErfassenViewErgebniseingangsartStore().isErgebniseingangsartVerschiedenVonWahl(getWahl()) // zurück immer
        || getWahl().getWahlart().getErgebnisartUmschaltbar().isUmschaltbarErgebniseingangErfassen(getErgebniseingangsart(), isLetzterErgebniseingangAktuellVorhandenHavingStatusGueltig(),
            getStimmeingabeErfassenViewStatus().isStimmeingabeMoeglich());
  }

  protected abstract Collection<Stimmeingabe> getStimmeingabenFuerDiesenView();

  protected Set<EntityTK> getStimmeingabeTksFuerDiesenView() {
    return EntityUtil.mappeNachKonkretestemTk(getStimmeingabenFuerDiesenView()).keySet();
  }

  public boolean isStimmeingabePlausiFehlerBtnPanelRendered() {
    return false; // Allgemein???: falls lange Seite (viele Tabellenzeilen/StimmeingabeGruppeStimmen) -> isErfassungGesperrt() && hasStimmeingabePlausiFehler();
  }

  public boolean isStimmeingabeErfassenBtnPanelRendered() {
    return !isStimmeingabePlausiFehlerBtnPanelRendered(); // ELECTVAPP-1040
  }

  public boolean isBtnScrollUpInitiallyRendered() {
    return false;
  }

  public boolean isBtnScrollDownInitiallyRendered() {
    return false; // ELECTVAPP-1040
  }

  public boolean hasStimmeingabePlausiFehler() {
    return !getStimmeingabenPlausiFehler().isEmpty();
  }

  public List<StimmeingabePlausiFehler> getStimmeingabenPlausiFehler() {
    if (requestScopedState.getStimmeingabenPlausiFehler() == null) {
      List<StimmeingabePlausiFehler> stimmeingabenPlausiFehler = createStimmeingabePlausiFehler(Stimmeingabefehlerart.ERROR);

      if (stimmeingabenPlausiFehler.isEmpty()) {
        stimmeingabenPlausiFehler = createStimmeingabePlausiFehler(Stimmeingabefehlerart.WARNING);
      }

      stimmeingabenPlausiFehler.sort(new StimmeingabePlausiFehlerByStimmeingabeBezugComparator());

      requestScopedState.setStimmeingabenPlausiFehler(stimmeingabenPlausiFehler);
    }

    return requestScopedState.getStimmeingabenPlausiFehler();
  }

  protected List<StimmeingabePlausiFehler> createStimmeingabePlausiFehler(Stimmeingabefehlerart stimmeingabefehlerart) {
    return getStimmeingabefehlerLetzterErgebniseingang().stream()
        .filter(stimmeingabefehler -> stimmeingabefehlerart.equals(stimmeingabefehler.getFehlerart()) && containsStimmeingabeByTk(stimmeingabefehler.getStimmeingabe()))
        .map(stimmeingabefehler -> new StimmeingabePlausiFehler(getMessages(), stimmeingabefehler, getStimmeingabeBezugId(stimmeingabefehler.getStimmeingabe()),
            getStimmeingabeBezug(stimmeingabefehler.getStimmeingabe())))
        .collect(Collectors.toList());
  }

  protected abstract String getStimmeingabeBezug(Stimmeingabe stimmeingabe);

  public String getStimmeingabeBezugId(Stimmeingabe stimmeingabe) {
    if (stimmeingabe != null && stimmeingabe.getTk() != null) {
      return "se-" + stimmeingabe.getTk().toString().hashCode();
    }

    return "";
  }

  private boolean containsStimmeingabeByTk(Stimmeingabe stimmeingabe) {
    return getStimmeingabeTksFuerDiesenView().contains(stimmeingabe.getTk());
  }

  protected StimmeingabeKopfzahlErfassenModel getStimmeingabeKopfzahlErfassenModel() {
    if (!isStimmenErfassenModelsBuilt()) {
      buildStimmenErfassenModels();
    }

    return getAktuellerErgebniseingang().getStimmeingabeKopfzahlErfassenModel();
  }

  protected StimmeingabeGruppeStimmenErfassenModel getStimmeingabeGruppeStimmenErfassenModel() {
    if (!isStimmenErfassenModelsBuilt()) {
      buildStimmenErfassenModels();
    }

    return getAktuellerErgebniseingang().getStimmeingabeGruppeStimmenErfassenModel();
  }

  protected StimmeingabeListenkandidaturStimmenErfassenModel getStimmeingabeListenkandidaturStimmenErfassenModel(GruppeImGebiet gruppeImGebiet) {
    if (!isStimmenErfassenModelsBuilt()) {
      buildStimmenErfassenModels();
    }

    return getAktuellerErgebniseingang().getStimmeingabeListenkandidaturStimmenErfassenModel(gruppeImGebiet);
  }

  protected StimmeingabeReferendumVorlageStimmenErfassenModel getStimmeingabeReferendumVorlageStimmenErfassenModel() {
    if (!isStimmenErfassenModelsBuilt()) {
      buildStimmenErfassenModels();
    }

    return getAktuellerErgebniseingang().getStimmeingabeReferendumVorlageStimmenErfassenModel();
  }

  protected boolean isStimmenErfassenModelsBuilt() {
    return getAktuellerErgebniseingang().isStimmeingabeKopfzahlErfassenModelSet() || getAktuellerErgebniseingang().isStimmeingabeGruppeStimmenErfassenModelSet()
        || getAktuellerErgebniseingang().hasStimmeingabeListenkandidaturStimmenErfassenModels() || getAktuellerErgebniseingang().isStimmeingabeReferendumVorlageStimmenErfassenModelSet();
  }

  protected void buildStimmenErfassenModels() {
    long millis = System.currentTimeMillis();

    getStimmeingabeErfassenDescription().buildStimmeingabeErfassenModels(getStimmeingabeErfassenContext());

    LOG.debug("buildStimmenErfassenModels [runtime={} ms]", System.currentTimeMillis() - millis);
  }

  protected StimmeingabeKopfzahlErfassenModelBuilder getStimmeingabeKopfzahlErfassenModelBuilder() {
    return getStimmeingabeErfassenDescription().getStimmeingabeKopfzahlErfassenModelBuilder();
  }

  protected StimmeingabeGruppeStimmenErfassenModelBuilder getStimmeingabeGruppeStimmenErfassenModelBuilder() {
    return getStimmeingabeErfassenDescription().getStimmeingabeGruppeStimmenErfassenModelBuilder();
  }

  protected StimmeingabeGruppeStimmenErfassenModelContext createStimmeingabeGruppeStimmenErfassenModelContext() {
    return new StimmeingabeGruppeStimmenErfassenModelContext(getStimmeingabeErfassenContext(), getLayoutStimmeingabeGruppeStimmen());
  }

  protected StimmeingabeListenkandidaturStimmenErfassenModelBuilder getStimmeingabeListenkandidaturStimmenErfassenModelBuilder() {
    return getStimmeingabeErfassenDescription().getStimmeingabeListenkandidaturStimmenErfassenModelBuilder();
  }

  protected StimmeingabeReferendumOptionStimmenErfassenModelBuilder getStimmeingabeReferendumOptionStimmenErfassenModelBuilder() {
    return getStimmeingabeErfassenDescription().getStimmeingabeReferendumOptionStimmenErfassenModelBuilder();
  }

  protected StimmeingabePlausiBuilder getStimmeingabePlausiBuilder() {
    return getStimmeingabeErfassenDescription().getStimmeingabePlausiBuilder();
  }

  protected abstract StimmeingabeErfassenDescription getStimmeingabeErfassenDescription();

  @Redirect
  @UseCaseSecure(UseCasePermission.STIMMEINGABE_CREATE)
  public String save() {
    stimmeingabeBoundaryCreate.save(getAktuellerErgebniseingangOverrideErgebniseingangsart(), getLetzterErgebniseingangAktuell(), getStimmeingabePlausiBuilder());

    addInfoMessage("stimmeingabe.erfassen.action.save.success.message");

    return resetEingabeInternal();
  }

  public boolean isBtnSaveRendered() {
    return hasPermission(UseCasePermission.STIMMEINGABE_CREATE) && !isErfassungGesperrt();
  }

  public boolean isBtnAusbleibendRendered() {
    return hasPermission(UseCasePermission.GEBIET_ERGEBNISEINGABESTATUS_SETZEN_AUSBLEIBEND) && !isErfassungGesperrt() && !getStimmeingabeErfassenContext().hasLetzterErgebniseingangAktuell()
        && !isErgebniseingangsartVerschiedenVonWahl();
  }

  public boolean isBtnSaveDisabled() {
    return !isGebietLaesstEingabeZu();
  }

  public boolean isBtnAusbleibendDisabled() {
    return !isGebietLaesstEingabeZu() || isErfasstFuerAndereGebiete();
  }

  public boolean isBtnResetRendered() {
    return !isErfassungGesperrt();
  }

  public boolean isBtnResetDisabled() {
    return false;
  }

  @Redirect
  public String reset() {
    requestScopedState.reset();
    stimmeingabeErfassenViewStatus = null;
    return getViewId();
  }

  @Redirect
  public String resetEingabe() {
    return resetEingabeInternal();
  }

  private String resetEingabeInternal() {
    resetGebietsbaum();
    return reset();
  }

  // ELECT-9913
  public boolean isBtnRefreshRendered() {
    return !isBtnSaveRendered() && !isBtnResetRendered() && !isBtnErfassungEntsperrenRendered() && !isBtnFehlerBestaetigenRendered();
  }

  public boolean isBtnRefreshDisabled() {
    return false;
  }

  @Redirect
  public String refresh() {
    return resetEingabe();
  }

  @Redirect
  public String gotoAusbleibend() {
    return StimmeingabeAusbleibendView.VIEW_ID;
  }

  public boolean isBtnCancelRendered() {
    return isErfassungAufUnterseite();
  }

  @Redirect
  public String cancel() {
    resetGebietsbaum();
    return goBack();
  }

  public boolean isBtnTabelleAusdruckenRendered() {
    return /*
            * ELECTBB-286 auch Muss-Fehler isErfassungGesperrt() // &&
            */
    isLetzterErgebniseingangAktuellVorhanden() //
        && hasPermission(UseCasePermission.STIMMEINGABE_TABELLE_AUSDRUCKEN);
  }

  public boolean isTabelleAusdruckenHeaderRendered() {
    return isBtnTabelleAusdruckenRendered();
  }

  public List<String> getTabelleAusdruckenHeader() {
    List<String> result = new ArrayList<>();

    if (isLetzterErgebniseingangAktuellVorhanden()) {
      Wahl wahl = getWahl();

      Ergebniseingang ergebniseingang = getLetzterErgebniseingangAktuell();

      String ergebnisStatusInfo = getErgebnisStatusInfo().getPrintInfo(); // ELECTKOM-471

      String erfasstDurch = ergebniseingang.getErfasser();
      Date erfasstAm = ergebniseingang.getZeitstempel();

      result.add(getMessage("stimmeingabe.tabelle.ausdrucken.header.wahl", ergebnisStatusInfo, wahl.getName(), userLocale.formatDate(wahl.getWahldatum()))); // ELECTKOM-489
      result.add(getMessage("stimmeingabe.tabelle.ausdrucken.header.gebiet", ergebnisStatusInfo, getGebietName())); // ELECTKOM-489
      result.add(getMessage("stimmeingabe.tabelle.ausdrucken.header.erfassung", ergebnisStatusInfo, erfasstDurch, userLocale.formatDate(erfasstAm), userLocale.formatTimeHHmm(erfasstAm)));
    }

    return result;
  }

  // WICHTIG: Bei Änderungen auch die Logik in ErgebniseingangStatusView anpassen!!!
  public boolean isBtnFehlerBestaetigenRendered() {
    boolean result = isErfassungGesperrt() //
        && isStimmeingabeErfassenViewStatusFehlerBestaetigen() //
        && isGebietLaesstEingabeZu(() -> true/* ELECT-9200 Kann-Fehler bestaetigen unabhängig von der Herkunft */ );

    return getStimmeingabeSecurity().isBtnFehlerBestaetigenRendered(getStimmeingabeErfassenContext(), result);
  }

  // WICHTIG: Bei Änderungen auch die Logik in ErgebniseingangStatusView anpassen!!!
  public boolean isBtnFehlerBestaetigenDisabled() {
    return false;
  }

  @Redirect
  @UseCaseSecure(UseCasePermission.STIMMEINGABE_FEHLER_BESTAETIGEN)
  public String bestaetigeFehler() {
    stimmeingabeBoundaryCreate.saveUndKannFehlerFuerBenutzerBestaetigen(getAktuellerErgebniseingangOverrideErgebniseingangsart(), getLetzterErgebniseingangAktuell(), getStimmeingabePlausiBuilder());

    return resetEingabeInternal();
  }

  public boolean isBtnEingaengeLoeschenRendered() {
    if (isErfassungAufUnterseite()) {
      return false; // ELECT-9329, Aufruf auf einer Unterseite löscht nicht nur die angezeigten Werte, sondern alle Stimmeingaben!
    }

    StimmeingabeErfassenViewContext context = getStimmeingabeErfassenContext();
    boolean result = isGebietLaesstEingabeZu() && context.hasLetzterErgebniseingangAktuell();
    return getStimmeingabeSecurity().isBtnEingaengeLoeschenRendered(context, result);
  }

  public boolean isBtnEingaengeLoeschenDisabled() {
    return false;
  }

  public String getBtnEingaengeLoeschenLabel() {
    return getMessage("stimmeingabe.erfassen.action.delete.all.entities");
  }

  public String getEingaengeLoeschenConfirmMessage() {
    return getMessage("stimmeingabe.erfassen.action.delete.all.confirm.message", getGebietName());
  }

  // eg. Neues Ergebnis erfassen
  public boolean isBtnErfassungEntsperrenRendered() {
    boolean result = isErfassungGesperrt() && (isStimmeingabeErfassenViewStatusFehlerBestaetigen() || isStimmeingabeErfassenViewStatusNurLesen());

    return getStimmeingabeSecurity().isBtnErfassungEntsperrenRendered(getStimmeingabeErfassenContext(), result);
  }

  public boolean isBtnNiederschriftKorrigierenRendered() {
    return isErgebniseingangsartEndgueltig() && hasPermission(UseCasePermission.NIEDERSCHRIFT_KORREKTUR_CREATE) && isBtnErfassungEntsperrenRendered();
  }

  public boolean isBtnErfassungEntsperrenDisabled() {
    return !isGebietLaesstEingabeZu();
  }

  public boolean isBtnNiederschriftKorrigierenDisabled() {
    return isBtnErfassungEntsperrenDisabled();
  }

  @Redirect
  public String entsperreErfassung() {
    setStimmeingabeErfassenViewStatus(StimmeingabeErfassenViewStatus.EINGABE);

    resetGebietsbaum();
    return getViewId();
  }

  @Redirect
  public String niederschriftKorrigieren() {
    return NiederschriftKorrekturCreateView.VIEW_ID;
  }

  public boolean isBtnSaveFiktiveWerteRendered() {
    if (isErfassungAufUnterseite() || !isSaveFiktiveWerteImContextErlaubt()) {
      return false;
    }

    return getStimmeingabeSecurity().isSaveFiktiveWerteErlaubt();
  }

  protected boolean isSaveFiktiveWerteImContextErlaubt() {
    return getStimmeingabeSecurity().isSaveFiktiveWerteErlaubt(getStimmeingabeErfassenContext()) && isGebietLaesstEingabeZu();
  }

  public boolean isBtnSaveFiktiveWerteDisabled() {
    return false;
  }

  @Redirect
  @UseCaseSecure(UseCasePermission.STIMMEINGABE_TESTWERTE_FUELLEN)
  public String saveFiktiveWerte() {
    try {
      if (isSaveFiktiveWerteImContextErlaubt()) {
        Wahldurchfuehrungsgebiet gebiet = getGebiet();
        Ergebniseingangsart ergebniseingangsart = getErgebniseingangsart();
        log("Erzeuge fiktive Stimmeingaben [gebiet = {}, ergebniseingangsart = {}] - START", gebiet.getNummer(), ergebniseingangsart);

        StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten = stimmeingabeFiktivewerteCreate.berechneStimmenhochrechnerAusgabedatenNachDefaultKonfigurationen(gebiet, ergebniseingangsart);

        HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus = stimmenhochrechnerAusgabedaten.getAusfuehrbarkeitStatus();
        if (ausfuehrbarkeitStatus.isAlgorithmusAusfuehrbar()) {
          stimmeingabeFiktivewerteCreate.createAndBehebePlausiFehler(gebiet, ergebniseingangsart, getErgebniseingangErfasser(), getLetzterErgebniseingangAktuell(),
              isErgebniseingangsartVerschiedenVonWahl(), stimmenhochrechnerAusgabedaten);
          log("Erzeuge fiktive Stimmeingaben [gebiet = {}, ergebniseingangsart = {}] - DONE", gebiet.getNummer(), ergebniseingangsart);
        } else {
          log(messages.lokalisiere(ausfuehrbarkeitStatus));
          addErrorMessage(FacesMessageUtil.getEnumMessageKey(ausfuehrbarkeitStatus));
        }
      }
    } catch (Exception ex) {
      addErrorMessage(ex);
    }

    return resetEingabeInternal();
  }

  protected boolean hasPermission(UseCasePermission permission) {
    return getPrincipal().hasUseCasePermission(permission);
  }

  protected StimmeingabeErfassenViewContext getStimmeingabeErfassenContext() {
    if (requestScopedState.getStimmeingabeErfassenViewContext() == null) {
      requestScopedState.setStimmeingabeErfassenViewContext(createStimmeingabeErfassenContext());
    }

    return requestScopedState.getStimmeingabeErfassenViewContext();
  }

  private StimmeingabeErfassenViewContext createStimmeingabeErfassenContext() {
    Ergebniseingangsart ergebniseingangsart = getErgebniseingangsart();
    Ergebniseingangsherkunft ergebniseingangsherkunft = getErgebniseingangsherkunft();

    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getGebiet();
    Wahldurchfuehrung wahldurchfuehrung = wahldurchfuehrungsgebiet.getWahldurchfuehrung();

    ErgebniseingangErfasser erfasser = getErgebniseingangErfasser();

    Ergebniseingang aktuellerErgebniseingang = new Ergebniseingang();
    aktuellerErgebniseingang.setErgebniseingangsart(ergebniseingangsart);
    aktuellerErgebniseingang.setErgebniseingangsherkunft(ergebniseingangsherkunft);
    aktuellerErgebniseingang.setWahldurchfuehrungsgebiet(wahldurchfuehrungsgebiet);
    aktuellerErgebniseingang.setErfasser(erfasser.getKennung());

    List<Ergebniseingabestufe> ergebniseingabestufenDesc = new ArrayList<>(collectErgebniseingabestufen());
    ergebniseingabestufenDesc.sort(Ergebniseingabestufe.REIHENFOLGE_DESC);

    Ergebniseingang letzterErgebniseingangErfasst = null;
    Map<Ergebniseingabestufe, Ergebniseingang> letzterErgebniseingangErfasstPerErgebniseingabestufe = new HashMap<>();

    for (Ergebniseingabestufe ergebniseingabestufe : ergebniseingabestufenDesc) {
      Ergebniseingang letzterErgebniseingangInErgebniseingabestufe = wahldurchfuehrungsgebiet.getLetzterErgebniseingangInErgebniseingabestufe(ergebniseingangExtendedDAO, ergebniseingangsart,
          ergebniseingabestufe);

      if (letzterErgebniseingangInErgebniseingabestufe != null) {
        if (letzterErgebniseingangErfasst == null) {
          letzterErgebniseingangErfasst = letzterErgebniseingangInErgebniseingabestufe;
        }

        letzterErgebniseingangErfasstPerErgebniseingabestufe.put(ergebniseingabestufe, letzterErgebniseingangInErgebniseingabestufe);
      }
    }

    Ergebniseingabestufe ergebniseingabestufe = findAktuellerErgebniseingangErgebniseingabestufe(erfasser, letzterErgebniseingangErfasst, letzterErgebniseingangErfasstPerErgebniseingabestufe);
    aktuellerErgebniseingang.setErgebniseingabestufe(ergebniseingabestufe);

    Ergebniseingang letzterErgebniseingangAktuell = letzterErgebniseingangErfasstPerErgebniseingabestufe.get(ergebniseingabestufe); // Stimmeingaben mit Daten aus gleicher Stufe vorbelegen
    List<Stimmeingabe> letzterErgebniseingangAktuellStimmeingaben = new ArrayList<>();
    List<Stimmeingabefehler> letzterErgebniseingangAktuellStimmeingabefehler = new ArrayList<>();

    Ergebniseingang letzterErgebniseingangAktuellGueltig = null;
    List<Stimmeingabe> letzterErgebniseingangAktuellGueltigStimmeingaben = new ArrayList<>();

    if (letzterErgebniseingangAktuell != null) {
      letzterErgebniseingangAktuellStimmeingaben = collectStimmeingaben(letzterErgebniseingangAktuell);
      letzterErgebniseingangAktuellStimmeingabefehler = collectStimmeingabefehler(letzterErgebniseingangAktuell);

      if (letzterErgebniseingangAktuell.isGueltig()) {
        letzterErgebniseingangAktuellGueltig = letzterErgebniseingangAktuell;
        letzterErgebniseingangAktuellGueltigStimmeingaben = letzterErgebniseingangAktuellStimmeingaben;
      } else {
        letzterErgebniseingangAktuellGueltig = findLetzterErgebniseingangInErgebniseingabestufeGueltig(wahldurchfuehrungsgebiet, ergebniseingangsart, ergebniseingabestufe);
        letzterErgebniseingangAktuellGueltigStimmeingaben = collectStimmeingaben(letzterErgebniseingangAktuellGueltig);
      }
    }

    Ergebniseingang vergleichErgebniseingang = findVergleichErgebniseingang(wahldurchfuehrungsgebiet, ergebniseingangsart, ergebniseingabestufenDesc, ergebniseingabestufe,
        letzterErgebniseingangAktuell);
    List<Stimmeingabe> vergleichErgebniseingangStimmeingaben = new ArrayList<>();

    if (vergleichErgebniseingang != null) {
      vergleichErgebniseingangStimmeingaben = collectStimmeingaben(vergleichErgebniseingang);
    }

    Ergebniseingang letzterErgebniseingangAktuellVorlaeufig = null;
    List<Stimmeingabe> letzterErgebniseingangAktuellVorlaeufigStimmeingaben = new ArrayList<>();

    if (wahldurchfuehrung != null && wahldurchfuehrung.getWahlart() != null && wahldurchfuehrung.getWahlart().getErfassungUebernahmeManuell() != null) {
      WahlErgebniseingangUebernahmeManuell erfassungUebernahmeManuell = wahldurchfuehrung.getWahlart().getErfassungUebernahmeManuell(); // ELECT-8352

      boolean aktuellerErgebniseingangEndgueltigVorbelegbar = aktuellerErgebniseingang.isErgebniseingangsartEndgueltig() && letzterErgebniseingangAktuell == null
          && /* ELECT-11806 */ wahldurchfuehrungsgebiet.isEingabeMoeglich(ergebniseingangsart, ergebniseingangsherkunft, () -> false);

      if (aktuellerErgebniseingangEndgueltigVorbelegbar && erfassungUebernahmeManuell.isErsteEingabeEndgueltigMitLetzterVorlaeufigVorbelegen()) {
        letzterErgebniseingangAktuellVorlaeufig = wahldurchfuehrungsgebiet.getLetzterErgebniseingangAktuell(ergebniseingangExtendedDAO, Ergebniseingangsart.VORLAEUFIG);

        if (letzterErgebniseingangAktuellVorlaeufig != null && (!erfassungUebernahmeManuell.isLetzteVorlaeufigeMussGueltigSein() || letzterErgebniseingangAktuellVorlaeufig.isGueltig())) {
          letzterErgebniseingangAktuellVorlaeufigStimmeingaben = collectStimmeingaben(letzterErgebniseingangAktuellVorlaeufig);

          if (wahldurchfuehrungsgebiet.hasAnyAnzahlWahlberechtigte()) { // ELECTKOM-478
            letzterErgebniseingangAktuellVorlaeufigStimmeingaben.removeIf(this::isStimmeingabeKopfzahlWahlberechtigte);
          }

          addInfoMessage("stimmeingabe.erfassen.vorbelegt.mit.vorlaeufiger.eingabe");
        }
      }
    }

    return new StimmeingabeErfassenViewContext( //
        aktuellerErgebniseingang, //

        letzterErgebniseingangAktuell, letzterErgebniseingangAktuellStimmeingaben, letzterErgebniseingangAktuellStimmeingabefehler, //

        letzterErgebniseingangAktuellGueltig, letzterErgebniseingangAktuellGueltigStimmeingaben, //

        letzterErgebniseingangAktuellVorlaeufig, letzterErgebniseingangAktuellVorlaeufigStimmeingaben, //

        vergleichErgebniseingang, vergleichErgebniseingangStimmeingaben,

        letzterErgebniseingangErfasst, letzterErgebniseingangErfasstPerErgebniseingabestufe,

        createStimmeingabeErfassenViewRequestScopedStateProxy(), getStimmeingabeKopfzahlErfassenLabelGenerator(), getStimmeingabeGruppeStimmenErfassenLabelGenerator(),
        getStimmeingabeListenkandidaturStimmenErfassenLabelGenerator(), getStimmeingabeReferendumOptionStimmenErfassenLabelGenerator(), gebietNummerFormatter,
        new WahlvorschlaegeSupplier(wahldurchfuehrung, wahlvorschlagExtendedDAO), getEntityAufStimmzettelDaten(), //
        getFindByErfasstFuerGebiete() //
    );
  }

  @Nonnull
  private Function<Gebiet, List<Wahldurchfuehrungsgebiet>> getFindByErfasstFuerGebiete() {
    if (wahldurchfuehrungsgebietExtendedDAO == null) {
      // tests
      return x -> emptyList();
    }
    return wahldurchfuehrungsgebietExtendedDAO::findByErfasstFuerGebiete;
  }

  protected boolean isStimmeingabeKopfzahlWahlberechtigte(Stimmeingabe stimmeingabe) {
    return stimmeingabe != null && stimmeingabe.isSystemGruppenStimmeingabe() && systemGruppeEigenschaften.isGruppeEqualsSystemGruppe(stimmeingabe.getGruppe(), SystemGruppe.WAHLBERECHTIGTE);
  }

  protected List<Stimmeingabe> collectStimmeingaben(@Nullable Ergebniseingang ergebniseingang) {
    if (ergebniseingang != null) {
      /*
       * HINT: das auch mögliche LAZY-Loading per letzterErgebniseingang.getStimmeingaben() gibt nur Proxies zurück, die mit Unterklassen, wie ZwischensummeEingabe, nicht mehr gut funktionieren...
       *
       * Cf. http://java-success.blogspot.de/2012/09/understanding-hibernate-proxy-objects.html
       */
      List<Stimmeingabe> stimmeingaben = stimmeingabeExtendedDAO.findByErgebniseingang(ergebniseingang);
      stimmeingaben.forEach(Stimmeingabe::getStimmeingabeTK); // lazy loading

      return stimmeingaben;
    }

    return emptyList();
  }

  protected List<Stimmeingabefehler> collectStimmeingabefehler(@Nullable Ergebniseingang ergebniseingang) {
    List<Stimmeingabefehler> result = emptyList();

    if (ergebniseingang != null && !ergebniseingang.isGueltig()) {
      result = stimmeingabefehlerExtendedDAO.findByErgebniseingangAndFehlerart(ergebniseingang, Stimmeingabefehlerart.ERROR);

      if (result.isEmpty()) {
        result = stimmeingabefehlerExtendedDAO.findByErgebniseingang(ergebniseingang);
      }
    }

    return result;
  }

  protected StimmeingabeErfassenViewRequestScopedState createStimmeingabeErfassenViewRequestScopedStateProxy() {
    return AbstractStimmeingabeErfassenView.this::getGruppenImGebiet;
  }

  protected StimmeingabeReferendumOptionStimmenErfassenLabelGenerator getStimmeingabeReferendumOptionStimmenErfassenLabelGenerator() {
    return getStimmeingabeErfassenDescription().getStimmeingabeReferendumOptionStimmenErfassenLabelGenerator();
  }

  protected StimmeingabeListenkandidaturStimmenErfassenLabelGenerator getStimmeingabeListenkandidaturStimmenErfassenLabelGenerator() {
    return getStimmeingabeErfassenDescription().getStimmeingabeListenkandidaturStimmenErfassenLabelGenerator();
  }

  protected StimmeingabeGruppeStimmenErfassenLabelGenerator getStimmeingabeGruppeStimmenErfassenLabelGenerator() {
    return getStimmeingabeErfassenDescription().getStimmeingabeGruppeStimmenErfassenLabelGenerator();
  }

  protected StimmeingabeKopfzahlErfassenLabelGenerator getStimmeingabeKopfzahlErfassenLabelGenerator() {
    return getStimmeingabeErfassenDescription().getStimmeingabeKopfzahlErfassenLabelGenerator();
  }

  @Override
  public ReentrantLock getLock() {
    return lock;
  }

  @Override
  public String getName() {
    return getClass().getName();
  }

  @Override
  public String getLogMessageContext() {
    return "Principal: " + principal.getBenutzerKennung() + ", Gebiet: " + getGebietName() + ", Wahl: " + getWahl() + ", Gebiet: " + getGebiet();
  }

  @Redirect
  @UseCaseSecure(UseCasePermission.STIMMEINGABE_DELETE)
  public String eingaengeLoeschen() {
    Wahldurchfuehrungsgebiet gebiet = getGebiet();

    loescheStimmeingabenNonStatic(singletonList(gebiet), getSelectedErgebnis(), stimmeingabeErfassungLoeschen, getErgebniseingangErfasser());

    addInfoMessage("stimmeingabe.erfassen.action.delete.all.success.message", gebiet.getName());

    return resetEingabeInternal();
  }

  protected void loescheStimmeingabenNonStatic(List<Wahldurchfuehrungsgebiet> gebiete, Ergebnis ergebnis, StimmeingabeErfassungLoeschen stimmeingabeErfassungLoeschen,
      ErgebniseingangErfasser erfasser) {
    loescheStimmeingaben(gebiete, ergebnis, stimmeingabeErfassungLoeschen, erfasser);
  }

  public static void loescheStimmeingaben(Collection<Wahldurchfuehrungsgebiet> gebiete, Ergebnis ergebnis, StimmeingabeErfassungLoeschen stimmeingabeErfassungLoeschen,
      ErgebniseingangErfasser erfasser) {
    loescheStimmeingaben(gebiete, ergebnis, stimmeingabeErfassungLoeschen, erfasser, asList(Ergebniseingabestufe.values()));
  }

  public static void loescheStimmeingaben(Collection<Wahldurchfuehrungsgebiet> gebiete, Ergebnis ergebnis, StimmeingabeErfassungLoeschen stimmeingabeErfassungLoeschen,
      ErgebniseingangErfasser erfasser, Collection<Ergebniseingabestufe> ergebniseingabestufen) {
    // QuickHack für ELECTBB-339
    long tryCont = 0;

    boolean proceed = false;

    do {
      tryCont++;

      try {
        stimmeingabeErfassungLoeschen.eingabenLoeschen(gebiete, ergebnis, erfasser, ergebniseingabestufen);

        proceed = true;
      } catch (Exception ex) {
        if (tryCont < MAX_TRIES) {
          LOG.warn("Fehler beim Löschen der Stimmen, Versuch: " + tryCont + " von " + MAX_TRIES + " Fehlermeldung: " + ex.getMessage());

          try {
            Thread.sleep(ElectRandom.getRandom().nextInt(500));
          } catch (InterruptedException iex) {
            throw new RuntimeException(iex.getMessage(), iex.getCause());
          }
        } else {
          throw ex;
        }
      }
    } while (!proceed);
  }

  protected void resetGebietsbaum() {
    gebietsbaumView.reset();
  }

  protected void redirectToStimmeingabeErfassenView() {
    String targetViewId = getStimmeingabeErfassenViewId();

    if (suppportsUpdate()) {
      // Wenn Erfassungsgebiet => alles neu laden
      if (!getViewId().equals(targetViewId)) {
        resetGebietsbaum();
        FacesContextUtil.redirectToViewNoRedirectParameter(targetViewId);
      }
    } else {
      resetGebietsbaum();
      FacesContextUtil.redirectToViewNoRedirectParameter(targetViewId);
    }
  }

  protected String getStimmeingabeErfassenViewId() {
    return getStimmeingabeErfassenViewResolver().getViewId();
  }

  protected boolean suppportsUpdate() {
    return false; // ganze Seite laden => ja, macht sonst Probleme, Stimmeingaben anderer Gebiete, Druck geht nicht ELECTSL-54, ...
  }

  private WahlParameterStimmeingabeErfassungsmaskeUmgangBlank resolveUmgangBlankValue(WahlParameterWerte wahlParameterWerte) {
    return resolveWahlParameterWertEnumValueOrDefault(wahlParameterWerte, WahlParameterStimmeingabeErfassungsmaskeUmgangBlank.class,
        WahlParameterStimmeingabeErfassungsmaskeUmgangBlank.defaultValue());
  }

  protected AbstractWahlParameterLoader getWahlParameterLoader() {
    return wahlParameterExtendedLoader;
  }

  public String getPageTitle() {
    return getMessage("stimmeingabe.gruppe.stimmen.erfassen.page.title");
  }

  public String getPageHeader() {
    return getMessage("stimmeingabe.gruppe.stimmen.erfassen.page.title");
  }

  @Override
  public ErgebnisStatusInfo getErgebnisStatusInfo() {
    return new WasErgebnisStatusInfo(getMessages(), getWahl(), getErgebniseingangKonfiguration(), getErgebniseingangsart(), getAktuellerErgebniseingangErgebniseingabestufe());
  }

  public boolean isBtnEingangsartWechselnDisabled() {
    return false;
  }

  public boolean isBtnEingangsartWechselnRendered() {
    return isErgebniseingangsartWechselnUnterstuetzt() && getPrincipal().hasUseCasePermission(UseCasePermission.STIMMEINGABE_ERGEBNISEINGANGSART_WECHSELN);
  }

  @Redirect
  @UseCaseSecure(UseCasePermission.STIMMEINGABE_ERGEBNISEINGANGSART_WECHSELN)
  public String eingangsartWechseln() {
    if (isBtnEingangsartWechselnRendered() && !isBtnEingangsartWechselnDisabled()) {
      getStimmeingabeErfassenViewErgebniseingangsartStore().ergebniseEingangsartWechseln(getWahl());
      resetEingabe();
      addInfoMessage("stimmeingabe.gruppe.stimmen.erfassen.ergebnisart.gewechselt", getMessage(getErgebniseingangsart()));
    }
    return getViewId();
  }

  public String getBtnEingangsartWechselnLabel() {
    boolean erfassenGesperrt = isErfassungGesperrt();
    Ergebniseingangsart ergebniseingangsart = getErgebniseingangsart();

    if (erfassenGesperrt) {
      if (ergebniseingangsart.getArtNachWechsel().isVorlaeufig()) {
        return getMessage("stimmeingabe.gruppe.stimmen.erfassen.ergebnisart.wechseln.vorlaeufig");
      }
      return getMessage("stimmeingabe.gruppe.stimmen.erfassen.ergebnisart.wechseln.endgueltig");
    } else {
      if (ergebniseingangsart.getArtNachWechsel().isVorlaeufig()) {
        return getMessage("stimmeingabe.gruppe.stimmen.erfassen.ergebnisart.wechseln.vorlaeufig.ohne.speichern");
      }
      return getMessage("stimmeingabe.gruppe.stimmen.erfassen.ergebnisart.wechseln.endgueltig.ohne.speichern");
    }
  }

  public String getBtnSaveLabel() {
    if (getErgebniseingangKonfiguration().isWechselErgebniseingangsartenUnterstuetzt(getWahl())) {
      return getMessage("stimmeingabe.ergebnisart.verschieden.edit.action.save", getMessage(getErgebniseingangsart()));
    }

    return getMessage("entity.edit.action.save");
  }

  protected boolean isErgebniseingangsartVerschiedenVonWahl() {
    return getStimmeingabeErfassenViewErgebniseingangsartStore().isErgebniseingangsartVerschiedenVonWahl(getWahl());
  }

  protected EntityAufStimmzettelDaten getEntityAufStimmzettelDaten() {
    return entityAufStimmzettelDatenExtendedDAOFactory.getEntityAufStimmzettelDaten();
  }

  public Map<String, Map<String, Object>> getErfassungshinweise() {
    Map<String, Map<String, Object>> result = new LinkedHashMap<>();

    Wahl wahl = getWahl();
    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getGebiet();

    if (wahl != null && wahldurchfuehrungsgebiet != null) {
      Map<String, Object> entriesStimmvergabe = new LinkedHashMap<>();

      long anzahlStimmenProWaehlerMaximal = anzahlStimmenProWaehlerBestimmer.getAnzahlStimmenProWaehlerMaximal(wahldurchfuehrungsgebiet);

      if (anzahlStimmenProWaehlerMaximal > 0) {
        if (wahl.isWahlMitAnzahlSitze()) {
          Long anzahlSitze = wahldurchfuehrungsgebiet.getAnzahlSitzeImWahlgebiet();

          if (anzahlSitze != null && anzahlSitze > 0 && !Objects.equals(anzahlSitze, anzahlStimmenProWaehlerMaximal)) {
            entriesStimmvergabe.put(getMessage(GebietConstants.MESSAGE_KEY_ANZAHL_SITZE_EINZAHL), formatNumber(anzahlSitze));
          }
        }

        String anzahlStimmenProWaehlerMaximalLabelMsgKey = WahlartConstants.MESSAGE_KEY_ANZAHL_STIMMEN_PRO_WAEHLER_EINZAHL;

        boolean wahlMitDirektstimmen = wahl.isWahlMitDirektkandidatengebieten();
        boolean wahlMitListenstimmen = wahl.isWahlMitListengebieten();
        boolean wahlMitReferendumstimmen = wahl.isWahlMitReferendengebieten();

        if (wahlMitReferendumstimmen) {
          anzahlStimmenProWaehlerMaximalLabelMsgKey = "stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.ANZAHL_STIMMEN_PRO_WAEHLER_PRO_REFERENDUM_VORLAGE";
        } else if (wahlMitDirektstimmen && wahlMitListenstimmen) {
          anzahlStimmenProWaehlerMaximalLabelMsgKey = "stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.ANZAHL_STIMMEN_PRO_WAEHLER_PRO_STIMMENART";
        }

        entriesStimmvergabe.put(getMessage(anzahlStimmenProWaehlerMaximalLabelMsgKey), formatNumber(anzahlStimmenProWaehlerMaximal));

        if (anzahlStimmenProWaehlerMaximal > 1) {
          if (wahlMitDirektstimmen) {
            long anzahlStimmenProWaehlerProDirektkandidaturMaximal = anzahlStimmenProWaehlerBestimmer.getAnzahlStimmenProWaehlerProDirektkandidaturMaximal(wahldurchfuehrungsgebiet,
                direktkandidatExtendedDAO::findByGebiet);

            if (anzahlStimmenProWaehlerProDirektkandidaturMaximal > 0) {
              entriesStimmvergabe.put(getMessage("stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.ANZAHL_STIMMEN_PRO_WAEHLER_PRO_DIREKTKANDIDATUR"),
                  formatNumber(anzahlStimmenProWaehlerProDirektkandidaturMaximal));
            }
          }

          if (wahlMitListenstimmen && wahl.isWahlMitListenkandidatenStimmen()) {
            long anzahlStimmenProWaehlerProListenkandidaturMaximal = anzahlStimmenProWaehlerBestimmer.getAnzahlStimmenProWaehlerProListenkandidaturMaximal(wahldurchfuehrungsgebiet,
                listeExtendedDAO::findByGebiet);

            if (anzahlStimmenProWaehlerProListenkandidaturMaximal > 0) {
              entriesStimmvergabe.put(getMessage("stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.ANZAHL_STIMMEN_PRO_WAEHLER_PRO_LISTENKANDIDATUR"),
                  formatNumber(anzahlStimmenProWaehlerProListenkandidaturMaximal));
            }
          }
        }

        boolean waehlerBenannteWahlvorschlaegeErfassbar = isGebietWaehlerBenannteWahlvorschlaegeErfassbar();

        if (waehlerBenannteWahlvorschlaegeErfassbar) {
          entriesStimmvergabe.put(getMessage("stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.WAEHLER_BENANNTE_WAHLVORSCHLAEGE_ERFASSBAR"),
              getMessages().lokalisiere(waehlerBenannteWahlvorschlaegeErfassbar));
        }
      }

      WahlartErgebnisartVorlaeufigErfassungsart wahlartErgebnisartVorlaeufigErfassungsart = resolveWahlParameterWertEnumValueOrDefault(
          WahlParameterWerte.STIMMEINGABE_WAHLART_ERGEBNISART_VORLAEUFIG_ERFASSUNGSART, WahlartErgebnisartVorlaeufigErfassungsart.class, WahlartErgebnisartVorlaeufigErfassungsart.ERGEBNIS_KOMPLETT);

      if (isErgebniseingangsartVorlaeufig() && WahlartErgebnisartVorlaeufigErfassungsart.TREND_STIMMZETTEL_NUR_EINZELNER_WAHLVORSCHLAG.equals(wahlartErgebnisartVorlaeufigErfassungsart)) {
        Long anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendertWichtungInProzent = getWahlParameterLoader().resolveLongValue(getWahl(),
            WahlParameterWerte.STIMMEINGABE_WAHLART_ERGEBNISART_VORLAEUFIG_ERFASSUNGSART_TREND_STIMMZETTEL_NUR_EINZELNER_WAHLVORSCHLAG_WICHTUNG);

        if (anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendertWichtungInProzent != null && anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendertWichtungInProzent > 0) {
          entriesStimmvergabe.put(getMessage("WahlParameterWerte.STIMMEINGABE_WAHLART_ERGEBNISART_VORLAEUFIG_ERFASSUNGSART_TREND_STIMMZETTEL_NUR_EINZELNER_WAHLVORSCHLAG_WICHTUNG.label"),
              formatNumber(anzahlStimmzettelNurEinzelnerWahlvorschlagVeraendertWichtungInProzent) + " %");
        }
      }

      if (!entriesStimmvergabe.isEmpty()) {
        result.put(getMessage("stimmeingabe.erfassen.erfassungshinweise.stimmvergabe.header"), entriesStimmvergabe);
      }

      Map<String, Object> entriesEinstellungen = new LinkedHashMap<>();
      entriesEinstellungen.put(getMessage(CustomParameter.generateKurznameMsgKey(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_UMGANG_BLANK_ERFASSEN)),
          getStimmeingabeErfassenUmgangBlankDisplayValue());
      entriesEinstellungen.put(getMessage(CustomParameter.generateKurznameMsgKey(WahlParameterWerte.STIMMEINGABE_ERFASSUNGSMASKE_UMGANG_BLANK_ANZEIGEN)),
          getStimmeingabeAnzeigenUmgangBlankDisplayValue());

      if (!entriesEinstellungen.isEmpty()) {
        result.put(getMessage("stimmeingabe.erfassen.erfassungshinweise.einstellungen.header"), entriesEinstellungen);
      }
    }

    return result;
  }

  protected <V extends Enum< ? >> V resolveWahlParameterWertEnumValueOrDefault(WahlParameterWerte wahlParameterWerte, Class<V> enumClass, V defaultValue) {
    V value = getWahlParameterLoader().resolveEnumValue(getWahl(), wahlParameterWerte, enumClass);

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

    return defaultValue;
  }

  private String formatNumber(Long anzahlStimmenProWaehlerMaximal) {
    return getUserLocale().formatNumber(anzahlStimmenProWaehlerMaximal);
  }

  protected Ergebniseingangsherkunft getErgebniseingangsherkunft() {
    return ERGEBNISEINGANGSHERKUNFT_MANUELLE_ERFASSUNG;
  }

  protected BooleanSupplier getEingangsherkunftIgnorieren() {
    return () -> getPrincipal().hasUseCasePermission(UseCasePermission.STIMMEINGABE_CREATE_EINGANGSHERKUNFT_IGNORIEREN);
  }

  public Map<String, AdvancedDynaFormModel> getModelsLetzteErgebniseingaenge() {
    Map<String, AdvancedDynaFormModel> result = new LinkedHashMap<>();

    List<Ergebniseingabestufe> ergebniseingabestufenDesc = new ArrayList<>(collectErgebniseingabestufen());
    ergebniseingabestufenDesc.sort(Ergebniseingabestufe.REIHENFOLGE_DESC);

    boolean ergebniseingabestufePropertyRendered = ergebniseingabestufenDesc.size() > 1;

    Ergebniseingang letzterErgebniseingangAktuell = getLetzterErgebniseingangAktuell();
    Ergebniseingang letzterErgebniseingangAktuellGueltig = null;

    if (letzterErgebniseingangAktuell != null) {
      if (letzterErgebniseingangAktuell.isGueltig()) {
        result.put(getMessage("stimmeingabe.erfassen.letzter.gueltiger.ergebniseingang"), createModelLetzterErgebniseingang(letzterErgebniseingangAktuell, ergebniseingabestufePropertyRendered));
      } else {
        result.put(getMessage("stimmeingabe.erfassen.letzter.ergebniseingang"), createModelLetzterErgebniseingang(letzterErgebniseingangAktuell, ergebniseingabestufePropertyRendered));

        letzterErgebniseingangAktuellGueltig = getLetzterErgebniseingangAktuellGueltig();

        if (letzterErgebniseingangAktuellGueltig != null && !letzterErgebniseingangAktuellGueltig.equals(letzterErgebniseingangAktuell)) {
          result.put(getMessage("stimmeingabe.erfassen.letzter.gueltiger.ergebniseingang"),
              createModelLetzterErgebniseingang(letzterErgebniseingangAktuellGueltig, ergebniseingabestufePropertyRendered));
        }
      }
    }

    for (Ergebniseingabestufe ergebniseingabestufe : ergebniseingabestufenDesc) {
      Ergebniseingang letzterErgebniseingangInErgebniseingabestufe = getLetzterErgebniseingangErfasstInErgebniseingabestufe(ergebniseingabestufe);

      if (letzterErgebniseingangInErgebniseingabestufe != null) {
        result.put(getMessage("stimmeingabe.erfassen.letzter.ergebniseingang." + ergebniseingabestufe.name()),
            createModelLetzterErgebniseingang(letzterErgebniseingangInErgebniseingabestufe, ergebniseingabestufePropertyRendered));
      }
    }

    return result;
  }

  private AdvancedDynaFormModel createModelLetzterErgebniseingang(Ergebniseingang ergebniseingang, boolean ergebniseingabestufePropertyRendered) {
    FormBuilder formBuilder = new FormBuilder();

    ErgebniseingangProperties ergebniseingangProperties = ergebniseingang.getProperties();

    formBuilder.addProperty(ergebniseingangProperties.getZeitstempelProperty());
    formBuilder.addProperty(ergebniseingangProperties.getErfasserProperty());

    if (ergebniseingabestufePropertyRendered) {
      formBuilder.addProperty(ergebniseingangProperties.getErgebniseingabestufeProperty());
    }

    formBuilder.addProperty(ergebniseingangProperties.getErgebniseingangsherkunftProperty());
    formBuilder.addProperty(ergebniseingangProperties.getErgebniseingangsfehlerstatusProperty());

    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getGebiet();

    if (letzterErgebniseingangCustomInformationResolver.isCustomInformationVorhanden(wahldurchfuehrungsgebiet, ergebniseingang)) {
      formBuilder.addProperty(new ReadOnlyStringProperty(letzterErgebniseingangCustomInformationResolver.getCustomInformationLabel(wahldurchfuehrungsgebiet, ergebniseingang),
          letzterErgebniseingangCustomInformationResolver.getCustomInformation(wahldurchfuehrungsgebiet, ergebniseingang)));
    }

    return formBuilder.globalReadonly().createModel();
  }

  protected boolean isLetzterErgebniseingangAktuellVorhandenHavingStatusGueltig() {
    return isLetzterErgebniseingangAktuellVorhanden() && getLetzterErgebniseingangAktuell().isGueltig();
  }

  protected boolean isLetzterErgebniseingangAktuellVorhandenHavingStatusUngueltig() {
    return isLetzterErgebniseingangAktuellVorhandenHavingStatusUngueltigMussFehler() || isLetzterErgebniseingangAktuellVorhandenHavingStatusUngueltigKannFehler();
  }

  protected boolean isLetzterErgebniseingangAktuellVorhandenHavingStatusUngueltigMussFehler() {
    return isLetzterErgebniseingangAktuellVorhanden() && getLetzterErgebniseingangAktuell().isUngueltigMussFehler();
  }

  protected boolean isLetzterErgebniseingangAktuellVorhandenHavingStatusUngueltigKannFehler() {
    return isLetzterErgebniseingangAktuellVorhanden() && getLetzterErgebniseingangAktuell().isUngueltigKannFehler();
  }

  protected boolean isLetzterErgebniseingangAktuellVorhandenHavingStatusFiktiv() {
    return isLetzterErgebniseingangAktuellVorhanden() && getLetzterErgebniseingangAktuell().isFiktiv();
  }

  public boolean isLetzterErgebniseingangAktuellVorhanden() {
    return getLetzterErgebniseingangAktuell() != null;
  }

  protected Ergebniseingang getLetzterErgebniseingangAktuell() {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangAktuell();
  }

  protected Ergebniseingang getLetzterErgebniseingangAktuellGueltig() {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangAktuellGueltig();
  }

  protected Ergebniseingang getLetzterErgebniseingangErfasst() {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangErfasst();
  }

  protected Ergebniseingang getLetzterErgebniseingangErfasstInErgebniseingabestufe(Ergebniseingabestufe ergebniseingabestufe) {
    return getStimmeingabeErfassenContext().getLetzterErgebniseingangErfasstInErgebniseingabestufe(ergebniseingabestufe);
  }

  protected boolean isVergleichErgebniseingangVorhanden() {
    return getVergleichErgebniseingang() != null;
  }

  protected Ergebniseingang getVergleichErgebniseingang() {
    return getStimmeingabeErfassenContext().getVergleichErgebniseingang();
  }

  protected ErgebniseingangErfasser getErgebniseingangErfasser() {
    return ErgebniseingangErfasser.benutzer(getPrincipal());
  }

  protected Ergebniseingangsart getErgebniseingangsart() {
    return getErgebniseingangsart(getWahl());
  }

  protected Ergebniseingangsart getErgebniseingangsart(Wahl wahl) {
    return getStimmeingabeErfassenViewErgebniseingangsartStore().getErgebniseingangsart(wahl);
  }

  public boolean isErgebniseingangsartEndgueltig() {
    return isErgebniseingangsartEndgueltig(getWahl());
  }

  protected boolean isErgebniseingangsartEndgueltig(Wahl wahl) {
    return getErgebniseingangsart(wahl).isEndgueltig();
  }

  public boolean isErgebniseingangsartVorlaeufig() {
    return isErgebniseingangsartVorlaeufig(getWahl());
  }

  protected boolean isErgebniseingangsartVorlaeufig(Wahl wahl) {
    return getErgebniseingangsart(wahl).isVorlaeufig();
  }

  protected List<Ergebniseingabestufe> collectErgebniseingabestufen() {
    return getErgebniseingangKonfiguration().collectErgebniseingabestufen(getWahl());
  }

  protected Ergebniseingabestufe findAktuellerErgebniseingangErgebniseingabestufe(ErgebniseingangErfasser erfasser, Ergebniseingang letzterErgebniseingangErfasst,
      Map<Ergebniseingabestufe, Ergebniseingang> letzterErgebniseingangErfasstPerErgebniseingabestufe) {
    return getErgebniseingangKonfiguration().getErgebniseingabestufeInitial(getWahl());
  }

  protected Ergebniseingang findLetzterErgebniseingangInErgebniseingabestufeGueltig(Ergebniseingang ergebniseingang, Ergebniseingabestufe ergebniseingabestufe) {
    return findLetzterErgebniseingangInErgebniseingabestufeGueltig(ergebniseingang.getWahldurchfuehrungsgebiet(), ergebniseingang.getErgebniseingangsart(), ergebniseingabestufe);
  }

  protected Ergebniseingang findLetzterErgebniseingangInErgebniseingabestufeGueltig(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Ergebniseingangsart ergebniseingangsart,
      Ergebniseingabestufe ergebniseingabestufe) {
    return wahldurchfuehrungsgebiet.getLetzterErgebniseingangInErgebniseingabestufeGueltig(ergebniseingangExtendedDAO, ergebniseingangsart, ergebniseingabestufe);
  }

  @Nullable
  protected Ergebniseingang findVergleichErgebniseingang(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Ergebniseingangsart ergebniseingangsart, List<Ergebniseingabestufe> ergebniseingabestufen,
      Ergebniseingabestufe ergebniseingabestufe, Ergebniseingang letzterErgebniseingangAktuell) {
    return null; // Bisher nur für NL zum Vergleich Ersterfassung vs. Zweiterfassung...
  }

  protected Customer getCustomer() {
    return currentCustomer.getCustomer();
  }

  protected ElectCommonPrincipal getPrincipal() {
    return principal;
  }

  protected StimmeingabeSecurity getStimmeingabeSecurity() {
    return stimmeingabeSecurity;
  }

  protected ErgebniseingangKonfiguration getErgebniseingangKonfiguration() {
    return ergebniseingangKonfiguration;
  }

  protected StimmeingabeErfassenViewResolver getStimmeingabeErfassenViewResolver() {
    return stimmeingabeErfassenViewResolver;
  }

  protected StimmeingabeErfassenViewErgebniseingangsartStore getStimmeingabeErfassenViewErgebniseingangsartStore() {
    return stimmeingabeErfassenViewErgebniseingangsartStore;
  }

  private static void log(String message, Object... params) {
    LOG.info(message, params);
  }

}
