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

import static java.util.stream.Collectors.toMap;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

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

import de.ivu.elect.business.ergebnis.entity.DHondtQuotient;
import de.ivu.elect.business.ergebnis.entity.DHondtQuotientListenImGebiet;
import de.ivu.elect.business.ergebnis.entity.DHondtQuotientListenInGruppe;
import de.ivu.elect.business.ergebnis.entity.DHondtQuotientPolitischeGruppe;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.ErgebnisLogEintrag;
import de.ivu.elect.business.ergebnis.entity.ErgebnisdatenWahlbeteiligungCacheKey;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.ergebnis.entity.Sitze;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.ergebnis.entity.WahlbeteiligungWahlberechtigeQuelle;
import de.ivu.elect.business.ergebniskonflikt.entity.ErgebnisKonflikt;
import de.ivu.elect.business.fremdkandidatur.entity.Fremdkandidatur;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietPolygonPoint;
import de.ivu.elect.business.gebietsbaum.entity.Gebietsgruppierung;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppeEigenschaften;
import de.ivu.elect.business.gruppeimgebiet.control.GruppeImGebietReihenfolgeVorsortierer;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.internet.control.GebietUndStimmabgabeart;
import de.ivu.elect.business.internet.entity.InternetSeite;
import de.ivu.elect.business.kandidatenportal.entity.WvpWarnung;
import de.ivu.elect.business.kandidatur.control.StimmzettelFormatter;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.person.entity.PersonenVerknuepfung;
import de.ivu.elect.business.pruefung.boundary.ZulassungPruefer;
import de.ivu.elect.business.sitzberechnung.control.Sitzberechnungsalgorithmus;
import de.ivu.elect.business.sitzberechnung.control.wahlbeteiligung.BerechnungWahlbeteiligungMittelwertGewichtungsart;
import de.ivu.elect.business.staat.entity.StaatenGruppe;
import de.ivu.elect.business.stimmeingabe.boundary.StimmeingabeErfassenDescriptionResolver;
import de.ivu.elect.business.stimmeingabe.entity.ErgebnisGueltigerEingang;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabePlausiKonfiguration;
import de.ivu.elect.business.stimmen.control.ErgebnisStimmenFilter;
import de.ivu.elect.business.stimmzettel.boundary.EntityAufStimmzettelDatenStimmzettelPositionBerechnung;
import de.ivu.elect.business.stimmzettel.entity.Stimmzettel;
import de.ivu.elect.business.stimmzettel.entity.StimmzettelKopfdatum;
import de.ivu.elect.business.veranstaltung.entity.Veranstaltung;
import de.ivu.elect.business.wahl.control.Loader;
import de.ivu.elect.business.wahl.control.ParameterLoader;
import de.ivu.elect.business.wahl.control.StimmenLoader;
import de.ivu.elect.business.wahl.entity.AbstractWahlParameterLoader;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahlausschuss.entity.WahlausschussEntschaedigung;
import de.ivu.elect.business.wahlausschuss.entity.Wahlausschussmitglied;
import de.ivu.elect.business.wahlbeteiligung.entity.WahlbeteiligungAuswertung;
import de.ivu.elect.business.wahlbeteiligung.entity.WahlbeteiligungVerlauf;
import de.ivu.elect.business.wahlrechtspruefung.entity.Pruefkriterium;
import de.ivu.elect.business.wahlrechtspruefung.entity.Wahlrechtspruefung;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.business.wahlvorschlagsperson.entity.WahlvorschlagsPerson;

public class ErgebnisDaten extends ErgebnisDatenLoader {

  private static final long serialVersionUID = 1L;

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

  @SuppressWarnings("rawtypes")
  private final Sitzberechnungsalgorithmus sitzberechnungsalgorithmus;

  Map<Ergebniseingangsart, Map<Gebiet, Ergebniseingang>> letztenGueltigenEingangFuerGebietUndArt;

  // Sitze Vorperiode: siehe Wahldaten
  private final SitzePool sitzePool = new SitzePool(this::getSitze, this::getListen);

  private Map<Gebiet, List<Gewaehlt>> gewaehlteFuerGebietOderDarunter;

  private Map<Gebiet, List<Gewaehlt>> gewaehlteFuerGebiet;

  private Map<Kandidatur, Gewaehlt> gewaehlteFuerKandidatur;

  private Map<Gebiet, List<ErgebnisKonflikt>> konflikteFuerGebietUndDarunter;

  private Map<Gebiet, Ergebniseingang> letztenGueltigenEingangFuerGebiet;

  public ErgebnisDaten(ErgebnisDaten ergebnisDaten) {
    super(ergebnisDaten);

    this.sitzberechnungsalgorithmus = ergebnisDaten.getSitzberechnungsalgorithmus();
  }

  public ErgebnisDaten(Wahldurchfuehrung wahl, Loader<Wahldurchfuehrungsgebiet> gebieteLoader, Loader<Gruppe> gruppenLoader, Loader<GruppeImGebiet> gruppenImGebietLoader,
      Loader<Wahlvorschlag> wahlvorschlaegeLoader, Loader<Kandidatur> kandidaturenLoader, Loader<PersonenVerknuepfung> personenVerknuepfungenLoader, StimmenLoader stimmenLoader,
      Loader<WahlvorschlagsPerson> wahlvorschlagsPersonenLoader, Loader<Wahlausschussmitglied> wahlausschussmitgliederLoader, Loader<WahlausschussEntschaedigung> wahlausschussEntschaedigungenLoader,
      Loader<StaatenGruppe> staatenGruppenLoader, Loader<Gebietsgruppierung> gebietsgruppierungenLoader, Ergebnis ergebnis, Ergebnis ergebnisVorperiode, StimmenPool stimmen,
      Loader<Gewaehlt> gewaehlteLoader, Loader<Gewaehlt> gewaehlteVorperiodeLoader, Loader<Sitze> sitzeLoader, Loader<Sitze> sitzeVorperiodeLoader, Loader<ErgebnisKonflikt> konflikteLoader,
      SystemGruppeEigenschaften systemGruppeEigenschaften, StimmenKumulierer stimmenKumulierer, @SuppressWarnings("rawtypes") Sitzberechnungsalgorithmus sitzberechnungsalgorithmus,
      Loader<ErgebnisLogEintrag> ergebnisLogeintraegeLoader, Loader<DHondtQuotient> dHondtQuotientenLoader, Loader<ErgebnisGueltigerEingang> gueltigerEingaengeLoader,
      AbstractWahlParameterLoader wahlParameterLoader, ZulassungPruefer zulassungPruefer, StimmzettelFormatter stimmzettelFormatter,
      EntityAufStimmzettelDatenStimmzettelPositionBerechnung entityAufStimmzettelDatenStimmzettelPositionBerechnung, GruppeImGebietReihenfolgeVorsortierer gruppeImGebietReihenfolgeVorsortierer,
      Loader<GebietPolygonPoint> gebietPolygonPointsLoader, Loader<Fremdkandidatur> fremdkandidaturenLoader, Loader<InternetSeite> internetSeitenLoader, Loader<Gebiet> erfassteGebieteLoader,
      ErgebnisStimmenFilter ergebnisStimmenFilter, ParameterLoader<Ergebniseingang, Ergebniseingangsart> letzterErgebniseingangLoader,
      ParameterLoader<ErgebnisGueltigerEingang, Ergebniseingangsart> ergebnisGueltigerEingangLoader, Loader<WahlbeteiligungVerlauf> wahlbeteiligungVerlaeufeLoader,
      Loader<WahlbeteiligungAuswertung> wahlbeteiligungAuswertungenLoader, StimmeingabeErfassenDescriptionResolver stimmeingabeErfassenDescriptionResolver,
      Loader<StimmeingabePlausiKonfiguration> stimmeingabePlausiKonfigurationenLoader, Loader<Veranstaltung> veranstaltungenLoader, Loader<Stimmzettel> stimmzettelLoader,
      Loader<StimmzettelKopfdatum> stimmzettelKopfdatenLoader, Loader<Pruefkriterium> pruefkriterienLoader, Loader<Wahlrechtspruefung> wahlrechtspruefungenLoader,
      Loader<WvpWarnung> wvpWarnungenLoader) {
    super(wahl, gebieteLoader, gruppenLoader, gruppenImGebietLoader, wahlvorschlaegeLoader, kandidaturenLoader, personenVerknuepfungenLoader, stimmenLoader, gewaehlteVorperiodeLoader,
        sitzeVorperiodeLoader, staatenGruppenLoader, gebietsgruppierungenLoader, wahlvorschlagsPersonenLoader, wahlausschussmitgliederLoader, wahlausschussEntschaedigungenLoader, ergebnis,
        ergebnisVorperiode, stimmen, systemGruppeEigenschaften, stimmenKumulierer, wahlParameterLoader, zulassungPruefer, stimmzettelFormatter, entityAufStimmzettelDatenStimmzettelPositionBerechnung,
        gruppeImGebietReihenfolgeVorsortierer, gebietPolygonPointsLoader, fremdkandidaturenLoader, internetSeitenLoader, gewaehlteLoader, sitzeLoader, konflikteLoader, ergebnisLogeintraegeLoader,
        dHondtQuotientenLoader, gueltigerEingaengeLoader, erfassteGebieteLoader, ergebnisStimmenFilter, letzterErgebniseingangLoader, ergebnisGueltigerEingangLoader, wahlbeteiligungVerlaeufeLoader,
        wahlbeteiligungAuswertungenLoader, stimmeingabeErfassenDescriptionResolver, stimmeingabePlausiKonfigurationenLoader, veranstaltungenLoader, stimmzettelLoader, stimmzettelKopfdatenLoader,
        pruefkriterienLoader, wahlrechtspruefungenLoader, wvpWarnungenLoader);

    this.sitzberechnungsalgorithmus = sitzberechnungsalgorithmus;
  }

  public ErgebnisDaten(StimmergebnisDaten stimmergebnisDaten, Loader<Gewaehlt> gewaehlteLoader, Loader<Sitze> sitzeLoader, Loader<ErgebnisKonflikt> konflikteLoader,
      Loader<ErgebnisLogEintrag> ergebnisLogeintraegeLoader, @SuppressWarnings("rawtypes") Sitzberechnungsalgorithmus sitzberechnungsalgorithmus, Loader<DHondtQuotient> dHondtQuotientenLoader,
      Loader<ErgebnisGueltigerEingang> gueltigerEingaengeLoader) {
    super(stimmergebnisDaten, gewaehlteLoader, sitzeLoader, konflikteLoader, ergebnisLogeintraegeLoader, dHondtQuotientenLoader, gueltigerEingaengeLoader);

    this.sitzberechnungsalgorithmus = sitzberechnungsalgorithmus;
  }

  public List<Gewaehlt> getGewaehlteFuerGebietOderDarunter(Gebiet gebiet) {
    initGewaehlteFuerGebietOderDarunter();

    return gewaehlteFuerGebietOderDarunter.get(gebiet);
  }

  public List<Gewaehlt> getGewaehlteFuerGebietOderDarunter(Gebiet gebiet, Predicate<Gewaehlt> gewaehltPredicate) {
    return getGewaehlteFuerGebietOderDarunter(gebiet).stream().filter(gewaehltPredicate).collect(Collectors.toList());
  }

  private final Map<Gebiet, Gewaehlt> gewaehlteDirektkandidaturCache = new HashMap<>();

  /**
   * @param gebiet
   * @return wenn gebiet ein Direktwahlgebiet ist und es dort genau einen gewählten Bewerber gibt, antworte diese Kandidatur, ansonsten null.
   */
  @Nullable
  public Kandidatur getGewaehlteDirektkandidatur(Gebiet gebiet) {

    Gewaehlt gewaehlteDirektkandidaturAlsGewaehlt = getGewaehlteDirektkandidaturAlsGewaehlt(gebiet);

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

    return gewaehlteDirektkandidaturAlsGewaehlt.getKandidatur();
  }

  @Nullable
  public Gewaehlt getGewaehlteDirektkandidaturAlsGewaehlt(Gebiet gebiet) {
    if (gebiet == null) {
      return null;
    }

    return gewaehlteDirektkandidaturCache.computeIfAbsent(gebiet, x -> {
      if (!gebiet.isDirektwahlgebiet()) {
        return null;
      }

      List<Gewaehlt> gewaehlte = new ArrayList<>(getGewaehlteFuerGebiet(gebiet));
      gewaehlte.removeIf(g -> !g.getGewaehltStatus().isGewaehlt());
      if (gewaehlte.size() != 1) {
        return null;
      }

      return gewaehlte.get(0);
    });
  }

  private final Map<Wahldurchfuehrungsgebiet, Kandidatur> erstunterlegenerDirektkandidatCache = new HashMap<>();

  @Nullable
  public Kandidatur getErstunterlegenerDirektkandidat(Wahldurchfuehrungsgebiet gebiet) {
    if (gebiet == null) {
      return null;
    }
    return erstunterlegenerDirektkandidatCache.computeIfAbsent(gebiet, x -> getErstunterlegenerDirektkandidat(gebiet, getGewaehlteDirektkandidatur(gebiet)));
  }

  /**
   * @return wenn das gebiet ein Direktwahlgebiet ist, antworte den Kandidaten, der nicht gewaehlterKandidat ist, aber danach die meisten Erststimmen im Gebiet hat. Wenn das gebiet kein
   *         Direktwahlgebiet ist oder wenn es mehrere oder keinen solchen Kandidaten gibt, return null.
   */
  @Nullable
  public Kandidatur getErstunterlegenerDirektkandidat(Wahldurchfuehrungsgebiet gebiet, Kandidatur gewaehlterKandidat) {
    if (gewaehlterKandidat == null) {
      return null;
    }

    if (!gebiet.isDirektwahlgebiet()) {
      return null;
    }

    List<Kandidatur> erstunterlegene = new ArrayList<>();
    long besteStimmenzahl = -1;

    Map<PolitischeGruppe, Long> direktstimmenAktuell = getErststimmenAktuell(gebiet, Stimmabgabeart.GESAMT);

    for (Direktkandidatur direktkandidatur : getDirektkandidaturenNachGebiet(gebiet)) {
      if (!direktkandidatur.equals(gewaehlterKandidat)) {
        long anzahlStimmen = direktstimmenAktuell.getOrDefault(direktkandidatur.getGruppe(), 0L);
        if (anzahlStimmen > besteStimmenzahl) {
          besteStimmenzahl = anzahlStimmen;
          erstunterlegene.clear();
        }
        if (anzahlStimmen == besteStimmenzahl) {
          erstunterlegene.add(direktkandidatur);
        }
      }
    }

    if (erstunterlegene.size() != 1) {
      return null;
    }

    return erstunterlegene.get(0);
  }

  public List<Gewaehlt> getGewaehlteFuerGebiet(Gebiet gebiet) {
    if (gewaehlteFuerGebiet == null) {
      gewaehlteFuerGebiet = initGewaehlteFuerGebiet();
    }
    List<Gewaehlt> result = gewaehlteFuerGebiet.get(gebiet);
    if (result == null) {
      return Collections.emptyList();
    }
    return result;
  }

  private Map<Gebiet, List<Gewaehlt>> initGewaehlteFuerGebiet() {
    Map<Gebiet, List<Gewaehlt>> result = new HashMap<>();
    for (Gewaehlt gewaehlt : getGewaehlte()) {
      Gebiet gebiet = gewaehlt.getKandidatur().getWahlvorschlag().getGebiet();
      if (!result.containsKey(gebiet)) {
        result.put(gebiet, new ArrayList<>());
      }
      result.get(gebiet).add(gewaehlt);
    }
    return result;
  }

  private void initGewaehlteFuerGebietOderDarunter() {
    if (gewaehlteFuerGebietOderDarunter == null) {
      gewaehlteFuerGebietOderDarunter = new HashMap<>();
      for (Gewaehlt gewaehlt : getGewaehlte()) {
        Gebiet gebiet = gewaehlt.getKandidatur().getWahlvorschlag().getGebiet();
        putGewaehltInGebietUndDrueber(gebiet, gewaehlt, gewaehlteFuerGebietOderDarunter);
      }

      for (Gebiet gebiet : getGebiete()) {
        if (!gewaehlteFuerGebietOderDarunter.containsKey(gebiet)) {
          gewaehlteFuerGebietOderDarunter.put(gebiet, new ArrayList<>());
        }
      }

    }
  }

  private void putGewaehltInGebietUndDrueber(Gebiet gebiet, Gewaehlt gewaehlt, Map<Gebiet, List<Gewaehlt>> gewaehlteFuerGebietOderDarunter) {

    List<Gewaehlt> gewaehlteInGebiet = gewaehlteFuerGebietOderDarunter.computeIfAbsent(gebiet, k -> new ArrayList<>());
    gewaehlteInGebiet.add(gewaehlt);
    if (gebiet.getUebergeordnetesGebiet() != null) {
      putGewaehltInGebietUndDrueber(gebiet.getUebergeordnetesGebiet(), gewaehlt, gewaehlteFuerGebietOderDarunter);
    }

  }

  public Gewaehlt getGewaehlteFuerKandidatur(Kandidatur kandidatur) {
    initGewaehlteFuerKandidatur();
    return gewaehlteFuerKandidatur.get(kandidatur);
  }

  private void initGewaehlteFuerKandidatur() {
    if (gewaehlteFuerKandidatur == null) {
      gewaehlteFuerKandidatur = new HashMap<>();
      for (Gewaehlt gewaehlt : getGewaehlte()) {
        gewaehlteFuerKandidatur.put(gewaehlt.getKandidatur(), gewaehlt);
      }
    }
  }

  public List<Sitze> getSitzeFuerGebiet(Gebiet gebiet) {
    return sitzePool.getSitzeFuerGebiet(gebiet);
  }

  public List<Sitze> getSitzeFuerGebietUndDarunter(Gebiet gebiet) {
    return sitzePool.getSitzeFuerGebietUndDarunter(gebiet);
  }

  public Long getAnzahlSitzeGesamtFuerGebietUndDarunter(Gebiet gebiet) {
    Long summeSitze = 0L;
    for (Entry<Gruppe, Long> sitzeFuerGruppe : getAnzahlSitzeFuerGebietUndDarunter(gebiet).entrySet()) {
      summeSitze += sitzeFuerGruppe.getValue();
    }

    return summeSitze;
  }

  private final Map<Gebiet, Map<Gruppe, Long>> anzahlSitzeFuerGebietUndDarunterCache = new HashMap<>();

  public Map<Gruppe, Long> getAnzahlSitzeFuerGebietUndDarunter(Gebiet gebiet) {
    return anzahlSitzeFuerGebietUndDarunterCache.computeIfAbsent(gebiet, x -> sitzePool.getAnzahlSitzeFuerGebietUndDarunter(gebiet));
  }

  public Collection<Sitze> getSitzeFuerPolitischeGruppe(PolitischeGruppe politischeGruppe) {
    return sitzePool.getSitzeFuerPolitischeGruppe(politischeGruppe);
  }

  public Sitze getSitzeFuerListe(Liste liste) {
    return sitzePool.getSitzeFuerListe(liste);
  }

  public List<ErgebnisKonflikt> getKonflikteFuerGebietUndDarunter(Gebiet gebiet) {
    initKonflikteFuerGebietUndDarunter();

    return konflikteFuerGebietUndDarunter.get(gebiet);
  }

  private void initKonflikteFuerGebietUndDarunter() {

    if (konflikteFuerGebietUndDarunter == null) {
      konflikteFuerGebietUndDarunter = new HashMap<>();
      for (Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet : getGebiete()) {
        konflikteFuerGebietUndDarunter.put(wahldurchfuehrungsgebiet, new ArrayList<>());
      }

      for (ErgebnisKonflikt ergebnisKonflikt : getKonflikte()) {
        Gebiet gebiet = ergebnisKonflikt.getGebiet(getKandidaturen(), getListen(), getListenverbindungen());
        while (gebiet != null) {
          konflikteFuerGebietUndDarunter.get(gebiet).add(ergebnisKonflikt);
          gebiet = gebiet.getUebergeordnetesGebiet();
        }
      }
    }
  }

  @SuppressWarnings("rawtypes")
  public Sitzberechnungsalgorithmus getSitzberechnungsalgorithmus() {
    return sitzberechnungsalgorithmus;
  }

  public List<DHondtQuotientPolitischeGruppe> getDHondtQuotientenPolitischeGruppeSortiertNachRang() {

    return getdHondtQuotienten().stream().filter(dHondtQuotient -> dHondtQuotient.isInstanceOf(DHondtQuotientPolitischeGruppe.class))
        .map(dHondtQuotient -> dHondtQuotient.castTo(DHondtQuotientPolitischeGruppe.class).getEntity()).sorted(new DHondtQuotientByRangComparator()).collect(Collectors.toList());
  }

  public Map<Wahldurchfuehrungsgebiet, List<DHondtQuotientListenImGebiet>> getDHondtQuotientListenImGebiet() {
    Map<Wahldurchfuehrungsgebiet, List<DHondtQuotientListenImGebiet>> result = new HashMap<>();
    for (DHondtQuotient dHondtQuotient : getdHondtQuotienten()) {
      if (dHondtQuotient.isInstanceOf(DHondtQuotientListenImGebiet.class)) {
        DHondtQuotientListenImGebiet dHondtQuotientListenImGebiet = dHondtQuotient.castTo(DHondtQuotientListenImGebiet.class).getEntity();
        Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = dHondtQuotientListenImGebiet.getWahldurchfuehrungsgebiet();
        if (!result.containsKey(wahldurchfuehrungsgebiet)) {
          result.put(wahldurchfuehrungsgebiet, new ArrayList<>());
        }
        result.get(wahldurchfuehrungsgebiet).add(dHondtQuotientListenImGebiet);
      }
    }
    for (List<DHondtQuotientListenImGebiet> dHondtQuotientListenImGebiets : result.values()) {
      dHondtQuotientListenImGebiets.sort(new DHondtQuotientByRangComparator());
    }

    return result;
  }

  public Map<PolitischeGruppe, List<DHondtQuotientListenInGruppe>> getDHondtQuotientListenInGruppe() {
    Map<PolitischeGruppe, List<DHondtQuotientListenInGruppe>> result = new HashMap<>();
    for (DHondtQuotient dHondtQuotient : getdHondtQuotienten()) {
      if (dHondtQuotient.isInstanceOf(DHondtQuotientListenInGruppe.class)) {
        DHondtQuotientListenInGruppe dHondtQuotientListenInGruppe = dHondtQuotient.castTo(DHondtQuotientListenInGruppe.class).getEntity();

        PolitischeGruppe politischeGruppe = dHondtQuotientListenInGruppe.getPolitischeGruppe();

        if (!result.containsKey(politischeGruppe)) {
          result.put(politischeGruppe, new ArrayList<>());
        }
        result.get(politischeGruppe).add(dHondtQuotientListenInGruppe);
      }
    }
    for (List<DHondtQuotientListenInGruppe> dHondtQuotientListenInGruppe : result.values()) {
      dHondtQuotientListenInGruppe.sort(new DHondtQuotientByRangComparator());
    }

    return result;
  }

  public Ergebniseingang getLetztenGueltigenEingangFuerGebiet(Gebiet gebiet) {
    if (letztenGueltigenEingangFuerGebiet == null) {
      letztenGueltigenEingangFuerGebiet = getGueltigerEingaenge().stream().collect(toMap(e -> e.getErgebniseingang().getGebiet(), ErgebnisGueltigerEingang::getErgebniseingang));
    }
    return letztenGueltigenEingangFuerGebiet.get(gebiet);
  }

  @Nullable
  @Deprecated // Unsinn
  public ErgebnisGueltigerEingang getLetztenGueltigenEingang() {
    ErgebnisGueltigerEingang result = null;
    for (ErgebnisGueltigerEingang ergebnisGueltigerEingang : getGueltigerEingaenge()) {
      if (result == null || result.getErgebniseingang().getZeitstempel().before(ergebnisGueltigerEingang.getErgebniseingang().getZeitstempel())) {
        result = ergebnisGueltigerEingang;
      }
    }

    return result;
  }

  @Nullable
  public Ergebniseingang getLetztenGueltigenEingangFuerGebietUndArt(Gebiet gebiet, Ergebniseingangsart ergebniseingangsart) {
    if (letztenGueltigenEingangFuerGebietUndArt == null) {
      letztenGueltigenEingangFuerGebietUndArt = new EnumMap<>(Ergebniseingangsart.class);
      for (ErgebnisGueltigerEingang ergebnisGueltigerEingang : getGueltigerEingaenge()) {
        Ergebniseingangsart ergebniseingangsartFuerErgebnisArt = Ergebniseingangsart.getErgebniseingangsartFuerErgebnisArt(ergebnisGueltigerEingang.getErgebnis().getErgebnisart());
        Map<Gebiet, Ergebniseingang> nachGebiet = letztenGueltigenEingangFuerGebietUndArt.computeIfAbsent(ergebniseingangsartFuerErgebnisArt, x -> new HashMap<>());
        nachGebiet.put(ergebnisGueltigerEingang.getGebiet(), ergebnisGueltigerEingang.getErgebniseingang());
      }
    }
    return letztenGueltigenEingangFuerGebietUndArt.computeIfAbsent(ergebniseingangsart, x -> new HashMap<>()).get(gebiet);
  }

  public Optional<Entry<PolitischeGruppe, Long>> getPolitischeGruppeMeisteErststimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getEntryWithLargestValue(getErststimmenAktuellGesamt(wahldurchfuehrungsgebiet));
  }

  public Optional<Entry<PolitischeGruppe, Long>> getPolitischeGruppeMeisteErststimmenVorperiode(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getEntryWithLargestValue(getErststimmenVorperiodeGesamt(wahldurchfuehrungsgebiet));
  }

  public Optional<Entry<PolitischeGruppe, Long>> getPolitischeGruppeMeisteZweitstimmen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getEntryWithLargestValue(getZweitstimmenAktuellGesamt(wahldurchfuehrungsgebiet));
  }

  public Optional<Entry<PolitischeGruppe, Long>> getPolitischeGruppeMeisteZweitstimmenVorperiode(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getEntryWithLargestValue(getZweitstimmenVorperiodeGesamt(wahldurchfuehrungsgebiet));
  }

  private <K> Optional<Entry<K, Long>> getEntryWithLargestValue(Map<K, Long> map) {
    return map.entrySet().stream().max((entry1, entry2) -> entry1.getValue() > entry2.getValue() ? 1 : -1);
  }

  private final Map<ErgebnisdatenWahlbeteiligungCacheKey, ErgebnisDatenWahlbeteiligung> ergebnisDatenWahlbeteiligung = new HashMap<>();

  public boolean isAusgezaehlt(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return getLetztenGueltigenEingangFuerGebiet(wahldurchfuehrungsgebiet) != null;
  }

  public Long getAnzahlSitzeFuerPolitischeGruppe(PolitischeGruppe politischeGruppe) {
    return getSitzeFuerPolitischeGruppe(politischeGruppe).stream().mapToLong(Sitze::getAnzahl).sum();
  }

  @Nullable
  public BigDecimal getWahlbeteiligung(GebietUndStimmabgabeart gebietUndStimmabgabeart, boolean hochrechnungVerwenden,
      BerechnungWahlbeteiligungMittelwertGewichtungsart berechnungWahlbeteiligungMittelwertGewichtungsart, double defaultRuecklaufquote, int anzahlNachkommastellen,
      WahlbeteiligungWahlberechtigeQuelle wahlberechtigeQuelle) {

    if (hochrechnungVerwenden) {
      try {
        ErgebnisDatenWahlbeteiligung ergebnisDatenWahlbeteiligung = this.ergebnisDatenWahlbeteiligung.computeIfAbsent(
            new ErgebnisdatenWahlbeteiligungCacheKey(berechnungWahlbeteiligungMittelwertGewichtungsart, defaultRuecklaufquote, anzahlNachkommastellen),
            key -> new ErgebnisDatenWahlbeteiligung(this, berechnungWahlbeteiligungMittelwertGewichtungsart, defaultRuecklaufquote, anzahlNachkommastellen, wahlberechtigeQuelle));

        BigDecimal wahlbeteiligung = ergebnisDatenWahlbeteiligung.getWahlbeteiligung(gebietUndStimmabgabeart);
        // Niemals mehr als 100 % Wahlbeteiligung anzeigen
        if (wahlbeteiligung != null && wahlbeteiligung.compareTo(BigDecimal.ONE) > 0) {
          return null;
        }
        return wahlbeteiligung;
      } catch (Exception e) {
        LOG.error("Fehler beim Berechnen der Wahlbeteiligung: " + e.getMessage(), e);
      }
    }

    return getWahlbeteiligungOhneHochrechnung(gebietUndStimmabgabeart, anzahlNachkommastellen, wahlberechtigeQuelle);
  }

  @Nullable
  public BigDecimal getWahlbeteiligungOhneHochrechnung(GebietUndStimmabgabeart gebietUndStimmabgabeart, int anzahlNachkommastellen,
                                                  WahlbeteiligungWahlberechtigeQuelle wahlberechtigeQuelle) {
    // ELECT-9520 simpel und dumm
    if (gebietUndStimmabgabeart.getWahldurchfuehrungsgebiet() == null) {
      return null;
    }
    Long wahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl = wahlberechtigeQuelle.bestimmeAnzahlWahlberechtigte(
        () -> getWahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl(gebietUndStimmabgabeart),
        () -> gebietUndStimmabgabeart.getWahldurchfuehrungsgebiet().getAnzahlWahlberechtigterGesamtA1A2A3());
    if (wahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl == null || wahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl == 0L) {
      return null;
    }

    Long waehlerAktuellGesamt = getWaehlerAktuellGesamt(gebietUndStimmabgabeart);
    if (waehlerAktuellGesamt == null || waehlerAktuellGesamt == 0L) {
      return null;
    }

    if (waehlerAktuellGesamt > wahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl) {
      return null;
    }

    return BigDecimal.valueOf(waehlerAktuellGesamt).divide(BigDecimal.valueOf(wahlberechtigteGesamtAusgezaehltFuerErfassungsgebietUnterhalbVonAnzahl),
        anzahlNachkommastellen, RoundingMode.HALF_UP);
  }

  public boolean hatDirektkandidaturMitVorperiodenstimmenInGebietshierarchie(PolitischeGruppe politischeGruppe, Wahldurchfuehrungsgebiet gebiet) {
    return getDirektkandidaturenInGebietshierarchie(politischeGruppe, gebiet).stream()
        .map(direktkandidatur -> getErststimmenVorperiodeGesamt(politischeGruppe, direktkandidatur.getGebiet().castTo(Wahldurchfuehrungsgebiet.class).getEntity()))
        .anyMatch(direktstimmenVorperiodeGesamt -> direktstimmenVorperiodeGesamt != null && direktstimmenVorperiodeGesamt > 0L);
  }

  public boolean hatListeMitVorperiodenstimmenInGebietshierarchie(PolitischeGruppe politischeGruppe, Wahldurchfuehrungsgebiet gebiet) {
    return getListenInGebietshierarchie(politischeGruppe, gebiet).stream()
        .map(liste -> getZweitstimmenVorperiodeGesamt(politischeGruppe, liste.getGebiet().castTo(Wahldurchfuehrungsgebiet.class).getEntity()))
        .anyMatch(listenstimmenVorperiodeGesamt -> listenstimmenVorperiodeGesamt != null && listenstimmenVorperiodeGesamt > 0L);
  }
}
