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

import java.util.*;
import java.util.stream.Collectors;

import org.apache.commons.math3.random.RandomGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppe;
import de.ivu.elect.business.hochrechnung.boundary.Hochrechnungsalgorithm;
import de.ivu.elect.business.hochrechnung.entity.AbstandGetter;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.GebietDatenErgebnisSkalierung;
import de.ivu.elect.business.hochrechnung.entity.GewichteteSkalierung;
import de.ivu.elect.business.hochrechnung.entity.GewichtetesObject;
import de.ivu.elect.business.hochrechnung.entity.HochgerechneteGebietDaten;
import de.ivu.elect.business.hochrechnung.entity.HochrechnenMitDistanceWeightedKNearestNeighborsKonfiguration;
import de.ivu.elect.business.hochrechnung.entity.HochrechneralgorithmusAusfuehrbarkeitStatus;
import de.ivu.elect.business.hochrechnung.entity.KoordinatenInformationen;
import de.ivu.elect.business.hochrechnung.entity.StimmenHochrechnerEingabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerAusgabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerGebietAusgabedaten;
import de.ivu.elect.business.hochrechnung.entity.ZufaelligerStimmenHochrechnerKonfiguration;


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

public class HochrechnenMitDistanceWeightedKNearestNeighbors implements Hochrechnungsalgorithm {
  private static final Logger LOG = LoggerFactory.getLogger(HochrechnenMitDistanceWeightedKNearestNeighbors.class);

  private final HochrechnenMitDistanceWeightedKNearestNeighborsKonfiguration konfiguration;

  public HochrechnenMitDistanceWeightedKNearestNeighbors(HochrechnenMitDistanceWeightedKNearestNeighborsKonfiguration konfiguration) {
    this.konfiguration = konfiguration;
  }

  @Override
  public StimmenhochrechnerAusgabedaten ermittleHochgerechneteStimmen (StimmenHochrechnerEingabedaten<Long> eingabedaten) {
    HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.AUSFUEHRBAR;

    String bemerkung = null;
    if (konfiguration.getAbstandartZwischenGebieten().isEntferneGebieteOhneVorperiodenWerte()) {
      bemerkung = entferneGebieteOhneVorperiodenWerte(eingabedaten); // ELECT-14438
    }

    log("Hochrechner: Zufaellige Erzeugung anzahl Wahlberechtigte Fuer aktuelle Wahl wo sie nicht Vorhanden sind");
    setZufaelligeAnzahlWahlberechtigteWoNichtVorhanden(eingabedaten, getZufaelligeAnzahlWahlberechtigteGenerator());
    log("Hochrechner: Zufaellige Erzeugung anzahl Wahlberechtigte Fuer aktuelle Wahl wo sie nicht Vorhanden sind - done");

    log("Hochrechner: Ermittle Koordinaten-Informationen");
    KoordinatenInformationen koordinatenInformationen = new KoordinatenInformationenVonEingabedatenErzeuger(eingabedaten);
    log("Hochrechner: Ermittle Koordinaten-Informationen - done");

    AbstandGetter<Double> abstandGetter = konfiguration.getAbstandGetter();

    StimmenhochrechnerAusgabedaten result = berechneStimmenhochrechnerAusgabedaten(eingabedaten, abstandGetter, ausfuehrbarkeitStatus, koordinatenInformationen);

    korrigiereHochgerechneteStimmenFuerNeueGebieteWennNotwendig(eingabedaten, result, koordinatenInformationen);

    result.setBemerkung(bemerkung);

    return result;
  }

  private String entferneGebieteOhneVorperiodenWerte(StimmenHochrechnerEingabedaten<Long> eingabedaten) {
    Map<Gebiet, GebietDaten<Long>> vorperiode = eingabedaten.getVorperiode();
    Map<Gebiet, GebietDaten<Long>> aktuelleWahl = eingabedaten.getAktuelleWahl();
    long anzahlGebieteMitVorperiode = 0;
    long anzahlGebieteMitAktuellemErgebnis = 0;
    List<Gebiet> gebieteNichtZuBeruecksichtigen = new ArrayList<>();
    for (GebietDaten<Long> gebietDatenAktuell : aktuelleWahl.values()) {
      Gebiet gebiet = gebietDatenAktuell.getGebiet();
      if (vorperiode.containsKey(gebiet) && vorperiode.get(gebiet).isErgebnisVorhanden()) {
        if (gebiet.isErfassungsgebiet()) {
          anzahlGebieteMitVorperiode++;
          if (gebietDatenAktuell.isErgebnisVorhanden()) {
            anzahlGebieteMitAktuellemErgebnis++;
          }
        }
      } else {
        gebieteNichtZuBeruecksichtigen.add(gebiet);
      }
    }
    for (Gebiet gebiet : gebieteNichtZuBeruecksichtigen) {
      aktuelleWahl.remove(gebiet);
      vorperiode.remove(gebiet);
    }
    return String.format("Teilweise Hochrechnung - Berücksichtigte Ergebniseingänge: %s/%s", anzahlGebieteMitAktuellemErgebnis, anzahlGebieteMitVorperiode);
  }

  private StimmenhochrechnerAusgabedaten berechneStimmenhochrechnerAusgabedaten(StimmenHochrechnerEingabedaten<Long> eingabedaten, AbstandGetter<Double> abstandGetter, HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus, KoordinatenInformationen koordinatenInformationen) {
    log("Berechne Abstaende zwischen Gebieten");

    if (!isAbstandGetterDefiniert(koordinatenInformationen, abstandGetter)) {
      ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSGEWAEHLTES_KRITERIUMS_FUER_AEHNLICHKEITSGRAD_ZWISCHEN_GEBIETEN;
      return new StimmenhochrechnerAusgabedaten(ausfuehrbarkeitStatus);
    }
    log("Berechne Abstaende zwischen Gebieten - Done");

    StimmenhochrechnerAusgabedaten result = new StimmenhochrechnerAusgabedaten(ausfuehrbarkeitStatus);
    log("Initialize Ausgabedaten vom Hochrechner");
    result.initializeAusgabedaten(eingabedaten, koordinatenInformationen);
    log("Initialize Ausgabedaten vom Hochrechner - done");

    log("Berechne Trendwende pro Gebiet");
    Map<Gebiet, GebietDaten<Double>> skalierungProGebiet = berechneSkalierungProGebiet(result);
    log("Berechne Trendwende pro Gebiet - Done");

    log("Set Abstaende, wo Skalierung vorhanden ist");
    setAbstaendeZwischenGebietenWoSkalierungVorhandenIstInAusgabeDaten (result, skalierungProGebiet, koordinatenInformationen, abstandGetter);
    log("Set Abstaende, wo Skalierung vorhanden ist - Done");

    log("Berechne hochgerechnete Stimmen");
    berechneUndSetStimmenHochNachBerechnungDerAbstaende(result, skalierungProGebiet);
    log("Berechne hochgerechnete Stimmen - Done");

    return result;
  }

  private void korrigiereHochgerechneteStimmenFuerNeueGebieteWennNotwendig(StimmenHochrechnerEingabedaten<Long> eingabedaten, StimmenhochrechnerAusgabedaten result, KoordinatenInformationen koordinatenInformationen) {
    if (!result.getAusfuehrbarkeitStatus().isAlgorithmusAusfuehrbar()) {
      // Wenn der Algorithmus nicht ausfuehrbar ist, hat es kein Sinn, etwas zu unternehmen.
      return;
    }
    Set<Gebiet> neueGebiete = getGebieteOhneVorperiode(eingabedaten);
    if (neueGebiete.isEmpty()) {
      return;
    }
    if (neueGebiete.size() == eingabedaten.getAktuelleWahl().keySet().size()) { // Wenn alle Gebiete neu sind.
      return;
    }

    StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedatenOhneVorperiode = berechneHochgerechneteStimmenOhneVorperiode(getStimmenHochrechnerEingabedatenOhneVorperiode(eingabedaten),
        getKoordinatenInformationenOhneVorperiode(koordinatenInformationen));

    if (!stimmenhochrechnerAusgabedatenOhneVorperiode.getAusfuehrbarkeitStatus().isAlgorithmusAusfuehrbar()) {
      return;
    }

    neueGebiete.forEach(neuesGebiet -> {
      StimmenhochrechnerGebietAusgabedaten neueAusgabedaten = stimmenhochrechnerAusgabedatenOhneVorperiode.getAusgabedatenVomGebiet(neuesGebiet);
      result.getAusgabedatenProGebiet().put(neuesGebiet, neueAusgabedaten);
    });
  }

  private KoordinatenInformationen getKoordinatenInformationenOhneVorperiode(KoordinatenInformationen koordinatenInformationen) {
    return new KoordinatenInformationen() {
      @Override
      public List<Gebiet> getAlleGebiete() {
        return koordinatenInformationen.getAlleGebiete();
      }

      @Override
      public List<SystemGruppe> getAlleSystemGruppenSortiert() {
        return koordinatenInformationen.getAlleSystemGruppenSortiert();
      }

      @Override
      public List<PolitischeGruppe> getAllePolitischeGruppenSortiert() {
        return koordinatenInformationen.getAllePolitischeGruppenSortiert();
      }

      @Override
      public List<Stimmenart> getAlleStimmenartenSortiert() {
        return koordinatenInformationen.getAlleStimmenartenSortiert();
      }

      @Override
      public GruppeInVorperiodeVorhandenChecker getGruppeInVorperiodeVorhandenChecker() {
        return getGruppeInVorperiodeVorhandenCheckerReturnsAlwaysFalse();
      }

      private GruppeInVorperiodeVorhandenChecker getGruppeInVorperiodeVorhandenCheckerReturnsAlwaysFalse() {
        return new GruppeInVorperiodeVorhandenChecker() {
          @Override
          public boolean isSystemGruppeInVorperiodeVorhanden(SystemGruppe systemGruppe, Stimmenart stimmenart) {
            return false;
          }

          @Override
          public boolean isPolitischeGruppeInVorperiodeVorhanden(PolitischeGruppe politischeGruppe, Stimmenart stimmenart) {
            return false;
          }
        };
      }
    };
  }

  private StimmenhochrechnerAusgabedaten berechneHochgerechneteStimmenOhneVorperiode(StimmenHochrechnerEingabedaten<Long> eingabedatenOhneVorperiode, KoordinatenInformationen koordinatenInformationen) {
    HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.AUSFUEHRBAR;

    AbstandGetter<Double> abstandGetter = new AbstandImGebietsBaumGetter();

    StimmenhochrechnerAusgabedaten result = berechneStimmenhochrechnerAusgabedaten(eingabedatenOhneVorperiode, abstandGetter, ausfuehrbarkeitStatus, koordinatenInformationen);

    return result;
  }

  private StimmenHochrechnerEingabedaten<Long> getStimmenHochrechnerEingabedatenOhneVorperiode(StimmenHochrechnerEingabedaten<Long> eingabedaten) {
    HashMap<Gebiet, GebietDaten<Long>> leereVorperiode = new HashMap<>();
    eingabedaten.getAktuelleWahl().keySet().forEach(gebiet -> leereVorperiode.put(gebiet, getEmptyGebietDaten(gebiet)));

    return new StimmenHochrechnerEingabedaten<Long>() {
      @Override
      public Map<Gebiet, GebietDaten<Long>> getVorperiode() {
        return leereVorperiode;
      }

      @Override
      public Map<Gebiet, GebietDaten<Long>> getAktuelleWahl() {
        return eingabedaten.getAktuelleWahl();
      }
    };
  }

  private Set<Gebiet> getGebieteOhneVorperiode(StimmenHochrechnerEingabedaten<Long> eingabedaten) {
    Map<Gebiet, GebietDaten<Long>> vorperiode = eingabedaten.getVorperiode();
    return vorperiode.keySet().stream().filter(gebiet -> {
      GebietDaten<Long> gebietDaten = vorperiode.get(gebiet);
      return gebietDaten == null || !gebietDaten.isErgebnisVorhanden();
    }).collect(Collectors.toSet());
  }

  private ZufaelligeAnzahlWahlberechtigteGenerator getZufaelligeAnzahlWahlberechtigteGenerator() {
    ZufaelligerStimmenHochrechnerKonfiguration zufaelligerStimmenHochrechnerKonfiguration = new ZufaelligerStimmenHochrechnerKonfiguration();
    RandomGenerator randomGenerator = zufaelligerStimmenHochrechnerKonfiguration.getRandomGenerator();
    LOG.info("Random generator used by Hochrechnungsalgorithmus: " + randomGenerator.getClass());
    LOG.info("Seed used by random generator = " + zufaelligerStimmenHochrechnerKonfiguration.getSeedFuerRandomGenerator());
    return zufaelligerStimmenHochrechnerKonfiguration.getAnzahlWahlberechtigteGenerator();
  }

  private boolean isAbstandGetterDefiniert(KoordinatenInformationen koordinatenInformationen, AbstandGetter<Double> abstandGetter) {
    if (abstandGetter == null) {
      return false;
    }
    abstandGetter.setIsDefiniert(koordinatenInformationen);
    return abstandGetter.isDefiniert();
  }

  private void log(String msg) {
    LOG.debug(msg);
  }

  @Override
  public HochrechnenMitDistanceWeightedKNearestNeighborsKonfiguration getHochrechnungsalgorithmusKonfiguration() {
    return konfiguration;
  }

  private void berechneUndSetStimmenHochNachBerechnungDerAbstaende(StimmenhochrechnerAusgabedaten result, Map<Gebiet, GebietDaten<Double>> skalierungProGebiet) {
    result.getAusgabedatenProGebiet().values().forEach(
        gebietAusgabedaten -> {
          GebietDaten<Double> hochgerechnet = berechneStimmenHochFuerGebietAnhandTrendWende (gebietAusgabedaten, skalierungProGebiet);
          gebietAusgabedaten.setHochgerechnetAnteile(hochgerechnet);
        }
    );
  }

  private GebietDaten<Double> berechneStimmenHochFuerGebietAnhandTrendWende(StimmenhochrechnerGebietAusgabedaten gebietAusgabedaten, Map<Gebiet, GebietDaten<Double>> skalierungProGebiet) {
    GebietDaten<Double> gebietDatenVorperiode = gebietAusgabedaten.getVorperiodeAnteile();
    if (gebietDatenVorperiode == null) {
      return null;
    }
    List<GewichteteSkalierung> gewichteteSkalierungen = getGewichteteSkalierungenVonKNearestNeighbors(gebietAusgabedaten.getAbstaendeZuAnderenGebieten(), skalierungProGebiet);
    return new HochgerechneteGebietDaten(gebietDatenVorperiode, gebietAusgabedaten.getAktuellAnzahlen(), gewichteteSkalierungen, gebietAusgabedaten.getKoordinatenInformationen());
  }

  private List<GewichteteSkalierung> getGewichteteSkalierungenVonKNearestNeighbors(List<GewichtetesObject<Gebiet>> abstaendeZuAnderenGebieten, Map<Gebiet, GebietDaten<Double>> skalierungProGebiet) {
    DistanceWeightedKNearestNeighborsNormierer<Gebiet> dwknnNormierer = new DistanceWeightedKNearestNeighborsNormierer<>();
    List<GewichtetesObject<Gebiet>> weightedDistances = dwknnNormierer.getDistanceWeightedKNearestNeighbors(konfiguration.getK(), abstaendeZuAnderenGebieten);

    return weightedDistances.stream().map(gebietUndGewichteterAbstand ->
        new GewichteteSkalierung(skalierungProGebiet.get(gebietUndGewichteterAbstand.getObject()),
            gebietUndGewichteterAbstand.getGewicht())).collect(Collectors.toList());
  }

  private void setAbstaendeZwischenGebietenWoSkalierungVorhandenIstInAusgabeDaten(StimmenhochrechnerAusgabedaten ausgabedaten, Map<Gebiet, GebietDaten<Double>> skalierungProGebiet, KoordinatenInformationen koordinatenInformationen, AbstandGetter<Double> abstandGetter) {
    List<GebietDaten<Double>> alleGebietDatenVorperiode = ausgabedaten.getAusgabedatenProGebiet().values().stream().map(StimmenhochrechnerGebietAusgabedaten::getVorperiodeAnteile).collect(
        Collectors.toList());
    DistancesZwischenGebieten<Double> distances = new DistancesZwischenGebieten<>();

    if (abstandGetter == null || !abstandGetter.isDefiniert()) {
      return;
    }
    for (StimmenhochrechnerGebietAusgabedaten gebietAusgabedaten: ausgabedaten.getAusgabedatenProGebiet().values()) {
      List<GewichtetesObject<Gebiet>> abstaendeZuAnderen = distances.getAbstaendenVonGebietZuAllenAnderen(gebietAusgabedaten.getVorperiodeAnteile(), alleGebietDatenVorperiode, abstandGetter, koordinatenInformationen);
      List<GewichtetesObject<Gebiet>> abstaendeZuGebietenWoSkalierungVorhanden = abstaendeZuAnderen.stream().filter(abstand -> isSkalierungVorhanden(abstand.getObject(), skalierungProGebiet))
          .collect(Collectors.toList());
      gebietAusgabedaten.setAbstaendeZuAnderenGebieten(abstaendeZuGebietenWoSkalierungVorhanden);
    }
  }

  private boolean isSkalierungVorhanden(Gebiet gebiet, Map<Gebiet, GebietDaten<Double>> skalierungProGebiet) {
    GebietDaten<Double> skalierung = skalierungProGebiet.get(gebiet);
    return skalierung != null && skalierung.isErgebnisVorhanden();
  }

  private Map<Gebiet, GebietDaten<Double>> berechneSkalierungProGebiet(StimmenhochrechnerAusgabedaten initialisierteAusgabedaten) {
    Map<Gebiet, GebietDaten<Double>> result = new HashMap<>();

    initialisierteAusgabedaten.getAusgabedatenProGebiet().values().forEach(
        gebietAusgabedaten ->  {
          if (gebietAusgabedaten.getGebiet().isErfassungsgebiet() || !konfiguration.getAbstandartZwischenGebieten().isEntferneGebieteOhneVorperiodenWerte()) { // ELECT-14438
            GebietDaten<Double> gebietsSkalierung = berechneGebietsSkalierungVonVorperiodeZuAktuell(gebietAusgabedaten);
            result.put(gebietAusgabedaten.getGebiet(), gebietsSkalierung);
          }
        }
    );

    return result;
  }

  private GebietDaten<Double> berechneGebietsSkalierungVonVorperiodeZuAktuell(StimmenhochrechnerGebietAusgabedaten gebietAusgabedaten) {
    return new GebietDatenErgebnisSkalierung(gebietAusgabedaten.getVorperiodeAnteile(), gebietAusgabedaten.getAktuellAnteile(), gebietAusgabedaten.getKoordinatenInformationen());
  }
}