
package de.ivu.elect.business.ergebnis.control;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

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

import de.ivu.elect.business.ergebnis.boundary.AbstractStimmenDAO;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebietTK;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption;


import static java.util.Collections.emptyList;
import static java.util.Collections.emptySet;

public class StimmenPool implements Serializable {

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

  private static final long serialVersionUID = 1L;

  private static final int LISTENKANDIDATUR_STIMMEN_COUNT_EXPECTED = 1024 * 2;

  private static final int LISTENKANDIDATUR_COUNT_EXPECTED = 1024 * 2;

  private static final int GRUPPE_COUNT_EXPECTED = 48 * 2;

  private static final int GEBIETE_COUNT_EXPECTED = 512 * 2;

  private static final int DIREKTKANDIDAT_COUNT_EXPECTED = 256 * 2;

  private Map<Gruppe, Map<Gebiet, Stimmen>> direktstimmenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenAbzuziehenDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet;

  private Map<Gruppe, Map<Gebiet, Stimmen>> listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet;

  private Map<Listenkandidatur, Map<Gebiet, Stimmen>> listenkandidaturstimmenProGebiet;

  private Map<Listenkandidatur, Map<Gebiet, Stimmen>> listenkandidaturstimmenProGebietNichtEregbnisrelevant;

  private Map<Direktkandidat, Map<Gebiet, Stimmen>> direktkandidatStimmenProGebiet;

  private List<Stimmen> systemgruppenstimmen;

  private Map<ReferendumOption, Map<Gebiet, Stimmen>> referendumoptionStimmenProGebiet;

  private final StimmenPoolQuelle stimmenPoolQuelleDirektstimmen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen;

  private final StimmenPoolQuelle stimmenPoolQuelleListenkandidaturstimmen;

  private final StimmenPoolQuelle stimmenPoolQuelleSystemgruppenstimmen;

  private final StimmenPoolQuelle stimmenPoolQuelleReferendumstimmen;

  public static StimmenPool createStimmenPoolFuerAlleStimmen(List<Stimmen> alleStimmen, StimmenFilter stimmenFilter) {
    return new StimmenPool(alleStimmen, stimmenFilter);
  }

  private Map<Gebiet, List<Stimmen>> systemgruppenStimmenNachGebieten = null;

  public static StimmenPool createStimmenPoolFuerAlleStimmen(List<Stimmen> alleStimmen) {
    return createStimmenPoolFuerAlleStimmen(alleStimmen, LasseAllesDurchStimmenFilter.INSTANCE);
  }

  public static StimmenPool createStimmenPoolLeer() {
    return new StimmenPool(Collections.<Stimmen>emptyList(), LasseAllesDurchStimmenFilter.INSTANCE);
  }

  public static StimmenPool createStimmenPoolFuerAlleStimmenOhneListenkandidaturStimmen(List<Stimmen> alleStimmenOhneListenkandidatur, Function<Ergebnis, List<Stimmen>> listenkandidaturStimmenQuelle,
                                                                                        Ergebnis ergebnis, StimmenFilter stimmenFilter) {
    return new StimmenPool(alleStimmenOhneListenkandidatur, listenkandidaturStimmenQuelle, ergebnis, stimmenFilter);
  }

  public static StimmenPool createStimmenPool(final AbstractStimmenDAO stimmenDAO, final Ergebnis ergebnis, final StimmenFilter stimmenFilter) {
    return new StimmenPool( //
        () -> filter(stimmenDAO.findByErgebnisDirektstimmenNoCache(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisListenstimmenOhneListenkandidaturenNoCache(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisListenkandidaturstimmenNoCache(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisAndIsSystemGruppeNoCache(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisReferendumstimmenNoCache(ergebnis), stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_ABZUZIEHEN), stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_UNVERAENDERT),
            stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_VERAENDERT),
            stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_PANASCHIERT),
            stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_VERAENDERT),
            stimmenFilter), //
        () -> filter(
            stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_PANASCHIERT),
            stimmenFilter) //
    );
  }

  public static StimmenPool createStimmenPool(AbstractStimmenDAO stimmenDAO, Ergebnis ergebnis) {
    return createStimmenPool(stimmenDAO, ergebnis, LasseAllesDurchStimmenFilter.INSTANCE);
  }

  public static StimmenPool createStimmenPoolFuerGruppe(final AbstractStimmenDAO stimmenDAO, final Ergebnis ergebnis, final PolitischeGruppe politischeGruppe) {
    return new StimmenPool( //
        () -> stimmenDAO.findByErgebnisDirektstimmenFuerPolitischeGruppeNoCache(ergebnis, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisListenstimmenOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisListenkandidaturstimmenFuerPolitischeGruppeNoCache(ergebnis, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisAndIsSystemGruppeNoCache(ergebnis), //
        () -> stimmenDAO.findByErgebnisReferendumstimmenNoCache(ergebnis),

        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis, Stimmenart.LISTENSTIMME_ABZUZIEHEN,
            politischeGruppe), //

        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis,
            Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_UNVERAENDERT, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis,
            Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_VERAENDERT, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis,
            Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_PANASCHIERT, politischeGruppe), //

        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis,
            Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_VERAENDERT, politischeGruppe), //
        () -> stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenFuerPolitischeGruppeNoCache(ergebnis,
            Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_PANASCHIERT, politischeGruppe) //
    );
  }

  public static StimmenPool createStimmenPoolReadonly(ReadonlyExtendedStimmenLoader readonlyExtendedLoader, final AbstractStimmenDAO stimmenDAO, final Ergebnis ergebnis,
                                                      final StimmenFilter stimmenFilter) {
    return new StimmenPool( //
        () -> filter(stimmenDAO.findByErgebnisDirektstimmenNoCache(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisListenstimmenOhneListenkandidaturenNoCache(ergebnis), stimmenFilter), //
        () -> {
          List<Stimmen> byErgebnisListenkandidaturstimmen = filter(stimmenDAO.findByErgebnisListenkandidaturstimmenNoCache(ergebnis), stimmenFilter);

          return byErgebnisListenkandidaturstimmen;
        }, () -> filter(readonlyExtendedLoader.findByErgebnisAndIsSystemGruppe(ergebnis), stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisReferendumstimmenNoCache(ergebnis), stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_ABZUZIEHEN), stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_UNVERAENDERT),
            stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_VERAENDERT),
            stimmenFilter), //
        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_PANASCHIERT),
            stimmenFilter), //

        () -> filter(stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_VERAENDERT),
            stimmenFilter), //
        () -> filter(
            stimmenDAO.findByErgebnisStimmenartOhneListenkandidaturenNoCache(ergebnis, Stimmenart.LISTENSTIMME_KEIN_EINZELNER_WAHLVORSCHAG_PANASCHIERT),
            stimmenFilter) //
    );
  }

  protected StimmenPool(List<Stimmen> alleStimmenOhneListenkandidatur, Function<Ergebnis, List<Stimmen>> listenkandidaturStimmenQuelle, Ergebnis ergebnis, StimmenFilter stimmenFilter) {
    super();

    List<Stimmen> direktstimmen = new ArrayList<>();
    List<Stimmen> listenstimmenDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenAbzuziehenDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe = new ArrayList<>();
    List<Stimmen> systemgruppenstimmen = new ArrayList<>();
    List<Stimmen> referendumstimmen = new ArrayList<>();

    for (Stimmen stimmen : alleStimmenOhneListenkandidatur) {
      if (stimmenFilter.isBetrachteStimmen(stimmen)) {
        if (stimmen.isGruppeSystemGruppe()) {
          systemgruppenstimmen.add(stimmen);
        } else if (stimmen.isDirektstimme()) {
          direktstimmen.add(stimmen);
        } else if (stimmen.isListenstimme()) {
          listenstimmenDerGruppe.add(stimmen);
        } else if (stimmen.isReferendumstimme()) {
          referendumstimmen.add(stimmen);
        } else if (stimmen.isListenstimmeAbzuziehen()) {
          listenstimmenAbzuziehenDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagUnveraendert()) {
          listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagVeraendert()) {
          listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagPanaschiert()) {
          listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeKeinEinzelnerWahlvorschlagVeraendert()) {
          listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeKeinEinzelnerWahlvorschlagPanaschiert()) {
          listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe.add(stimmen);
        } else {
          throw new IllegalStateException("Unbekannte Stimmenart: " + stimmen.getStimmenart() + ", " + stimmen.getGruppe()
              .getGruppeArt() + ", gruppe: " + stimmen.getGruppe() + ", stimmen:" + stimmen
              + ", isSystemgruppe: " + stimmen.getGruppe().isSystemGruppe());
        }
      }
    }

    this.stimmenPoolQuelleDirektstimmen = new StimmenPoolQuelleVorhanden(direktstimmen);
    this.stimmenPoolQuelleListenstimmenDerGruppen = new StimmenPoolQuelleVorhanden(listenstimmenDerGruppe);
    this.stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen = new StimmenPoolQuelleVorhanden(listenstimmenAbzuziehenDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe);
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe);
    this.stimmenPoolQuelleListenkandidaturstimmen = () -> filter(listenkandidaturStimmenQuelle.apply(ergebnis), stimmenFilter);
    this.stimmenPoolQuelleSystemgruppenstimmen = new StimmenPoolQuelleVorhanden(systemgruppenstimmen);
    this.stimmenPoolQuelleReferendumstimmen = new StimmenPoolQuelleVorhanden(referendumstimmen);
  }

  @Deprecated // Finder definieren(!)
  public boolean isDirektOderListenstimmenVorhanden() {
    return !stimmenPoolQuelleDirektstimmen.ladeStimmen().isEmpty() || !stimmenPoolQuelleListenstimmenDerGruppen.ladeStimmen().isEmpty();
  }

  // ELECT-2229
  private static List<Stimmen> filter(List<Stimmen> stimmen, StimmenFilter stimmenFilter) {
    // Optimierung, wenn eh alles durchgeht nicht filtern
    if (LasseAllesDurchStimmenFilter.INSTANCE == stimmenFilter) {
      return stimmen;
    }

    return stimmen.stream().filter(stimmenFilter::isBetrachteStimmen).collect(Collectors.toCollection(() -> new ArrayList<>(stimmen.size())));

  }

  protected StimmenPool(StimmenPoolQuelle stimmenPoolQuelleDirektstimmen, StimmenPoolQuelle stimmenPoolQuelleListenstimmenDerGruppen,
                        StimmenPoolQuelle stimmenPoolQuelleListenkandidaturstimmen, StimmenPoolQuelle stimmenPoolQuelleSystemgruppenstimmen, StimmenPoolQuelle stimmenPoolQuelleReferendumstimmen,
                        StimmenPoolQuelle stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen, StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen,
                        StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen, StimmenPoolQuelle stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen,
                        StimmenPoolQuelle stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen, StimmenPoolQuelle stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen) {
    super();

    this.stimmenPoolQuelleDirektstimmen = stimmenPoolQuelleDirektstimmen;
    this.stimmenPoolQuelleListenstimmenDerGruppen = stimmenPoolQuelleListenstimmenDerGruppen;
    this.stimmenPoolQuelleListenkandidaturstimmen = stimmenPoolQuelleListenkandidaturstimmen;
    this.stimmenPoolQuelleSystemgruppenstimmen = stimmenPoolQuelleSystemgruppenstimmen;
    this.stimmenPoolQuelleReferendumstimmen = stimmenPoolQuelleReferendumstimmen;
    this.stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen = stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen;
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen = stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen;
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen = stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen;
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen = stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen;
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen = stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen;
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen = stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen;
  }

  protected StimmenPool(List<Stimmen> stimmenListe, StimmenFilter stimmenFilter) {
    super();

    List<Stimmen> direktstimmen = new ArrayList<>();
    List<Stimmen> listenkandidaturStimmen = new ArrayList<>();
    List<Stimmen> listenstimmenDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenAbzuziehenDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe = new ArrayList<>();
    List<Stimmen> listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe = new ArrayList<>();
    List<Stimmen> systemgruppenstimmen = new ArrayList<>();
    List<Stimmen> referendumstimmen = new ArrayList<>();

    for (Stimmen stimmen : stimmenListe) {
      if (stimmenFilter.isBetrachteStimmen(stimmen)) {
        if (stimmen.isGruppeSystemGruppe()) {
          systemgruppenstimmen.add(stimmen);
        } else if (stimmen.isDirektstimme()) {
          direktstimmen.add(stimmen);
        } else if (stimmen.getListenkandidatur() != null) {
          listenkandidaturStimmen.add(stimmen);
        } else if (stimmen.isListenstimme()) {
          listenstimmenDerGruppe.add(stimmen);
        } else if (stimmen.isReferendumstimme()) {
          referendumstimmen.add(stimmen);
        } else if (stimmen.isListenstimmeAbzuziehen()) {
          listenstimmenAbzuziehenDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagUnveraendert()) {
          listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagVeraendert()) {
          listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeEinzelnerWahlvorschlagPanaschiert()) {
          listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeKeinEinzelnerWahlvorschlagVeraendert()) {
          listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe.add(stimmen);
        } else if (stimmen.isListenstimmeKeinEinzelnerWahlvorschlagPanaschiert()) {
          listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe.add(stimmen);
//        } else if (stimmen.getStimmenart().isGesamt()) {
//          // ignore TODO ELECT-15228
//          LOG.warn("Ignore TODO ELECT-15228");
        } else {
          throw new IllegalStateException("Unbekannte Stimmenart: " + stimmen.getStimmenart() + ", " + stimmen.getGruppe().getGruppeArt());
        }
      }
    }

    this.stimmenPoolQuelleDirektstimmen = new StimmenPoolQuelleVorhanden(direktstimmen);
    this.stimmenPoolQuelleListenstimmenDerGruppen = new StimmenPoolQuelleVorhanden(listenstimmenDerGruppe);
    this.stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen = new StimmenPoolQuelleVorhanden(listenstimmenAbzuziehenDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe);
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe);
    this.stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen = new StimmenPoolQuelleVorhanden(
        listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe);
    this.stimmenPoolQuelleListenkandidaturstimmen = new StimmenPoolQuelleVorhanden(listenkandidaturStimmen);
    this.stimmenPoolQuelleSystemgruppenstimmen = new StimmenPoolQuelleVorhanden(systemgruppenstimmen);
    this.stimmenPoolQuelleReferendumstimmen = new StimmenPoolQuelleVorhanden(referendumstimmen);
  }

  @Nullable
  public Stimmen getListenstimmenDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenDerGruppenProGebiet == null) {
      ladeListenstimmenDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  public Collection<Stimmen> getListenkandidaturstimmen(Listenkandidatur listenkandidatur) {
    if (listenkandidaturstimmenProGebiet == null) {
      ladeListenkandidaturstimmen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenkandidaturstimmenProGebiet.get(listenkandidatur);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenkandidaturstimmenNichtErgebnisrelevant(Listenkandidatur listenkandidatur) {
    if (listenkandidaturstimmenProGebietNichtEregbnisrelevant == null) {
      ladeListenkandidaturstimmen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenkandidaturstimmenProGebietNichtEregbnisrelevant.get(listenkandidatur);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  @Nullable
  public Stimmen getListenkandidaturstimmenAufErfassungsgebiet(Listenkandidatur listenkandidatur, Gebiet gebiet) {
    if (listenkandidaturstimmenProGebiet == null) {
      ladeListenkandidaturstimmen();
    }

    Map<Gebiet, Stimmen> gebietStimmenMap = listenkandidaturstimmenProGebiet.get(listenkandidatur);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenkandidaturstimmenNichtErgebnisrelevantAufErfassungsgebiet(Listenkandidatur listenkandidatur, Gebiet gebiet) {
    if (listenkandidaturstimmenProGebietNichtEregbnisrelevant == null) {
      ladeListenkandidaturstimmen();
    }

    Map<Gebiet, Stimmen> gebietStimmenMap = listenkandidaturstimmenProGebietNichtEregbnisrelevant.get(listenkandidatur);

    if (gebietStimmenMap == null) {
      return null;
    }

    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenAbzuziehenDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenAbzuziehenDerGruppenProGebiet == null) {
      ladeListenstimmenAbzuziehenDerGruppen();
    }

    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenAbzuziehenDerGruppenProGebiet.get(gruppe);

    if (gebietStimmenMap == null) {
      return null;
    }

    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet == null) {
      ladeListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  @Nullable
  public Stimmen getListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppeAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet == null) {
      ladeListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  public Collection<Stimmen> getListenstimmenDerGruppe(Gruppe gruppe) {
    if (listenstimmenDerGruppenProGebiet == null) {
      ladeListenstimmenDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  @Deprecated // unsinnige Methode
  public Stream<Stimmen> getListenstimmenStream() {
    if (listenstimmenDerGruppenProGebiet == null) {
      ladeListenstimmenDerGruppen();
    }
    // Liefert einen Stream aller Elemente der geschachtelten Map, d.h. aller Values der inneren Maps
    return listenstimmenDerGruppenProGebiet.values().stream().flatMap(map -> map.values().stream());
  }

  public Collection<Stimmen> getListenstimmenAbzuziehenDerGruppe(Gruppe gruppe) {
    if (listenstimmenAbzuziehenDerGruppenProGebiet == null) {
      ladeListenstimmenAbzuziehenDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenAbzuziehenDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe(Gruppe gruppe) {
    if (listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe(Gruppe gruppe) {
    if (listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppe(Gruppe gruppe) {
    if (listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet == null) {
      ladeListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppe(Gruppe gruppe) {
    if (listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet == null) {
      ladeListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  public Collection<Stimmen> getListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppe(Gruppe gruppe) {
    if (listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet == null) {
      ladeListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet.get(gruppe);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  private <E> List<E> notNull(List<E> stimmen) {
    if (stimmen == null) {
      return emptyList();
    }
    return stimmen;
  }

  private Set<Stimmen> notNull(Set<Stimmen> stimmen) {
    if (stimmen == null) {
      return emptySet();
    }
    return stimmen;
  }

  private void ladeListenkandidaturstimmen() {
    listenkandidaturstimmenProGebiet = new HashMap<>(LISTENKANDIDATUR_COUNT_EXPECTED);
    listenkandidaturstimmenProGebietNichtEregbnisrelevant = new HashMap<>(LISTENKANDIDATUR_COUNT_EXPECTED);
    for (Stimmen stimmen : ladeListenstimmenDerListenkandidaturen()) {
      if (stimmen.getStimmenart().isListenkandidaturstimmeNichtErgebnisrelevant()) {
        listenkandidaturstimmenProGebietNichtEregbnisrelevant.computeIfAbsent(stimmen.getListenkandidatur(), x -> new HashMap<>())
            .put(stimmen.getGebiet(), stimmen);
      } else {
        listenkandidaturstimmenProGebiet.computeIfAbsent(stimmen.getListenkandidatur(), x -> new HashMap<>(LISTENKANDIDATUR_STIMMEN_COUNT_EXPECTED))
            .put(stimmen.getGebiet(), stimmen);
      }
    }
  }

  private List<Stimmen> ladeListenstimmenDerListenkandidaturen() {
    LOG.info("lade listenkandidaturStimmen");
    List<Stimmen> stimmen = stimmenPoolQuelleListenkandidaturstimmen.ladeStimmen();
    LOG.info("lade listenkandidaturStimmen - done: {}", stimmen.size());
    return stimmen;
  }

  private Map<Gruppe, Map<Gebiet, Stimmen>> mappeNachGruppe(StimmenPoolQuelle stimmenPoolQuelle) {
    Map<Gruppe, Map<Gebiet, Stimmen>> result = new HashMap<>(GRUPPE_COUNT_EXPECTED);
    for (Stimmen stimmen : stimmenPoolQuelle.ladeStimmen()) {
      result.computeIfAbsent(stimmen.getGruppe(), k -> new HashMap<>(GEBIETE_COUNT_EXPECTED)).put(stimmen.getGebiet(), stimmen);
    }

    return result;
  }

  private void ladeListenstimmenDerGruppen() {
    LOG.info("ladeListenstimmenDerGruppen");
    listenstimmenDerGruppenProGebiet = mappeNachGruppe(stimmenPoolQuelleListenstimmenDerGruppen);
    LOG.info("ladeListenstimmenDerGruppen - done");
  }

  private void ladeListenstimmenAbzuziehenDerGruppen() {
    LOG.info("ladeListenstimmenAbzuziehenDerGruppen");
    listenstimmenAbzuziehenDerGruppenProGebiet = mappeNachGruppe(stimmenPoolQuelleListenstimmenAbzuziehenDerGruppen);
    LOG.info("ladeListenstimmenAbzuziehenDerGruppen - done");
  }

  private void ladeListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen() {
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen");
    listenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppenProGebiet = mappeNachGruppe(
        stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen);
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppen - done");
  }

  private void ladeListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen() {
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen");
    listenstimmenEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet = mappeNachGruppe(
        stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen);
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppen - done");
  }

  private void ladeListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen() {
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen");
    listenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet = mappeNachGruppe(
        stimmenPoolQuelleListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen);
    LOG.info("ladeListenstimmenEinzelnerWahlvorschlagPanaschiertDerGruppen - done");
  }

  private void ladeListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen() {
    LOG.info("ladeListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen");
    listenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppenProGebiet = mappeNachGruppe(
        stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen);
    LOG.info("ladeListenstimmenKeinEinzelnerWahlvorschlagVeraendertDerGruppen - done");
  }

  private void ladeListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen() {
    LOG.info("ladeListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen");
    listenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppenProGebiet = mappeNachGruppe(
        stimmenPoolQuelleListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen);
    LOG.info("ladeListenstimmenKeinEinzelnerWahlvorschlagPanaschiertDerGruppen - done");
  }

  @Nullable
  public Stimmen getDirektstimmenAufErfassungsgebiet(Gruppe gruppe, Gebiet gebiet) {
    if (direktstimmenProGebiet == null) {
      ladeDirektstimmen();
    }
    Map<Gebiet, Stimmen> stimmenAufGebiet = direktstimmenProGebiet.get(gruppe);
    if (stimmenAufGebiet == null) {
      return null;
    }
    return stimmenAufGebiet.get(gebiet);
  }

  public Collection<Stimmen> getDirektstimmen(Gruppe gruppe) {
    if (direktstimmenProGebiet == null) {
      ladeDirektstimmen();
    }
    Map<Gebiet, Stimmen> stimmenAufGebiet = direktstimmenProGebiet.get(gruppe);
    if (stimmenAufGebiet == null) {
      return emptyList();
    }
    return stimmenAufGebiet.values();
  }

  public Stream<Stimmen> getDirektstimmenStream() {
    if (direktstimmenProGebiet == null) {
      ladeDirektstimmen();
    }
    // Liefert einen Stream aller Elemente der geschachtelten Map, d.h. aller Values der inneren Maps
    return direktstimmenProGebiet.values().stream().flatMap(map -> map.values().stream());
  }

  private void ladeDirektstimmen() {
    LOG.info("ladeDirektstimmen");
    direktstimmenProGebiet = mappeNachGruppe(stimmenPoolQuelleDirektstimmen);
    LOG.info("ladeDirektstimmen - done");
  }

  public Collection<Stimmen> getDirektkandidatStimmen(Direktkandidat direktkandidat, @Nullable Function<Gruppe, List<Direktkandidat>> direktKandidatenDerGruppe) {
    if (direktkandidatStimmenProGebiet == null) {
      ladeDirektkandidatStimmen(direktKandidatenDerGruppe);
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = direktkandidatStimmenProGebiet.get(direktkandidat);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  @Nullable
  public Stimmen getDirektkandidatStimmenAufErfassungsgebiet(Direktkandidat direktkandidat, Gebiet gebiet, @Nullable Function<Gruppe, List<Direktkandidat>> direktKandidatenDerGruppe) {
    if (direktkandidatStimmenProGebiet == null) {
      ladeDirektkandidatStimmen(direktKandidatenDerGruppe);
    }

    Map<Gebiet, Stimmen> gebietStimmenMap = direktkandidatStimmenProGebiet.get(direktkandidat);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  private void ladeDirektkandidatStimmen(@Nullable Function<Gruppe, List<Direktkandidat>> direktKandidatenDerGruppe) {
    direktkandidatStimmenProGebiet = new HashMap<>(DIREKTKANDIDAT_COUNT_EXPECTED);
    List<Stimmen> stimmens = stimmenPoolQuelleDirektstimmen.ladeStimmen();
    for (Stimmen stimmen : stimmens) {
      if (stimmen.isPolitischeStimme()) {
        Direktkandidat direktkandidat = getDirektkandidatForGruppeImGebiet(stimmen.getGruppe(), stimmen.getGebiet(), direktKandidatenDerGruppe);

        direktkandidatStimmenProGebiet.computeIfAbsent(direktkandidat, k -> new HashMap<>(GEBIETE_COUNT_EXPECTED)).put(stimmen.getGebiet(), stimmen);
      }
    }
  }

  /*
   * **** You found a BIG UGLY HACK. Beat FNE for it! *****
   *
   * PROBLEM: Performance. Aufruf von StimmenKumulierer.getSummeStimmenFuerDirektkandidaturAufGebietUndDarunter() dauert 35 Sekunden.
   *
   * LÖSUNG: getDirektkandidatForGruppeImGebiet(GruppeImGebiet). Benannter Aufruf dauert noch 4 Sekunden.
   *
   * WEG: Gehe nicht für jeden Direktkandidaten über GruppeImGebiet.getUebergeordneteGruppeImGebiet(), was JEDES MAL wieder ALLE (!) GruppenImGebiet seiner Gruppe lädt, sondern lade beim ersten
   * Abfragen einer Gruppe sofort alle GruppenImGebiet der Gruppe und ordne allen ihren jeweiligen Direktkandidaten zu.
   */
  private Map<Gruppe, Map<Gebiet, Direktkandidat>> direktkandidatProGebietProGruppe;

  @Nullable
  private Direktkandidat getDirektkandidatForGruppeImGebiet(Gruppe gruppe, Gebiet gebiet, @Nullable Function<Gruppe, List<Direktkandidat>> direktKandidatenDerGruppe) {
    // ELECT-12101
    if (direktKandidatenDerGruppe == null) {
      return getDirektkandidatForGruppeImGebiet(gruppe, gebiet);
    }

    if (direktkandidatProGebietProGruppe == null) {
      direktkandidatProGebietProGruppe = new HashMap<>(GRUPPE_COUNT_EXPECTED);
    }

    return direktkandidatProGebietProGruppe.computeIfAbsent(gruppe, x -> new HashMap<>(GEBIETE_COUNT_EXPECTED))
        .computeIfAbsent(gebiet, x -> notNull(direktKandidatenDerGruppe.apply(gruppe)).stream()
            .filter(
                direktkandidat -> gruppe.equals(direktkandidat.getGruppe()) && gebiet.isInGebietshierarchieUntergeordnetesGebietVon(direktkandidat.getGebiet()))
            .findFirst().orElse(null));
  }

  @Nullable
  private Direktkandidat getDirektkandidatForGruppeImGebiet(Gruppe gruppe, Gebiet gebiet) {

    if (direktkandidatProGebietProGruppe == null) {
      direktkandidatProGebietProGruppe = new HashMap<>(GRUPPE_COUNT_EXPECTED);
    }

    Map<Gebiet, Direktkandidat> direktkandidatenDerGruppe = direktkandidatProGebietProGruppe.get(gruppe);
    if (direktkandidatenDerGruppe == null) {
      direktkandidatenDerGruppe = new HashMap<>(GEBIETE_COUNT_EXPECTED);

      Map<GruppeImGebietTK, GruppeImGebiet> gruppenImGebietDerGruppe = GruppeImGebiet.mappeNachGruppeGebietSchluessel(gruppe.getGruppenImGebiet());
      for (GruppeImGebiet visitedGruppeImGebiet : gruppenImGebietDerGruppe.values()) {
        findDirektkandidatForGruppeImGebiet(visitedGruppeImGebiet, direktkandidatenDerGruppe, gruppenImGebietDerGruppe);
      }

      direktkandidatProGebietProGruppe.put(gruppe, direktkandidatenDerGruppe);
    }

    return direktkandidatenDerGruppe.get(gebiet);
  }

  /**
   * Findet den Direktkandidaten der GruppeImGebiet in der Gebietshierarchie und speichert ihn in der alleDirektkandidatenDerGruppe-Map.
   * <p/>
   * NUR zum INTERNEN GEBRAUCH in getDirektkandidatForGruppeImGebiet(GruppeImGebiet)!
   */
  @Nullable
  private Direktkandidat findDirektkandidatForGruppeImGebiet(GruppeImGebiet gruppeImGebiet, Map<Gebiet, Direktkandidat> alleDirektkandidatenDerGruppe,
                                                             Map<GruppeImGebietTK, GruppeImGebiet> alleGruppenImGebietDerGruppe) {
    Gruppe gruppe = gruppeImGebiet.getGruppe();
    Gebiet gebiet = gruppeImGebiet.getGebiet();

    Direktkandidat result = alleDirektkandidatenDerGruppe.get(gruppeImGebiet.getGebiet());
    if (result == null) {
      if (!gruppeImGebiet.getDirektkandidaten().isEmpty()) {
        result = gruppeImGebiet.getDirektkandidaten().iterator().next();
      } else {
        if (gebiet.getUebergeordnetesGebiet() != null) {
          GruppeImGebiet uebergeordneteGruppeImGebiet = alleGruppenImGebietDerGruppe.get(
              new GruppeImGebietTK(gebiet.getUebergeordnetesGebiet().getGebietTK(), gruppe.getGruppeTK()));
          if (uebergeordneteGruppeImGebiet != null) {
            result = findDirektkandidatForGruppeImGebiet(uebergeordneteGruppeImGebiet, alleDirektkandidatenDerGruppe, alleGruppenImGebietDerGruppe);
          }
        }
      }

      alleDirektkandidatenDerGruppe.put(gebiet, result);
    }

    return result;
  }

  public List<Stimmen> getSystemgruppenStimmen() {
    if (systemgruppenstimmen == null) {
      ladeSystemgruppenstimmen();
    }

    return systemgruppenstimmen;
  }

  public Map<Gebiet, List<Stimmen>> getSystemgruppenStimmenUnterhalbVonGebiet() {
    if (systemgruppenStimmenNachGebieten == null) {
      systemgruppenStimmenNachGebieten = new HashMap<>(GEBIETE_COUNT_EXPECTED);
      for (Stimmen stimmen : getSystemgruppenStimmen()) {
        Gebiet gebiet = stimmen.getGebiet();
        while (gebiet != null) {
          systemgruppenStimmenNachGebieten.computeIfAbsent(gebiet, x -> new ArrayList<>()).add(stimmen);
          gebiet = gebiet.getUebergeordnetesGebiet();
        }
      }
    }
    return systemgruppenStimmenNachGebieten;
  }

  private void ladeSystemgruppenstimmen() {
    LOG.info("lade systemgruppenstimmen");
    systemgruppenstimmen = stimmenPoolQuelleSystemgruppenstimmen.ladeStimmen();
    LOG.info("lade systemgruppenstimmen - done");
  }

  public Collection<Stimmen> getReferendumOptionStimmen(ReferendumOption referendumOption) {
    if (referendumoptionStimmenProGebiet == null) {
      ladeReferendumOptionStimmen();
    }
    Map<Gebiet, Stimmen> gebietStimmenMap = referendumoptionStimmenProGebiet.get(referendumOption);
    if (gebietStimmenMap == null) {
      return emptyList();
    }
    return gebietStimmenMap.values();
  }

  @Nullable
  public Stimmen getReferendumOptionStimmenAufErfassungsgebiet(ReferendumOption referendumOption, Gebiet gebiet) {
    if (referendumoptionStimmenProGebiet == null) {
      ladeReferendumOptionStimmen();
    }

    Map<Gebiet, Stimmen> gebietStimmenMap = referendumoptionStimmenProGebiet.get(referendumOption);
    if (gebietStimmenMap == null) {
      return null;
    }
    return gebietStimmenMap.get(gebiet);
  }

  private void ladeReferendumOptionStimmen() {
    LOG.info("ladeReferendumOptionStimmen");
    referendumoptionStimmenProGebiet = new HashMap<>();
    for (Stimmen stimmen : stimmenPoolQuelleReferendumstimmen.ladeStimmen()) {
      referendumoptionStimmenProGebiet.computeIfAbsent(stimmen.getReferendumOption(), k -> new HashMap<>(GEBIETE_COUNT_EXPECTED))
          .put(stimmen.getGebiet(), stimmen);
    }
    LOG.info("ladeReferendumOptionStimmen - done");
  }

  private Map<Gebiet, Set<Stimmen>> stimmenProGebiet;

  public Collection<Stimmen> getAlleStimmenDerGruppe(Gruppe gruppe) {
    Collection<Stimmen> direktstimmen = getDirektstimmen(gruppe);
    Collection<Stimmen> listenstimmen = getListenstimmenDerGruppe(gruppe);
    Collection<Stimmen> result = new ArrayList<>(direktstimmen.size() + listenstimmen.size());
    result.addAll(direktstimmen);
    result.addAll(listenstimmen);
    return result;
  }

  public Set<Stimmen> getDirektListenSystemStimmenAufGebiet(Gebiet g) {
    if (stimmenProGebiet == null) {
      stimmenProGebiet = new HashMap<>(GEBIETE_COUNT_EXPECTED);

      Consumer<Stimmen> sc = stimmen -> {
        Gebiet gebiet = stimmen.getGebiet();
        if (!stimmenProGebiet.containsKey(gebiet)) {
          stimmenProGebiet.put(gebiet, new HashSet<>());
        }
        stimmenProGebiet.get(gebiet).add(stimmen);
      };
      stimmenPoolQuelleDirektstimmen.ladeStimmen().forEach(sc);
      stimmenPoolQuelleListenstimmenDerGruppen.ladeStimmen().forEach(sc);
      stimmenPoolQuelleSystemgruppenstimmen.ladeStimmen().forEach(sc);

    }

    return notNull(stimmenProGebiet.get(g));
  }
}
