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.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

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.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;

public class SkalierungImErgebnisBerechner {

  public static final Double SEHR_KLEINE_STIMMENANTEIL = 5E-3;

  private Double getSkalierung (Function<GebietDaten<Double>, Double> valueGetter, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, boolean isInVorperiodeVorhanden) {
    Double wertVorperiode = gebietDatenVorperiode != null? valueGetter.apply(gebietDatenVorperiode) : null;
    Double wertAktuell = gebietDatenAktuell != null? valueGetter.apply(gebietDatenAktuell) : null;
    return berechneSkalierung(wertVorperiode, wertAktuell, isInVorperiodeVorhanden);
  }

  private Double berechneSkalierung(Double wertVorperiode, Double wertAktuell, boolean wertInVorperiodeVorhanden) {
    // Wichtigste Funktion
    if (wertAktuell == null) {
      return null;
    }
    if (!wertInVorperiodeVorhanden) {
      return wertAktuell;
    }
    if (wertVorperiode == null || wertVorperiode < SEHR_KLEINE_STIMMENANTEIL) {
      // Wir ignorieren die kleinen Stimmenanteile, weil bei eine kleine Erhoehung in der Anzahl der Waehler sich die Skalierung radikal erhoehen kann.
      return null;
    }
    return (wertAktuell - wertVorperiode) / wertVorperiode;
  }

  public Map<Stimmenart, Double> berechneGueltigeSkalierung (GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    return berechneSystemGruppeSkalierung(SystemGruppe.GUELTIGE, gebietDatenVorperiode, gebietDatenAktuell, koordinatenInformationen);
  }

  public Map<Stimmenart, Double> berechneUngueltigeSkalierung (GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    return berechneSystemGruppeSkalierung(SystemGruppe.UNGUELTIGE, gebietDatenVorperiode, gebietDatenAktuell, koordinatenInformationen);
  }

  private Map<Stimmenart, Double> berechneSystemGruppeSkalierung(SystemGruppe systemGruppe, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    Map<Stimmenart, Double> result = new EnumMap<>(Stimmenart.class);
    koordinatenInformationen.getAlleStimmenartenSortiert()
        .forEach(stimmenart -> result.put(stimmenart,
            getSystemGruppeStimmeSkaliert(systemGruppe, gebietDatenVorperiode, gebietDatenAktuell, koordinatenInformationen, stimmenart)));
    return result;
  }

  private Double getSystemGruppeStimmeSkaliert(SystemGruppe systemGruppe, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen, Stimmenart stimmenart) {
    return getSkalierung(getSystemGruppeValueGetter(systemGruppe, stimmenart), gebietDatenVorperiode, gebietDatenAktuell,
        koordinatenInformationen.getGruppeInVorperiodeVorhandenChecker().isSystemGruppeInVorperiodeVorhanden(systemGruppe, stimmenart));
  }

  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;
  }



  public WahlteilnehmerAnzahl getAnzahlWaehlerImGebiet(GebietDaten<? extends Number> gebietDaten) {
    // Zurzeit machen wir keine Skalierung fuer B werte
    return gebietDaten != null? gebietDaten.getAnzahlWaehlerImGebiet() : new WahlteilnehmerAnzahl();
  }

  public WahlteilnehmerAnzahl getAnzahlWahlberechtigteImGebiet(GebietDaten<? extends Number> gebietDaten) {
    // Zurzeit machen wir keine Skalierung fuer A werte
    return gebietDaten != null? gebietDaten.getAnzahlWahlberechtigteImGebiet() : new WahlteilnehmerAnzahl();
  }

  public Gebiet getGebiet(GebietDaten<Double> vorperiode, GebietDaten<Double> aktuell) {
    if (aktuell != null && aktuell.getGebiet() != null) {
      return aktuell.getGebiet();
    } else if (vorperiode != null && vorperiode.getGebiet() != null) {
      return vorperiode.getGebiet();
    }
    return null;
  }

  public boolean isErgebnisVorhanden(GebietDaten<Double> aktuell) {
    return aktuell != null && aktuell.isErgebnisVorhanden();
  }




  public Map<PolitischeGruppe, ParteiStimmen<Double>> berechneParteiStimmenSkaliert(GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    Map<PolitischeGruppe, ParteiStimmen<Double>> result = new HashMap<>();
    List<PolitischeGruppe> fuerGebietRelevantePolitischeGruppen = getFuerGebietRelevantePolitischeGruppen(gebietDatenAktuell, koordinatenInformationen);
    fuerGebietRelevantePolitischeGruppen
        .forEach(politischeGruppe -> result.put(politischeGruppe, berechneSkalierteParteiStimme(politischeGruppe, gebietDatenVorperiode, gebietDatenAktuell, koordinatenInformationen)));
    return result;
  }

  private List<PolitischeGruppe> getFuerGebietRelevantePolitischeGruppen(GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    List<PolitischeGruppe> politischeGruppenVonErgebnisAktuell = extractPolitischeGruppenVonGebietDaten(gebietDatenAktuell);
    if (!politischeGruppenVonErgebnisAktuell.isEmpty()) {
      return politischeGruppenVonErgebnisAktuell;
    }
    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> berechneSkalierteParteiStimme(PolitischeGruppe politischeGruppe, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    Map<Stimmenart, Double> stimmen = new HashMap<>();
    koordinatenInformationen.getAlleStimmenartenSortiert()
        .forEach(stimmenart -> stimmen.put(stimmenart, berechneSkalierteStimme(politischeGruppe, stimmenart, gebietDatenVorperiode, gebietDatenAktuell, koordinatenInformationen)));
    Map<Listenkandidatur, Double> listenKandidaturStimmenSkaliert = berechneSkalierteListenKandidaturStimmen(politischeGruppe, gebietDatenAktuell);
    return new ParteiStimmen<>(politischeGruppe, stimmen, listenKandidaturStimmenSkaliert);
  }

  private Map<Listenkandidatur, Double> berechneSkalierteListenKandidaturStimmen(PolitischeGruppe politischeGruppe, GebietDaten<Double> gebietDatenAktuell) {
    if (gebietDatenAktuell == null) {
      return Collections.emptyMap();
    }
    ParteiStimmen<Double> parteiStimmen = gebietDatenAktuell.getParteiStimmen().get(politischeGruppe);
    if (parteiStimmen != null) {
      return parteiStimmen.getListenkandidaturStimmen();
    }
    return Collections.emptyMap();
  }

  private Double berechneSkalierteStimme(PolitischeGruppe politischeGruppe, Stimmenart stimmenart, GebietDaten<Double> gebietDatenVorperiode, GebietDaten<Double> gebietDatenAktuell, KoordinatenInformationen koordinatenInformationen) {
    return getSkalierung(getParteistimmenValueGetter(politischeGruppe, stimmenart),
        gebietDatenVorperiode,
        gebietDatenAktuell,
        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);
  }

  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;
  }




  public GebietDatenWahlbeteiligung berechneWahlbeteiligungSkalierung(GebietDaten<Double> vorperiode, GebietDaten<Double> aktuell) {
    GebietDatenWahlbeteiligung result = new GebietDatenWahlbeteiligung();
    if (!(isWahlbeteiligungVorhanden(vorperiode) || isWahlbeteiligungVorhanden(aktuell))) {
      return result;
    }
    Set<Stimmenart> stimmenarten = getStimmenartenFuerWahlbeteiligung(vorperiode, aktuell);
    for (Stimmenart stimmenart: stimmenarten) {
      result.setAnzahl(stimmenart, getSkalierung(getWahlbeteiligungValueGetter(stimmenart), vorperiode, aktuell, isVorperiodeErgebnisVorhanden(vorperiode)));
    }
    return result;
  }

  private boolean isWahlbeteiligungVorhanden(GebietDaten<Double> gebietDaten) {
    if (gebietDaten == null) {
      return false;
    }
    GebietDatenWahlbeteiligung wahlbeteiligung = gebietDaten.getWahlbeteiligung();
    return wahlbeteiligung != null && wahlbeteiligung.getOhneWahlscheinNotNull() + wahlbeteiligung.getMitWahlscheinNotNull() + wahlbeteiligung.getGesamtNotNull() > .001;
  }

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

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

  private Set<Stimmenart> getStimmenartenFuerWahlbeteiligung(GebietDaten<Double> vorperiode, GebietDaten<Double> aktuell) {
    Set<Stimmenart> result = new HashSet<>();
    if (vorperiode != null && vorperiode.getWahlbeteiligung() != null) {
      result.addAll(vorperiode.getWahlbeteiligung().getStimmenarten());
    }
    if (aktuell != null && aktuell.getWahlbeteiligung() != null) {
      result.addAll(aktuell.getWahlbeteiligung().getStimmenarten());
    }
    return result;
  }

  private boolean isVorperiodeErgebnisVorhanden(GebietDaten<Double> vorperiode) {
    return vorperiode != null && vorperiode.isErgebnisVorhanden();
  }

  public Map<Gruppe, ReferendumFrage<Double>> berechneReferendumFragenSkaliert(GebietDaten<Double> vorperiode, GebietDaten<Double> aktuell, KoordinatenInformationen koordinatenInformationen) {
    Map<Gruppe, ReferendumFrage<Double>> result = new HashMap<>();

    // da wir keine Refendum von der Vorperiode betrachten, setzen wir die Skalierung auf Daten der akuellen Wahl
    if (aktuell == null) {
      return result;
    }
    aktuell.getReferendumFragen().forEach(((referendumVorlage, referendumFrage) -> result.put(referendumVorlage, getEineKopieReferendumFrage(referendumFrage))));

    return result;
  }

  private ReferendumFrage<Double> getEineKopieReferendumFrage(ReferendumFrage<Double> referendumFrage) {
    return new ReferendumFrage<>(referendumFrage.getGueltige(), referendumFrage.getUngueltige(),
        referendumFrage.getReferendumVorlage(), new HashMap<>(referendumFrage.getStimmenPerReferendumOption()));
  }
}
