
package de.ivu.elect.business.stimmeingabe.control.plausi;

import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Predicate;

import javax.annotation.Nonnull;

import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.SystemGruppe;
import de.ivu.elect.business.stimmeingabe.boundary.AbstractErgebnisGueltigerEingangDAO;
import de.ivu.elect.business.stimmeingabe.control.ErgebniseingangEingabeHashCodeGenerator;
import de.ivu.elect.business.stimmeingabe.control.ErgebniseingangEingabeHashCodeGenerator.StimmeingabeIndiziert;
import de.ivu.elect.business.stimmeingabe.entity.ErgebnisGueltigerEingang;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.Stimmeingabe;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabePlausiErgebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabePlausiKonfiguration;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.presentation.search.WildcardConverter;

public abstract class AbstractStimmeingabeAnzahlenUngleichStimmeingabeAnzahlenInAnderenErfassungsgebietenPlausi extends AbstractStimmeingabePlausi {

  protected static final long ANZAHL_ZU_VERGLEICHEN_KEINE = 0L;

  protected static final long ANZAHL_ZU_VERGLEICHEN_ALLE = Long.MAX_VALUE;

  private final AbstractErgebnisGueltigerEingangDAO ergebnisGueltigerEingangDAO;

  private final ErgebniseingangEingabeHashCodeGenerator ergebniseingangEingabeHashCodeGenerator;

  private final Wahldurchfuehrung wahldurchfuehrung;

  private final Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet;

  private final Ergebniseingang ergebniseingang;

  private Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenartRelevant;

  private List<Wahldurchfuehrungsgebiet> erfassungsgebieteMitStimmeingabeAnzahlenIdentisch;

  public AbstractStimmeingabeAnzahlenUngleichStimmeingabeAnzahlenInAnderenErfassungsgebietenPlausi(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Ergebniseingang ergebniseingang,
      AbstractErgebnisGueltigerEingangDAO ergebnisGueltigerEingangDAO, ErgebniseingangEingabeHashCodeGenerator ergebniseingangEingabeHashCodeGenerator,
      StimmeingabePlausiKonfiguration stimmeingabePlausiKonfiguration, Messages messages) {
    super(stimmeingabePlausiKonfiguration, messages);

    this.ergebnisGueltigerEingangDAO = ergebnisGueltigerEingangDAO;

    this.ergebniseingangEingabeHashCodeGenerator = ergebniseingangEingabeHashCodeGenerator;

    this.wahldurchfuehrung = wahldurchfuehrungsgebiet.getWahldurchfuehrung();
    this.wahldurchfuehrungsgebiet = wahldurchfuehrungsgebiet;

    this.ergebniseingang = ergebniseingang;

    this.stimmeingabenPerStimmenartRelevant = null;

    this.erfassungsgebieteMitStimmeingabeAnzahlenIdentisch = null;
  }

  @Override
  protected boolean isPlausiResultCalculatedAutomatischKorrigierbar() {
    return false; // Plausi vergleicht nur, also kein SOLL-Wert vorhanden!
  }

  @Override
  protected StimmeingabePlausiResult calculatePlausiResult(@Nonnull Stimmeingabe stimmeingabeHavingPlausiResult) {
    if (collectErgebniseingangsartenRelevant().contains(getErgebniseingangsart()) && getErfassungsgebieteMitStimmeingabeAnzahlenIdentisch().isEmpty()) {
      return StimmeingabePlausiResult.KEIN_FEHLER;
    }

    return createPlausiResultOhneFiktiveWerteFehlerBeheber(stimmeingabeHavingPlausiResult);
  }

  protected abstract Collection<StimmeingabePlausiErgebniseingangsart> collectErgebniseingangsartenRelevant();

  protected Wahldurchfuehrung getWahldurchfuehrung() {
    return wahldurchfuehrung;
  }

  protected Wahldurchfuehrungsgebiet getWahldurchfuehrungsgebiet() {
    return wahldurchfuehrungsgebiet;
  }

  protected Ergebniseingang getErgebniseingang() {
    return ergebniseingang;
  }

  @Override
  protected List<Object> getStimmeingabefehlerBeschreibungParameters(Stimmeingabe stimmeingabe) {
    List<Object> result = new ArrayList<>(super.getStimmeingabefehlerBeschreibungParameters(stimmeingabe));

    List<Wahldurchfuehrungsgebiet> erfassungsgebiete = getErfassungsgebieteMitStimmeingabeAnzahlenIdentisch();

    result.add(erfassungsgebiete.size());
    result.add(erfassungsgebiete.stream().map(Wahldurchfuehrungsgebiet::getNummer).collect(joining(", ")));

    return result;
  }

  protected List<Wahldurchfuehrungsgebiet> getErfassungsgebieteMitStimmeingabeAnzahlenIdentisch() {
    if (erfassungsgebieteMitStimmeingabeAnzahlenIdentisch == null) {
      erfassungsgebieteMitStimmeingabeAnzahlenIdentisch = collectErfassungsgebieteMitStimmeingabeAnzahlenIdentisch(getErgebniseingangsart(), findGebietUebergeordnetRelevant(),
          collectStimmeingabenPerStimmenartRelevant(), collectErgebnisGueltigeEingaengeRelevant());
    }

    return erfassungsgebieteMitStimmeingabeAnzahlenIdentisch;
  }

  private List<Wahldurchfuehrungsgebiet> collectErfassungsgebieteMitStimmeingabeAnzahlenIdentisch(StimmeingabePlausiErgebniseingangsart ergebniseingangsart, Gebiet gebietUebergeordnet,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart, Collection<ErgebnisGueltigerEingang> ergebnisGueltigeEingaenge) {
    return ergebnisGueltigeEingaenge.stream() //
        .map(ErgebnisGueltigerEingang::getErgebniseingang) //
        .filter(ergebniseingangHavingErgebniseingangsart(ergebniseingangsart)) //
        .filter(ergebniseingangHavingGebietInGebietshierarchieUntergeordnet(gebietUebergeordnet)) //
        .filter(ergebniseingangHavingStimmeingabenAnzahlenIdentisch(stimmeingabenPerStimmenart)) //
        .map(Ergebniseingang::getWahldurchfuehrungsgebiet) //
        .collect(toList());
  }

  private Predicate<Ergebniseingang> ergebniseingangHavingErgebniseingangsart(StimmeingabePlausiErgebniseingangsart ergebniseingangsart) {
    return any -> {
      StimmeingabePlausiErgebniseingangsart stimmeingabePlausiErgebniseingangsart = StimmeingabePlausiErgebniseingangsart.UNBEKANNT;

      if (any != null) {
        stimmeingabePlausiErgebniseingangsart = StimmeingabePlausiErgebniseingangsart.of(any.getErgebniseingangsart());
      }

      return ergebniseingangsart.equals(stimmeingabePlausiErgebniseingangsart);
    };
  }

  private Predicate<Ergebniseingang> ergebniseingangHavingGebietInGebietshierarchieUntergeordnet(Gebiet gebietUebergeordnet) {
    return any -> any.getWahldurchfuehrungsgebiet() != null && any.getWahldurchfuehrungsgebiet().isInGebietshierarchieUntergeordnetesGebietVon(gebietUebergeordnet);
  }

  private Predicate<Ergebniseingang> ergebniseingangHavingStimmeingabenAnzahlenIdentisch(Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart) {
    return any -> isStimmeingabenAnzahlenIdentisch(stimmeingabenPerStimmenart, collectStimmeingabenPerStimmenart(any.getWahldurchfuehrungsgebiet(), any));
  }

  protected boolean isStimmeingabenAnzahlenIdentisch(Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    if (stimmeingabenPerStimmenart.isEmpty() || stimmeingabenInAnderemErfassungsgebietPerStimmenart.isEmpty()) {
      return false;
    }

    return isStimmeingabenAnzahlenIdentischInternal(stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected abstract boolean isStimmeingabenAnzahlenIdentischInternal(Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart);

  protected abstract Gebiet findGebietUebergeordnetRelevant();

  private Map<Stimmenart, List<StimmeingabeIndiziert>> collectStimmeingabenPerStimmenartRelevant() {
    if (stimmeingabenPerStimmenartRelevant == null) {
      stimmeingabenPerStimmenartRelevant = collectStimmeingabenPerStimmenart(wahldurchfuehrungsgebiet, ergebniseingang);
    }

    return stimmeingabenPerStimmenartRelevant;
  }

  private Map<Stimmenart, List<StimmeingabeIndiziert>> collectStimmeingabenPerStimmenart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Ergebniseingang ergebniseingang) {
    return ergebniseingangEingabeHashCodeGenerator.collectStimmeingabenPerStimmenart(wahldurchfuehrungsgebiet, ergebniseingang);
  }

  private Collection<ErgebnisGueltigerEingang> collectErgebnisGueltigeEingaengeRelevant() {
    String eingabeHashCodeLikePattern = createErgebniseingangEingabeHashCodeSearchPattern(ergebniseingangEingabeHashCodeGenerator, collectStimmeingabenPerStimmenartRelevant(),
        WildcardConverter.WILDCARD_CHARACTER_JPQL);

    return ergebnisGueltigerEingangDAO.findByWahlAndErgebniseingangEingabeHashCode(wahldurchfuehrung, eingabeHashCodeLikePattern).stream() //
        .filter(ergebnisGueltigerEingang -> !wahldurchfuehrungsgebiet.equals(ergebnisGueltigerEingang.getWahldurchfuehrungsgebiet()))//
        .collect(toList());
  }

  protected abstract String createErgebniseingangEingabeHashCodeSearchPattern(ErgebniseingangEingabeHashCodeGenerator ergebniseingangEingabeHashCodeGenerator,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart, String wildcardCharacter);

  private Optional<StimmeingabeIndiziert> findSystemGruppeStimmeingabe(SystemGruppe systemGruppe, Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart) {
    return stimmeingabenPerStimmenart.getOrDefault(stimmenart, new ArrayList<>()).stream() //
        .filter(stimmeingabe -> stimmeingabe.equalsSystemGruppe(systemGruppe)) //
        .findFirst();
  }

  protected boolean isWahlberechtigteAnzahlenIdentisch(Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe.WAHLBERECHTIGTE, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isWaehlerAnzahlenIdentisch(Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe.WAEHLER, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isGueltigeStimmenAnzahlenIdentisch(Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe.GUELTIGE, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isUngueltigeStimmenAnzahlenIdentisch(Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe.UNGUELTIGE, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isUebrigeStimmenAnzahlenIdentisch(Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe.UEBRIGE, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isGruppeStimmenAnzahlenIdentisch(long anzahlWahlvorschlaegeToCompare, Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isGruppeStimmenAnzahlenIdentisch(anzahlWahlvorschlaegeToCompare, ANZAHL_ZU_VERGLEICHEN_KEINE, stimmenart, stimmeingabenPerStimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isGruppeStimmenAnzahlenIdentisch(long anzahlWahlvorschlaegeToCompare, long anzahlListenkandidaturenToCompare, Stimmenart stimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    List<GruppeStimmeingabenIndiziert> stimmeingabenRelevantSorted = collectGruppeStimmeingabenSortedByErgebniseingangEingabeHashCodeOrder(stimmenart, stimmeingabenPerStimmenart);
    List<GruppeStimmeingabenIndiziert> stimmeingabenInAnderemErfassungsgebietRelevantSorted = collectGruppeStimmeingabenSortedByErgebniseingangEingabeHashCodeOrder(stimmenart,
        stimmeingabenInAnderemErfassungsgebietPerStimmenart);

    long anzahlWahlvorschlaegeComparable = Math.max(0, anzahlWahlvorschlaegeToCompare);
    anzahlWahlvorschlaegeComparable = Math.min(anzahlWahlvorschlaegeComparable, Math.min(stimmeingabenRelevantSorted.size(), stimmeingabenInAnderemErfassungsgebietRelevantSorted.size()));

    for (int i = 0; i < anzahlWahlvorschlaegeComparable; i++) {
      if (!isGruppeStimmenAnzahlenAnIndexIdentisch(i, anzahlListenkandidaturenToCompare, stimmeingabenRelevantSorted, stimmeingabenInAnderemErfassungsgebietRelevantSorted)) {
        return false;
      }
    }

    return anzahlWahlvorschlaegeComparable > 0;
  }

  protected boolean isGruppeStimmenAnzahlenAnIndexIdentisch(int wahlvorschlagIndexZeroBased, Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isGruppeStimmenAnzahlenAnIndexIdentisch(wahlvorschlagIndexZeroBased, ANZAHL_ZU_VERGLEICHEN_KEINE, stimmenart, stimmeingabenPerStimmenart,
        stimmeingabenInAnderemErfassungsgebietPerStimmenart);
  }

  protected boolean isGruppeStimmenAnzahlenAnIndexIdentisch(int wahlvorschlagIndexZeroBased, long anzahlListenkandidaturenToCompare, Stimmenart stimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    List<GruppeStimmeingabenIndiziert> stimmeingabenRelevantSorted = collectGruppeStimmeingabenSortedByErgebniseingangEingabeHashCodeOrder(stimmenart, stimmeingabenPerStimmenart);
    List<GruppeStimmeingabenIndiziert> stimmeingabenInAnderemErfassungsgebietRelevantSorted = collectGruppeStimmeingabenSortedByErgebniseingangEingabeHashCodeOrder(stimmenart,
        stimmeingabenInAnderemErfassungsgebietPerStimmenart);

    return isGruppeStimmenAnzahlenAnIndexIdentisch(wahlvorschlagIndexZeroBased, anzahlListenkandidaturenToCompare, stimmeingabenRelevantSorted, stimmeingabenInAnderemErfassungsgebietRelevantSorted);
  }

  private boolean isGruppeStimmenAnzahlenAnIndexIdentisch(int wahlvorschlagIndexZeroBased, long anzahlListenkandidaturenToCompare, List<GruppeStimmeingabenIndiziert> stimmeingaben,
      List<GruppeStimmeingabenIndiziert> stimmeingabenInAnderemErfassungsgebiet) {
    Optional<GruppeStimmeingabenIndiziert> stimmeingabenIfPresent = findGruppeStimmeingabenAnIndex(wahlvorschlagIndexZeroBased, stimmeingaben);
    Optional<GruppeStimmeingabenIndiziert> stimmeingabenInAnderemErfassungsgebietIfPresent = findGruppeStimmeingabenAnIndex(wahlvorschlagIndexZeroBased, stimmeingabenInAnderemErfassungsgebiet);

    if (stimmeingabenIfPresent.isPresent()) {
      if (stimmeingabenInAnderemErfassungsgebietIfPresent.isPresent()) {
        return isGruppeStimmeingabenAnzahlenIdentisch(wahlvorschlagIndexZeroBased, anzahlListenkandidaturenToCompare, stimmeingabenIfPresent.get(),
            stimmeingabenInAnderemErfassungsgebietIfPresent.get());
      }

      return false;
    }

    return !stimmeingabenInAnderemErfassungsgebietIfPresent.isPresent();
  }

  private boolean isGruppeStimmeingabenAnzahlenIdentisch(int wahlvorschlagIndexZeroBased, long anzahlListenkandidaturenToCompare, GruppeStimmeingabenIndiziert stimmeingaben,
      GruppeStimmeingabenIndiziert stimmeingabenInAnderemErfassungsgebiet) {
    StimmeingabeIndiziert wahlvorschlagStimmeingabe = stimmeingaben.getWahlvorschlagStimmeingabe();
    StimmeingabeIndiziert wahlvorschlagStimmeingabeInAnderemErfassungsgebiet = stimmeingabenInAnderemErfassungsgebiet.getWahlvorschlagStimmeingabe();

    boolean result = isStimmeingabeAnzahlenIdentisch(wahlvorschlagStimmeingabe, wahlvorschlagStimmeingabeInAnderemErfassungsgebiet);

    if (result) {
      List<StimmeingabeIndiziert> listenkandidaturStimmeingaben = stimmeingaben.getListenkandidaturStimmeingaben();
      List<StimmeingabeIndiziert> listenkandidaturStimmeingabenInAnderemErfassungsgebiet = stimmeingabenInAnderemErfassungsgebiet.getListenkandidaturStimmeingaben();

      long anzahlListenkandidaturenComparable = Math.max(0, anzahlListenkandidaturenToCompare);
      anzahlListenkandidaturenComparable = Math.min(anzahlListenkandidaturenComparable, Math.min(listenkandidaturStimmeingaben.size(), listenkandidaturStimmeingabenInAnderemErfassungsgebiet.size()));

      for (int i = 0; i < anzahlListenkandidaturenComparable; i++) {
        if (!isStimmeingabeAnzahlenIdentisch(listenkandidaturStimmeingaben.get(i), listenkandidaturStimmeingabenInAnderemErfassungsgebiet.get(i))) {
          return false;
        }
      }
    }

    return result;
  }

  private Optional<GruppeStimmeingabenIndiziert> findGruppeStimmeingabenAnIndex(int wahlvorschlagIndexZeroBased, List<GruppeStimmeingabenIndiziert> stimmeingaben) {
    if (wahlvorschlagIndexZeroBased >= 0 && stimmeingaben.size() > wahlvorschlagIndexZeroBased) {
      return Optional.of(stimmeingaben.get(wahlvorschlagIndexZeroBased));
    }

    return Optional.empty();
  }

  protected boolean isSystemGruppeStimmeingabeAnzahlenIdentisch(SystemGruppe systemGruppe, Stimmenart stimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isStimmeingabeAnzahlenIdentisch( //
        findSystemGruppeStimmeingabe(systemGruppe, stimmenart, stimmeingabenPerStimmenart), //
        findSystemGruppeStimmeingabe(systemGruppe, stimmenart, stimmeingabenInAnderemErfassungsgebietPerStimmenart) //
    );
  }

  protected boolean isSystemGruppeZwischensummeEingabeAnzahlenIdentisch(SystemGruppe systemGruppe, Stimmenart stimmenart, long zwischensummeIndex,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart, Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenInAnderemErfassungsgebietPerStimmenart) {
    return isStimmeingabeAnzahlenIdentisch( //
        findSystemGruppeZwischensummeEingabe(systemGruppe, stimmenart, zwischensummeIndex, stimmeingabenPerStimmenart), //
        findSystemGruppeZwischensummeEingabe(systemGruppe, stimmenart, zwischensummeIndex, stimmeingabenInAnderemErfassungsgebietPerStimmenart) //
    );
  }

  private Optional<StimmeingabeIndiziert> findSystemGruppeZwischensummeEingabe(SystemGruppe systemGruppe, Stimmenart stimmenart, long zwischensummeIndex,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart) {
    return stimmeingabenPerStimmenart.getOrDefault(stimmenart, new ArrayList<>()).stream() //
        .filter(stimmeingabe -> stimmeingabe.equalsSystemGruppe(systemGruppe) && stimmeingabe.equalsZwischensummeIndex(zwischensummeIndex)) //
        .findFirst();
  }

  private boolean isStimmeingabeAnzahlenIdentisch(StimmeingabeIndiziert stimmeingabe, StimmeingabeIndiziert stimmeingabeInAnderemErfassungsgebiet) {
    return isStimmeingabeAnzahlenIdentisch(Optional.ofNullable(stimmeingabe), Optional.ofNullable(stimmeingabeInAnderemErfassungsgebiet));
  }

  private boolean isStimmeingabeAnzahlenIdentisch(Optional<StimmeingabeIndiziert> stimmeingabeIfPresent, Optional<StimmeingabeIndiziert> stimmeingabeInAnderemErfassungsgebietIfPresent) {
    if (stimmeingabeIfPresent.isPresent()) {
      if (stimmeingabeInAnderemErfassungsgebietIfPresent.isPresent()) {
        return stimmeingabeIfPresent.get().getAnzahl() == stimmeingabeInAnderemErfassungsgebietIfPresent.get().getAnzahl();
      }

      return false;
    }

    return !stimmeingabeInAnderemErfassungsgebietIfPresent.isPresent();
  }

  private List<GruppeStimmeingabenIndiziert> collectGruppeStimmeingabenSortedByErgebniseingangEingabeHashCodeOrder(Stimmenart stimmenart,
      Map<Stimmenart, List<StimmeingabeIndiziert>> stimmeingabenPerStimmenart) {
    List<GruppeStimmeingabenIndiziert> result = new ArrayList<>();

    List<StimmeingabeIndiziert> gruppeStimmeingaben = ergebniseingangEingabeHashCodeGenerator.sortByHashCodeOrder(stimmeingabenPerStimmenart.getOrDefault(stimmenart, new ArrayList<>()).stream() //
        .filter(stimmeingabe -> !stimmeingabe.isZwischensummeEingabe() && stimmeingabe.hasWahlvorschlagStimmzettelPosition()) //
        .collect(toList()));

    List<StimmeingabeIndiziert> wahlvorschlagStimmeingaben = gruppeStimmeingaben.stream() //
        .filter(StimmeingabeIndiziert::isWahlvorschlagStimmeingabe) //
        .collect(toList());

    List<StimmeingabeIndiziert> listenkandidaturStimmeingaben = gruppeStimmeingaben.stream() //
        .filter(stimmeingabe -> stimmeingabe.isListenkandidaturStimmeingabe() && stimmeingabe.hasListenkandidaturStimmzettelPosition()) //
        .collect(toList());

    for (StimmeingabeIndiziert wahlvorschlagStimmeingabe : wahlvorschlagStimmeingaben) {
      List<StimmeingabeIndiziert> wahlvorschlagListenkandidaturStimmeingaben = new ArrayList<>();

      for (StimmeingabeIndiziert listenkandidaturStimmeingabe : listenkandidaturStimmeingaben) {
        if (Objects.equals(wahlvorschlagStimmeingabe.getWahlvorschlagStimmzettelPosition(), listenkandidaturStimmeingabe.getWahlvorschlagStimmzettelPosition())) {
          wahlvorschlagListenkandidaturStimmeingaben.add(listenkandidaturStimmeingabe);
        }
      }

      result.add(new GruppeStimmeingabenIndiziert(wahlvorschlagStimmeingabe, wahlvorschlagListenkandidaturStimmeingaben));
    }

    return result;
  }

  public static class GruppeStimmeingabenIndiziert {

    private final StimmeingabeIndiziert wahlvorschlagStimmeingabe;

    private final List<StimmeingabeIndiziert> listenkandidaturStimmeingaben;

    private GruppeStimmeingabenIndiziert(StimmeingabeIndiziert wahlvorschlagStimmeingabe, List<StimmeingabeIndiziert> listenkandidaturStimmeingaben) {
      super();

      this.wahlvorschlagStimmeingabe = wahlvorschlagStimmeingabe;

      this.listenkandidaturStimmeingaben = listenkandidaturStimmeingaben;
    }

    public StimmeingabeIndiziert getWahlvorschlagStimmeingabe() {
      return wahlvorschlagStimmeingabe;
    }

    public List<StimmeingabeIndiziert> getListenkandidaturStimmeingaben() {
      return listenkandidaturStimmeingaben;
    }
  }
}
