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

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

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.gruppe.entity.SystemGruppe;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.GebietDatenWahlbeteiligung;
import de.ivu.elect.business.hochrechnung.entity.GewichteteSkalierung;
import de.ivu.elect.business.hochrechnung.entity.GewichteterWert;
import de.ivu.elect.business.hochrechnung.entity.KoordinatenInformationen;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmen;
import de.ivu.elect.business.hochrechnung.entity.ReferendumFrage;
import de.ivu.elect.business.hochrechnung.entity.WahlteilnehmerAnzahl;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption;


import static de.ivu.elect.business.hochrechnung.control.SkalierungImErgebnisBerechner.SEHR_KLEINE_STIMMENANTEIL;
import static java.util.Arrays.asList;

public class GebietDatenHochrechnerAusGewichtetenSkalierungen {
  private static final Double FAKTOR_ZUR_REDUZIERUNG_DER_SKALIERUNG_FUER_IN_VORPERIODE_SEHR_KLEINE_ZAHLEN = 1E2;
  public static final long VERHAELTNIS_ANZAHL_STIMMEN_ERSTES_LISTENKANDIDATURS_ZU_ANZAHL_STIMMEN_LETZTES_LISTENKANDIDATURS = 10;
  private final GebietDatenNormierer<Double> gebietDatenNormierer = new GebietDatenNormierer<>();

  public WahlteilnehmerAnzahl getAnzahlWahlberechtigteImGebietFuerHochrechnung(WahlteilnehmerAnzahl anzahlWahlberechtigteVorperiode, WahlteilnehmerAnzahl anzahlWahlberechtigteAktuell) {
    if (anzahlWahlberechtigteAktuell == null || anzahlWahlberechtigteAktuell.getGesamtNotNull() == 0) {
      return anzahlWahlberechtigteVorperiode != null? anzahlWahlberechtigteVorperiode : new WahlteilnehmerAnzahl();
    }
    return anzahlWahlberechtigteAktuell;
  }

  public WahlteilnehmerAnzahl getAnzahlWaehlerImGebietFuerHochrechnung(WahlteilnehmerAnzahl anzahlWaehlerImGebietInAktuelleWahl, GebietDatenWahlbeteiligung hochgerechneteWahlbeteiligung, WahlteilnehmerAnzahl anzahlWahlberechtigteAktuellOderHochgerechnet) {
    if (anzahlWaehlerImGebietInAktuelleWahl != null && anzahlWaehlerImGebietInAktuelleWahl.getGesamtNotNull() > 0) {
      return anzahlWaehlerImGebietInAktuelleWahl;
    }
    return berechneAnzahlWaehlerVonHochgerechneterWahlbeteiligung(hochgerechneteWahlbeteiligung, anzahlWahlberechtigteAktuellOderHochgerechnet);
  }

  private WahlteilnehmerAnzahl berechneAnzahlWaehlerVonHochgerechneterWahlbeteiligung(GebietDatenWahlbeteiligung hochgerechneteWahlbeteiligung, WahlteilnehmerAnzahl anzahlWahlberechtigteAktuellOderHochgerechnet) {
    WahlteilnehmerAnzahl result = new WahlteilnehmerAnzahl();
    if (anzahlWahlberechtigteAktuellOderHochgerechnet == null) {
      return result;
    }

    for (Stimmenart stimmenart: asList(Stimmenart.OHNE_WAHLSCHEIN, Stimmenart.MIT_WAHLSCHEIN, Stimmenart.NICHT_IM_WAEHLERVERZEICHNIS)) {
      result.setAnzahl(stimmenart, Math.round(hochgerechneteWahlbeteiligung.getAnzahlNotNull(stimmenart) * anzahlWahlberechtigteAktuellOderHochgerechnet.getAnzahlNotNull(stimmenart)));
    }
    // weil Anzahl Waehler ist manchmal nur als GESAMT erfasst wird
    if (result.getGesamtNotNull() == 0) {
      result.setAnzahl(Stimmenart.GESAMT, Math.round(hochgerechneteWahlbeteiligung.getGesamtNotNull() * anzahlWahlberechtigteAktuellOderHochgerechnet.getGesamtNotNull()));
    }
    return result;
  }

  public Map<PolitischeGruppe, ParteiStimmen<Double>> getParteiStimmenHochgerechnet(GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    Map<PolitischeGruppe, ParteiStimmen<Double>> resultUnnormiert = new HashMap<>();
    List<PolitischeGruppe> fuerGebietRelevantePolitischeGruppen = getFuerGebietRelevantePolitischeGruppen(gebietDatenAktuellAlsAnzahlen,
        koordinatenInformationen);
    fuerGebietRelevantePolitischeGruppen
        .forEach(politischeGruppe -> resultUnnormiert.put(politischeGruppe, getHochgerechneteParteiStimme(politischeGruppe, gebietDatenVorperiode, gebietDatenAktuellAlsAnzahlen, gewichteteSkalierungen, koordinatenInformationen)));
    return gebietDatenNormierer.getParteiStimmenAnteileNormiert(resultUnnormiert);
  }

  private List<PolitischeGruppe> getFuerGebietRelevantePolitischeGruppen(GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, KoordinatenInformationen koordinatenInformationen) {
    List<PolitischeGruppe> result = extractPolitischeGruppenVonGebietDaten(gebietDatenAktuellAlsAnzahlen);
    if (!result.isEmpty()) {
      return result;
    }
    return koordinatenInformationen.getAllePolitischeGruppenSortiert();
  }

  private ArrayList<PolitischeGruppe> extractPolitischeGruppenVonGebietDaten(GebietDaten<? extends Number> gebietDatenAktuellAlsAnzahlen) {
    if (gebietDatenAktuellAlsAnzahlen == null) {
      return new ArrayList<>();
    }
    return new ArrayList<>(gebietDatenAktuellAlsAnzahlen.getParteiStimmen().keySet());
  }

  private ParteiStimmen<Double> getHochgerechneteParteiStimme(PolitischeGruppe politischeGruppe, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    Map<Stimmenart, Double> stimmen = new HashMap<>();
    koordinatenInformationen.getAlleStimmenartenSortiert()
        .forEach(stimmenart -> stimmen.put(stimmenart, getHochgerechneteStimme(politischeGruppe, stimmenart, gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen)));
    Map<Listenkandidatur, Double> listenkandidaturen = getListenkandidaturenStimmenHochgerechnet(politischeGruppe, gebietDatenVorperiode, gebietDatenAktuellAlsAnzahlen, gewichteteSkalierungen, koordinatenInformationen);
    return new ParteiStimmen<>(politischeGruppe, stimmen, listenkandidaturen);
  }

  private Map<Listenkandidatur, Double> getListenkandidaturenStimmenHochgerechnet(PolitischeGruppe politischeGruppe, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    if (gebietDatenAktuellAlsAnzahlen == null) {
      return Collections.emptyMap();
    }
    ParteiStimmen<Long> parteiStimme = gebietDatenAktuellAlsAnzahlen.getParteiStimmen().get(politischeGruppe);
    if (parteiStimme == null) {
      return Collections.emptyMap();
    }

    Set<Listenkandidatur> listenkandidaturen = parteiStimme.getListenkandidaturStimmen().keySet();
    if (listenkandidaturen.size() < 1) {
      return Collections.emptyMap();
    }

    Map<Listenkandidatur, Double> result = new HashMap<>();

    List<GewichteteSkalierung> filteredGewichteteSkalierungen = gewichteteSkalierungen.stream()
        .filter(gewichteteSkalierung -> isGebieteHabenGleicheListenKandidaturen(politischeGruppe, listenkandidaturen, gewichteteSkalierung)).collect(
            Collectors.toList());

    for (Listenkandidatur listenkandidatur: listenkandidaturen) {
      result.put(listenkandidatur,
          getHochgerechneteListenKandidaturStimme(politischeGruppe, listenkandidatur, gebietDatenVorperiode, filteredGewichteteSkalierungen));
    }

    if (!isErgebnisVorhanden(result)) {
      result = ParteiStimmenAlsPunkteAnEinerParabelGenerator.erzeugeListenKandidaturStimmenAlsPunkteAnEinerParabel(VERHAELTNIS_ANZAHL_STIMMEN_ERSTES_LISTENKANDIDATURS_ZU_ANZAHL_STIMMEN_LETZTES_LISTENKANDIDATURS, listenkandidaturen);
    }
    return result;
  }

  private boolean isErgebnisVorhanden(Map<Listenkandidatur, Double> result) {
    return result.values().stream().mapToDouble(StimmenHochrechnerHilfsfunktionen::notNull).sum() > 0;
  }

  private boolean isGebieteHabenGleicheListenKandidaturen(PolitischeGruppe politischeGruppe, Set<Listenkandidatur> listenkandidaturenAmGebiet, GewichteteSkalierung gewichteteSkalierung) {
    ParteiStimmen<Double> parteiStimmen = gewichteteSkalierung.getSkalierung().getParteiStimmen().get(politischeGruppe);
    return parteiStimmen != null && parteiStimmen.getListenkandidaturStimmen().keySet().equals(listenkandidaturenAmGebiet);
  }

  private Double getHochgerechneteListenKandidaturStimme(PolitischeGruppe politischeGruppe, Listenkandidatur listenkandidatur, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen) {
    return getHochgerechnetenWert(getListenKandidaturStimmenValueGetter(politischeGruppe, listenkandidatur),
        gebietDatenVorperiode,
        gewichteteSkalierungen,
        false);
  }

  private Function<GebietDaten<Double>, Double> getListenKandidaturStimmenValueGetter(PolitischeGruppe politischeGruppe, Listenkandidatur listenkandidatur) {
    return gebietDaten -> getListenKandidaturStimme(gebietDaten, politischeGruppe, listenkandidatur);
  }

  private Double getListenKandidaturStimme(GebietDaten<Double> gebietDaten, PolitischeGruppe politischeGruppe, Listenkandidatur listenkandidatur) {
    ParteiStimmen<Double> parteiStimme = gebietDaten.getParteiStimmen().get(politischeGruppe);
    if (parteiStimme != null) {
      return parteiStimme.getListenkandidaturStimmen().get(listenkandidatur);
    }
    return null;
  }

  private Double getHochgerechneteStimme(PolitischeGruppe politischeGruppe, Stimmenart stimmenart, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getHochgerechnetenWert(getParteistimmenValueGetter(politischeGruppe, stimmenart),
        gebietDatenVorperiode,
        gewichteteSkalierungen,
        isPolitischeGruppeStimmeAufGebietInVorperiodeVorhanden(gebietDatenVorperiode, politischeGruppe, stimmenart, koordinatenInformationen));
  }

  private boolean isPolitischeGruppeStimmeAufGebietInVorperiodeVorhanden(GebietDaten<Double> gebietDatenVorperiode, PolitischeGruppe politischeGruppe, Stimmenart stimmenart, KoordinatenInformationen koordinatenInformationen) {
    if (gebietDatenVorperiode == null) {
      return false;
    }
    return koordinatenInformationen.getGruppeInVorperiodeVorhandenChecker().isPolitischeGruppeInVorperiodeVorhanden(politischeGruppe, stimmenart) && gebietDatenVorperiode.getParteiStimmen().containsKey(politischeGruppe) && gebietDatenVorperiode.getParteiStimmen().get(politischeGruppe).getStimmen().containsKey(stimmenart);
  }


  public GebietDatenWahlbeteiligung getHochgerechneteWahlbeteiligung(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen) {
    GebietDatenWahlbeteiligung result = getHochgerechneteWahlbeteiligungVonSkalierungGebietenGleicherArt(gebietDatenVorperiode, gewichteteSkalierungen);
    if (isWahlteinhemerAnzahlVorhanden(result)) {
      return result;
    }
    return getHochgerechneteWahlbeteiligungVonSkalierungAllerGebiete(gebietDatenVorperiode, gewichteteSkalierungen);
  }

  private boolean isWahlteinhemerAnzahlVorhanden(GebietDatenWahlbeteiligung result) {
    return result.getOhneWahlscheinNotNull() + result.getMitWahlscheinNotNull() + result.getGesamtNotNull() > .001;
  }

  private GebietDatenWahlbeteiligung getHochgerechneteWahlbeteiligungVonSkalierungAllerGebiete(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen) {
    GebietDatenWahlbeteiligung result = new GebietDatenWahlbeteiligung();
    for (Stimmenart stimmenart: asList(Stimmenart.OHNE_WAHLSCHEIN, Stimmenart.MIT_WAHLSCHEIN, Stimmenart.NICHT_IM_WAEHLERVERZEICHNIS, Stimmenart.GESAMT)) {
      result.setAnzahl(stimmenart, trim(getHochgerechnetenWert(getWahlbeteiligungValueGetter(stimmenart), gebietDatenVorperiode, gewichteteSkalierungen, gebietDatenVorperiode != null && gebietDatenVorperiode.getWahlbeteiligung() != null && gebietDatenVorperiode.getWahlbeteiligung().getGesamtNotNull() > 0)));
    }
    return result;
  }

  private GebietDatenWahlbeteiligung getHochgerechneteWahlbeteiligungVonSkalierungGebietenGleicherArt(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen) {
    GebietDatenWahlbeteiligung result = new GebietDatenWahlbeteiligung();
    List<GewichteteSkalierung> filteredGewichteteSkalierungen = getGebieteAehnlicherArtZurErzeugungVonHochgerechneterWahlbeteiligung(gebietDatenVorperiode, gewichteteSkalierungen);
    for (Stimmenart stimmenart: asList(Stimmenart.OHNE_WAHLSCHEIN, Stimmenart.MIT_WAHLSCHEIN, Stimmenart.NICHT_IM_WAEHLERVERZEICHNIS, Stimmenart.GESAMT)) {
      result.setAnzahl(stimmenart, trim(getHochgerechnetenWert(getWahlbeteiligungValueGetter(stimmenart), gebietDatenVorperiode, filteredGewichteteSkalierungen, gebietDatenVorperiode != null && gebietDatenVorperiode.getWahlbeteiligung() != null && gebietDatenVorperiode.getWahlbeteiligung().getGesamtNotNull() > 0)));
    }
    return result;
  }

  private List<GewichteteSkalierung> getGebieteAehnlicherArtZurErzeugungVonHochgerechneterWahlbeteiligung(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen) {
    if (gebietDatenVorperiode == null) {
      return gewichteteSkalierungen;
    }
    return gewichteteSkalierungen.stream().filter(skalierung -> sindGebieteAehnlicherArtZurErzeugungVonHochgerechneterWahlbeteiligung(gebietDatenVorperiode.getGebiet(), skalierung.getSkalierung().getGebiet())).collect(
        Collectors.toList());
  }

  private boolean sindGebieteAehnlicherArtZurErzeugungVonHochgerechneterWahlbeteiligung(Gebiet gebiet, Gebiet skalierungGebiet) {
    // Beide Gebiete muessen gemeinsam Obergebiete, Briefwahlgebiete oder wie Urnengebiete sein.
    if (gebiet.isErfassungsgebiet() != skalierungGebiet.isErfassungsgebiet()) {
      return false;
    }
    if (gebiet.isBriefwahlgebiet() != skalierungGebiet.isBriefwahlgebiet()) {
      return false;
    }
    return true;
  }

  private Function<GebietDaten<Double>, Double> getWahlbeteiligungValueGetter(Stimmenart stimmenart) {
    return gebietDaten -> getWahlbeteiligung(gebietDaten, stimmenart);
  }

  private Double getWahlbeteiligung(GebietDaten<Double> gebietDaten, Stimmenart stimmenart) {
    if (gebietDaten != null && gebietDaten.getWahlbeteiligung() != null) {
      return gebietDaten.getWahlbeteiligung().getAnzahl(stimmenart);
    }
    return null;
  }


  public Map<Stimmenart, Double> getUngueltigeVonGueltige(Map<Stimmenart, Double> gueltige) {
    return gebietDatenNormierer.berechneUngueltigeAnteileVonGueltige(gueltige);
  }

  public Map<Stimmenart, Double> getGueltigeHochgerechnet(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    Map<Stimmenart, Double> gueltigeUnnormiert = getGueltigeHochgerechnetUnnormiert(gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);
    Map<Stimmenart, Double> ungueltigeUnnormiert = getUngueltigeHochgerechnetUnnormiert(gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);
    return gebietDatenNormierer.getNormierteGueltigeStimmen(gueltigeUnnormiert, ungueltigeUnnormiert);
  }

  private Map<Stimmenart, Double> getGueltigeHochgerechnetUnnormiert(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getSystemGruppeHochgerechnetUnnormiert (SystemGruppe.GUELTIGE, gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);
  }

  private Map<Stimmenart, Double> getUngueltigeHochgerechnetUnnormiert(GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getSystemGruppeHochgerechnetUnnormiert (SystemGruppe.UNGUELTIGE, gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);
  }

  private Map<Stimmenart, Double> getSystemGruppeHochgerechnetUnnormiert(SystemGruppe systemGruppe, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    Map<Stimmenart, Double> result = new EnumMap<>(Stimmenart.class);
    koordinatenInformationen.getAlleStimmenartenSortiert()
        .forEach(stimmenart -> result.put(stimmenart,
            getSystemGruppeStimmeHochgerechnet(systemGruppe, gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen, stimmenart)));
    return result;
  }

  private Double getSystemGruppeStimmeHochgerechnet(SystemGruppe systemGruppe, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen, Stimmenart stimmenart) {
    return getHochgerechnetenWert(getSystemGruppeValueGetter(systemGruppe, stimmenart), gebietDatenVorperiode, gewichteteSkalierungen,
        koordinatenInformationen.getGruppeInVorperiodeVorhandenChecker().isSystemGruppeInVorperiodeVorhanden(systemGruppe, stimmenart));
  }

  private Function<GebietDaten<Double>, Double> getParteistimmenValueGetter(PolitischeGruppe politischeGruppe, Stimmenart stimmenart) {
    return gebietDaten -> getParteiStimme(gebietDaten, politischeGruppe, stimmenart);
  }

  private Double getParteiStimme(GebietDaten<Double> gebietDaten, PolitischeGruppe politischeGruppe, Stimmenart stimmenart) {
    ParteiStimmen<Double> parteiStimme = gebietDaten.getParteiStimmen().get(politischeGruppe);
    if (parteiStimme != null) {
      return parteiStimme.getStimmen(stimmenart);
    }
    return null;
  }

  private Function<GebietDaten<Double>, Double> getSystemGruppeValueGetter(SystemGruppe systemGruppe, Stimmenart stimmenart) {
    if (systemGruppe.equals(SystemGruppe.GUELTIGE)) {
      return gebietDaten -> gebietDaten.getGueltige().get(stimmenart);
    } else if (systemGruppe.equals(SystemGruppe.UNGUELTIGE)) {
      return gebietDaten -> gebietDaten.getUngueltige().get(stimmenart);
    }
    return gebietDaten -> null;
  }



  private Double getHochgerechnetenWert(Function<GebietDaten<Double>, Double> valueGetter, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, boolean isInVorperiodeVorhanden) {
    Double wertVorperiode = gebietDatenVorperiode != null? valueGetter.apply(gebietDatenVorperiode) : null;
    Double gewichteteSkalierung = getReducedGewichteteSkalierung(valueGetter, gewichteteSkalierungen);
    return getSkaliertenWert (wertVorperiode, gewichteteSkalierung, isInVorperiodeVorhanden);
  }

  private Double getReducedGewichteteSkalierung(Function<GebietDaten<Double>, Double> valueGetter, List<GewichteteSkalierung> gewichteteSkalierungen) {
    List<GewichteterWert> gewichteteSkalierungswerte = getGewichteteSkalierungswerte(valueGetter, gewichteteSkalierungen, this::isSkalierungVorhanden);

    double summeDerGewichte = getSummeDerGewichte(gewichteteSkalierungswerte);
    if (summeDerGewichte == 0) {
      return null;
    }

    return gewichteteSkalierungswerte.stream().mapToDouble(GewichteterWert::getGewichtetenWertNonNull).sum() / summeDerGewichte;
  }

  private Double getSkaliertenWert(Double wertVorperiode, Double gewichteteSkalierung, boolean isInVorperiodeVorhanden) {
    if (gewichteteSkalierung == null) {
      return wertVorperiode;
    }
    Double result;
    if (isInVorperiodeVorhanden) {
      result = getSkaliertenWertWennWertInVorperiodeVorhandenSeinMuss(wertVorperiode, gewichteteSkalierung);
    } else {
      result = getSkaliertenWertWennWertInVorperiodeVorhandenNichtExisteierte(gewichteteSkalierung);
    }

    return trim(result);
  }

  private Double getSkaliertenWertWennWertInVorperiodeVorhandenNichtExisteierte(Double gewichteteSkalierung) {
    return gewichteteSkalierung;
  }

  private Double getSkaliertenWertWennWertInVorperiodeVorhandenSeinMuss(Double wertVorperiode, Double gewichteteSkalierung) {
    if (wertVorperiode != null && wertVorperiode > SEHR_KLEINE_STIMMENANTEIL) {
      return wertVorperiode * (1 + gewichteteSkalierung);
    }
    return getSkaliertenWertWennWertInVorperiodeVorhandenSeinMussAberFehltAmGebiet(gewichteteSkalierung);
  }

  private Double getSkaliertenWertWennWertInVorperiodeVorhandenSeinMussAberFehltAmGebiet(Double gewichteteSkalierung) {
    return gewichteteSkalierung / FAKTOR_ZUR_REDUZIERUNG_DER_SKALIERUNG_FUER_IN_VORPERIODE_SEHR_KLEINE_ZAHLEN;
  }

  private Double getSummeDerGewichte(List<GewichteterWert> gewichteteSkalierungswerte) {
    return gewichteteSkalierungswerte.stream().mapToDouble(GewichteterWert::getGewichtNonNull).sum();
  }

  private boolean isSkalierungVorhanden(GewichteterWert gewichteterWert) {
    return gewichteterWert.isGewichteterWertBerechenbar();
  }

  private List<GewichteterWert> getGewichteteSkalierungswerte(Function<GebietDaten<Double>, Double> valueGetter, List<GewichteteSkalierung> gewichteteSkalierungen, Predicate<GewichteterWert> gewichteterWerteFilter) {
    List<GewichteterWert> result = new ArrayList<>();
    gewichteteSkalierungen.forEach( gewichteteSkalierung -> {
          if (gewichteteSkalierung != null) {
            result.add(new GewichteterWert(valueGetter.apply(gewichteteSkalierung.getSkalierung()), gewichteteSkalierung.getGewicht()));
          }
        }
    );
    return result.stream().filter(gewichteterWerteFilter).collect(Collectors.toList());
  }

  private Double trim(Double x) {
    if (x == null || (x <= 1 && x >= 0)) {
      return x;
    }
    if (x < 0) {
      return 0D;
    }
    return 1D;
  }

  public Map<Gruppe, ReferendumFrage<Double>> getReferendumFragenHochgerechnet(GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    Map<Gruppe, ReferendumFrage<Double>> resultUnnormiert = new HashMap<>();
    if (gebietDatenAktuellAlsAnzahlen == null) {
      return resultUnnormiert;
    }

    gebietDatenAktuellAlsAnzahlen.getReferendumFragen()
        .forEach((referendumVorlage, referendumFrage) -> resultUnnormiert.put(referendumVorlage, getHochgerechneteReferendumStimmenEinerFrage(referendumFrage, gebietDatenVorperiode, gebietDatenAktuellAlsAnzahlen, gewichteteSkalierungen, koordinatenInformationen)));

    return gebietDatenNormierer.getReferendumFragenStimmenAnteileNormiert(resultUnnormiert);
  }

  private ReferendumFrage<Double> getHochgerechneteReferendumStimmenEinerFrage(ReferendumFrage<Long> referendumFrage, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Long> gebietDatenAktuellAlsAnzahlen, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    if (referendumFrage == null) {
      return null;
    }

    Double gueltige = getGueltigeReferendumOptionStimmeHochgerechnetUnnormiert(referendumFrage.getReferendumVorlage(), gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);
    Double ungueltige = getUngueltigeReferendumOptionStimmeHochgerechnetUnnormiert(referendumFrage.getReferendumVorlage(), gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen);

    Map<ReferendumOption, Double> stimmenPerReferendumOption = new HashMap<>();

    referendumFrage.getStimmenPerReferendumOption().keySet()
        .forEach(referendumOption -> stimmenPerReferendumOption
            .put(referendumOption, getHochgerechneteReferendumOptionStimme(referendumFrage.getReferendumVorlage(), referendumOption, gebietDatenVorperiode, gewichteteSkalierungen, koordinatenInformationen)));

    return new ReferendumFrage<>(gueltige, ungueltige, referendumFrage.getReferendumVorlage(), stimmenPerReferendumOption);
  }

  private Double getHochgerechneteReferendumOptionStimme(Gruppe referendumVorlage, ReferendumOption referendumOption, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getHochgerechnetenWert(getReferendumOptionStimmenValueGetter(referendumVorlage, referendumOption),
        gebietDatenVorperiode,
        gewichteteSkalierungen,
        false);
  }

  private Function<GebietDaten<Double>, Double> getReferendumOptionStimmenValueGetter(Gruppe referendumVorlage, ReferendumOption referendumOption) {
    return gebietDaten -> getReferendumOptionStimme(gebietDaten, referendumVorlage, referendumOption);
  }

  private Double getReferendumOptionStimme(GebietDaten<Double> gebietDaten, Gruppe referendumVorlage, ReferendumOption referendumOption) {
    ReferendumFrage<Double> referendumFrage = gebietDaten.getReferendumFragen().get(referendumVorlage);
    if (referendumFrage == null) {
      return null;
    }
    return referendumFrage.getStimmenPerReferendumOption().get(referendumOption);
  }

  private Double getGueltigeReferendumOptionStimmeHochgerechnetUnnormiert(Gruppe referendumVorlage, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getHochgerechnetenWert(getGueltigeReferendumOptionStimmeValueGetter(referendumVorlage), gebietDatenVorperiode, gewichteteSkalierungen,
        false);
  }

  private Function<GebietDaten<Double>, Double> getGueltigeReferendumOptionStimmeValueGetter(Gruppe referendumVorlage) {
    return gebietDaten -> getGueltigeReferendumOptionStimme(gebietDaten, referendumVorlage);
  }

  private Double getGueltigeReferendumOptionStimme(GebietDaten<Double> gebietDaten, Gruppe referendumVorlage) {
    ReferendumFrage<Double> referendumFrage = gebietDaten.getReferendumFragen().get(referendumVorlage);
    if (referendumFrage == null) {
      return null;
    }
    return referendumFrage.getGueltige();
  }

  private Double getUngueltigeReferendumOptionStimmeHochgerechnetUnnormiert(Gruppe referendumVorlage, GebietDaten<Double> gebietDatenVorperiode, List<GewichteteSkalierung> gewichteteSkalierungen, KoordinatenInformationen koordinatenInformationen) {
    return getHochgerechnetenWert(getUngueltigeReferendumOptionStimmeValueGetter(referendumVorlage), gebietDatenVorperiode, gewichteteSkalierungen,
        false);
  }

  private Function<GebietDaten<Double>, Double> getUngueltigeReferendumOptionStimmeValueGetter(Gruppe referendumVorlage) {
    return gebietDaten -> getUngueltigeReferendumOptionStimme(gebietDaten, referendumVorlage);
  }

  private Double getUngueltigeReferendumOptionStimme(GebietDaten<Double> gebietDaten, Gruppe referendumVorlage) {
    ReferendumFrage<Double> referendumFrage = gebietDaten.getReferendumFragen().get(referendumVorlage);
    if (referendumFrage == null) {
      return null;
    }
    return referendumFrage.getUngueltige();
  }
}
