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

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.hochrechnung.entity.GebietDatenWahlbeteiligung;
import de.ivu.elect.business.hochrechnung.entity.ReferendumFrage;
import de.ivu.elect.business.hochrechnung.entity.ZufaelligeReferendumFrageStimmenanteile;
import de.ivu.elect.business.hochrechnung.entity.ZufaelligeReferendumStimmenAnteileNormalverteilungKonfiguration;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahlvorschlag.control.ReferendumOptionByPositionComparator;
import de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption;


import static de.ivu.elect.business.hochrechnung.control.StimmenHochrechnerHilfsfunktionen.notNull;

public class ZufaelligeReferendumStimmenanteileNormalverteilungGenerator extends ZufaelligeReferendumFrageStimmenanteile {
  ZufaelligeReferendumStimmenAnteileNormalverteilungKonfiguration konfiguration;

  public ZufaelligeReferendumStimmenanteileNormalverteilungGenerator(ZufaelligeReferendumStimmenAnteileNormalverteilungKonfiguration konfiguration) {
    this.konfiguration = konfiguration;
  }

  @Override
  public Map<Gruppe, ReferendumFrage<Double>> generateZufaelligeReferendumFrageStimmenanteileUndAendereWahlbeteiligungDassQuorumErfuelltIst(Gebiet gebiet, Map<Gruppe, List<ReferendumOption>> referendumOptionenPerVorlage, GebietDatenWahlbeteiligung wahlbeteiligung) {
    Map<Gruppe, ReferendumFrage<Double>> resultUnnormiert = new HashMap<>();
    referendumOptionenPerVorlage.forEach(((referendumVorlage, referendumOptionen) ->
        resultUnnormiert.put(referendumVorlage, generateZufaelligeReferendumFrageStimmenanteile(referendumVorlage, referendumOptionen))));
    ueberpruefeQuorumUndKorrigiere(gebiet, resultUnnormiert, wahlbeteiligung);
    GebietDatenNormierer<Double> gebietDatenNormierer = new GebietDatenNormierer<>();
    return gebietDatenNormierer.getReferendumFragenStimmenAnteileNormiert(resultUnnormiert);
  }

  private void ueberpruefeQuorumUndKorrigiere(Gebiet gebiet, Map<Gruppe, ReferendumFrage<Double>> resultUnnormiert, GebietDatenWahlbeteiligung wahlbeteiligung) {
    Wahl wahl = gebiet.getWahl();
    if (wahl == null) {
      return;
    }
    BigDecimal quorumProzenthuerde = wahl.getQuorumProzenthuerde();
    if (quorumProzenthuerde == null) {
      return;
    }
    double quorum = quorumProzenthuerde.doubleValue() / 100;
    if (quorum <= 0) {
      return;
    }
    double mindestVerhaeltnisWahlbeteiligungZuQuorum = konfiguration.getMindestVerhaeltnisWahlbeteiligungZuQuorum();
    double mindestGewuenschteWahlbeteiligungGesamt = trimAnteil(mindestVerhaeltnisWahlbeteiligungZuQuorum * quorum, 0);
    ueberpruefeWahlbeteiligungGroesserAlsQuorumUndKorrigiere(mindestGewuenschteWahlbeteiligungGesamt, wahlbeteiligung);

    ueberpruefeJaWahlerErfuelltQuorumUndKorrigiere(quorum, resultUnnormiert, wahlbeteiligung);
  }

  private void ueberpruefeJaWahlerErfuelltQuorumUndKorrigiere(double quorum, Map<Gruppe, ReferendumFrage<Double>> referendumFragen, GebietDatenWahlbeteiligung wahlbeteiligung) {
    Double wahlbeteiligungGesamt = wahlbeteiligung.getGesamtNotNull();
    referendumFragen.values().forEach(referendumFrage -> ueberpruefeJaWahlerErfuelltQuorumUndKorrigiere(quorum, referendumFrage, wahlbeteiligungGesamt));
  }

  private void ueberpruefeJaWahlerErfuelltQuorumUndKorrigiere(double quorum, ReferendumFrage<Double> referendumFrage, Double wahlbeteiligungGesamt) {
    if (referendumFrage == null) {
      return;
    }
    Double mindestJaStimmenanteil = getMindestJaStimmenanteil(quorum, wahlbeteiligungGesamt);
    if (mindestJaStimmenanteil == null || mindestJaStimmenanteil <= 0) {
      return;
    }

    Map<ReferendumOption, Double> stimmenPerReferendumOption = referendumFrage.getStimmenPerReferendumOption();
    ReferendumOption jaReferendumOption = getReferendumOption("ja", new ArrayList<>(stimmenPerReferendumOption.keySet()));
    if (jaReferendumOption == null) {
      // Bei mehr als zwei Optionen gilt das Quorum nicht mehr
      return;
    }

    double sumStimmen = stimmenPerReferendumOption.values().stream().mapToDouble(StimmenHochrechnerHilfsfunktionen::notNull).sum();
    double jaStimmen = notNull(stimmenPerReferendumOption.get(jaReferendumOption));

    if (jaStimmen / sumStimmen < mindestJaStimmenanteil) {
      // let neueJaStimmen = jaStimmen + (-jaStimmen + mindestJaStimmenanteil * sumStimmen)/(1 - mindestJaStimmenanteil)
      // then mindestJaStimmenanteil == neueJaStimmen/(sumStimmen + neueJaStimmen - jaStimmen) // neueSumStimmen = (sumStimmen + neueJaStimmen - jaStimmen)
      double factorVonSumStimmen = (mindestJaStimmenanteil + 1) / 2;
      // Jeder factorVonSumStimmen >= mindestJaStimmenanteil erfuellt: neueJaStimmenanteil nach Normierung >= mindestJaStimmenanteil
      // Zum Checken kann die Ungleichung geloest werden:
      // neueJaStimmen / neueSumStimmen >= mindestJaStimmenanteil
      double neueJaStimmen = jaStimmen + (-jaStimmen + factorVonSumStimmen * sumStimmen)/(1 - mindestJaStimmenanteil);
      stimmenPerReferendumOption.put(jaReferendumOption, neueJaStimmen);
    }
  }

  private Double getMindestJaStimmenanteil(double quorum, Double wahlbeteiligungGesamt) {
    if (wahlbeteiligungGesamt == null || wahlbeteiligungGesamt <= 0) {
      return null;
    }
    return quorum / wahlbeteiligungGesamt;
  }

  private ReferendumOption getReferendumOption(String name, List<ReferendumOption> referendumOptionen) {
    // from de.ivu.elect.presentation.ergebnis.dokumente.wahlbuchreferendum.ReferendumOptionJaNeinFinder.getReferendumOption(java.lang.String, java.util.List<de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption>)
    ReferendumOption ja = referendumOptionen.stream()
        .filter(option -> option.getKurzbezeichnung() != null && option.getKurzbezeichnung().toLowerCase().contains(name)).findFirst().orElse(null);
    if (ja == null && !referendumOptionen.isEmpty() && referendumOptionen.size() <= 2) {
      referendumOptionen.sort(new ReferendumOptionByPositionComparator());
      ja = referendumOptionen.iterator().next();
    }
    if (ja == null) {
      String kurzbezeichnungen = referendumOptionen.stream().map(ReferendumOption::getKurzbezeichnung)
          .collect(Collectors.joining(", "));
      System.out.println("ReferendumOption " + name + " nicht gefunden. Vorhandene Optionen" + kurzbezeichnungen);
    }
    return ja;
  }

  private void ueberpruefeWahlbeteiligungGroesserAlsQuorumUndKorrigiere(double mindestGewuenschteWahlbeteiligungGesamt, GebietDatenWahlbeteiligung aktuelleWahlbeteiligung) {
    Double wahlbeteiligungGesamt = aktuelleWahlbeteiligung.getGesamtNotNull();
    if (wahlbeteiligungGesamt == null || wahlbeteiligungGesamt == 0) {
      return;
    }
    if (wahlbeteiligungGesamt <= mindestGewuenschteWahlbeteiligungGesamt) {
      behandeleWahlbeteiligungGesamtKleinerAlsMindsetGewuenschteWahlbeteiligungGesamt(mindestGewuenschteWahlbeteiligungGesamt, aktuelleWahlbeteiligung);
    }
  }

  private void behandeleWahlbeteiligungGesamtKleinerAlsMindsetGewuenschteWahlbeteiligungGesamt(double mindestGewuenschteWahlbeteiligungGesamt, GebietDatenWahlbeteiligung aktuelleWahlbeteiligung) {
    double multiplicationsfaktor = mindestGewuenschteWahlbeteiligungGesamt / aktuelleWahlbeteiligung.getGesamtNotNull();
    aktuelleWahlbeteiligung.getStimmenarten().forEach(stimmenart -> {
      erhoeheWahlbeteiligung (multiplicationsfaktor, stimmenart, aktuelleWahlbeteiligung);
    });
  }

  private void erhoeheWahlbeteiligung(double multiplicationsfaktor, Stimmenart stimmenart, GebietDatenWahlbeteiligung aktuelleWahlbeteiligung) {
    Double wahlbeteiligung = aktuelleWahlbeteiligung.getAnzahl(stimmenart);
    if (wahlbeteiligung != null) {
      double erhoehteWahlbeteiligung = trimAnteil(wahlbeteiligung * multiplicationsfaktor, 0);
      aktuelleWahlbeteiligung.setAnzahl(stimmenart, erhoehteWahlbeteiligung);
    }
  }

  private ReferendumFrage<Double> generateZufaelligeReferendumFrageStimmenanteile(Gruppe referendumVorlage, List<ReferendumOption> referendumOptionen) {
    double zufaelligeGueltigeStimmenanteil = getZufaelligeGueltigeStimmenanteil();
    double zufaelligeUngueltigeStimmenanteil = 1 - zufaelligeGueltigeStimmenanteil;

    Map<ReferendumOption, Double> stimmenPerReferendumOption = new HashMap<>();
    referendumOptionen.forEach(referendumOption -> stimmenPerReferendumOption.put(referendumOption, getZufaelligeReferendumOptionStimmenanteil(referendumOption)));
    return new ReferendumFrage<>(zufaelligeGueltigeStimmenanteil, zufaelligeUngueltigeStimmenanteil, referendumVorlage, stimmenPerReferendumOption);
  }

  private Double getZufaelligeReferendumOptionStimmenanteil(ReferendumOption referendumOption) {
    double sample = konfiguration.getNormalDistributionPerReferendumOption(referendumOption).sample();
    return trimAnteil(sample, 0);
  }

  private double getZufaelligeGueltigeStimmenanteil () {
    return trimAnteilGueltige(konfiguration.getNormalDistributionFuerGultigeStimmenanteil().sample());
  }

  private double trimAnteilGueltige(double anteil) {
    double gueltigeStimmenanteilMin = konfiguration.getGueltigeStimmenanteilMin();
    return trimAnteil(anteil, gueltigeStimmenanteilMin);
  }

  private double trimAnteil(double anteil, double min) {
    if (anteil >= min && anteil <= 1) {
      return anteil;
    }
    return anteil > 1? 1 : min;
  }
}
