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

import java.io.Serializable;
import java.util.Collections;
import java.util.Comparator;
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 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.PolitischeGruppe;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmen;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmenAlsPunkteAnEinerParabelGeneratorKonfiguration;
import de.ivu.elect.business.hochrechnung.entity.ZufaelligeParteiStimmenGenerator;
import de.ivu.elect.business.kandidatur.control.ListenkandidaturByRelativePositionComparator;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;

public class ParteiStimmenAlsPunkteAnEinerParabelGenerator extends ZufaelligeParteiStimmenGenerator {

  ParteiStimmenAlsPunkteAnEinerParabelGeneratorKonfiguration konfiguration;

  public ParteiStimmenAlsPunkteAnEinerParabelGenerator(ParteiStimmenAlsPunkteAnEinerParabelGeneratorKonfiguration konfiguration) {
    this.konfiguration = konfiguration;
  }

  @Override
  public Map<PolitischeGruppe, ParteiStimmen<Double>> generateZufaelligeParteiStimmenanteile(Gebiet gebiet, List<PolitischeGruppe> politischeGruppen, Function<PolitischeGruppe, Set<Stimmenart>> stimmenartenProPolitischeGruppe, Function<PolitischeGruppe, Set<Listenkandidatur>> listenkandidaturenProPolitischeGruppe) {
    Map<PolitischeGruppe, ParteiStimmen<Double>> resultUnnormiert = new HashMap<>();

    long anzahlPolitischerGruppen = politischeGruppen.size();
    Map<PolitischeGruppe, Double> stimmenProPolitischeGruppe = erzeugePolitischeGruppenAlsPunkteAnEinerParabel(konfiguration.getVerhaeltnisAnzahlStimmenErsterPolitischenGruppeZuAnzahlStimmenLetzterPolitischenGruppe(anzahlPolitischerGruppen), new HashSet<>(politischeGruppen));

    for (PolitischeGruppe politischeGruppe: politischeGruppen) {
      resultUnnormiert.put(politischeGruppe, getParteiStimmenFuerPolitischeGruppe(politischeGruppe, stimmenProPolitischeGruppe.get(politischeGruppe), stimmenartenProPolitischeGruppe.apply(politischeGruppe), listenkandidaturenProPolitischeGruppe.apply(politischeGruppe)));
    }

    return new GebietDatenNormierer<Double>().getParteiStimmenAnteileNormiert(resultUnnormiert);
  }

  private ParteiStimmen<Double> getParteiStimmenFuerPolitischeGruppe(PolitischeGruppe politischeGruppe, Double stimmenanteilGesamtFuerGruppe, Set<Stimmenart> stimmenarten, Set<Listenkandidatur> listenkandidaturen) {
    Map<Stimmenart, Double> stimmen = new EnumMap<>(Stimmenart.class);

    if (stimmenarten != null) {
      stimmenarten.forEach(stimmenart -> stimmen.put(stimmenart, stimmenanteilGesamtFuerGruppe));
    }

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

    if (listenkandidaturen != null && !listenkandidaturen.isEmpty()) {
      long anzahlListenkandidaturen = listenkandidaturen.size();
      listenkandidaturStimmen = erzeugeListenKandidaturStimmenAlsPunkteAnEinerParabel(
          konfiguration.getVerhaeltnisAnzahlStimmenErstesListenkandidatursZuAnzahlStimmenLetztesListenkandidaturs(anzahlListenkandidaturen), listenkandidaturen);
    }

    return new ParteiStimmen<>(politischeGruppe, stimmen, listenkandidaturStimmen);
  }

  private Map<PolitischeGruppe, Double> erzeugePolitischeGruppenAlsPunkteAnEinerParabel(long verhaeltnisAnzahlStimmenErsterPolitischenGruppeZuAnzahlStimmenLetzterPolitischenGruppe, Set<PolitischeGruppe> politischeGruppen) {
    Map<PolitischeGruppe, Double> result = erzeugeAnteileAlsPunkteAnEinerParabel(verhaeltnisAnzahlStimmenErsterPolitischenGruppeZuAnzahlStimmenLetzterPolitischenGruppe,
            politischeGruppen, new PolitscheGruppeByRelativerPositionComparator(), politischeGruppe -> 1L);

    return result;
  }

  public static Map<Listenkandidatur, Double> erzeugeListenKandidaturStimmenAlsPunkteAnEinerParabel(long verhaeltnisAnzahlStimmenErstesListenkandidatursZuAnzahlStimmenLetztesListenkandidaturs, Set<Listenkandidatur> listenkandidaturenAmGebiet) {
    Map<Listenkandidatur, Double> result = erzeugeAnteileAlsPunkteAnEinerParabel(verhaeltnisAnzahlStimmenErstesListenkandidatursZuAnzahlStimmenLetztesListenkandidaturs,
        listenkandidaturenAmGebiet,
        new ListenkandidaturByRelativePositionComparator(),
        ParteiStimmenAlsPunkteAnEinerParabelGenerator::getAnzahlNennungenFuerListenkandidatur);

    return result;
  }

  /**
   * Nutzung von Parabel ist sinnvoll, weil der Unterschied zwischen z. B. Listenkandidaturenstimmen wie quadratisch waechst mit ihren Positionen auf dem Stimmzettel.
   * @param verhaeltnisAnzahlFuerWichtigstesObjektZuAnzahlVomLetztenObjekt: z. B. 10 wenn das erste Objekt zehnfach groesser als das letzte Objekt ist
   * @return Anteile fuer die gegebene Objekten, die an einer Parabel (symmetrisch zur y achse) liegen.
   */
  public static <O> Map<O, Double> erzeugeAnteileAlsPunkteAnEinerParabel(long verhaeltnisAnzahlFuerWichtigstesObjektZuAnzahlVomLetztenObjekt, Set<O> objekte, Comparator<O> comparatorSortiertNachWichtigkeitAufsteigend, Function<O, Long> zusaetzlicheWichtungFunktoin) {
    // Gleichung fuer die Parabel: y = ax^2 + c
    // geht durch Punkte:
    // (0, 1 * c) ~ letztes Objekt
    // (anzahlObjekte - 1, verhaeltnisAnzahlFuerWichtigstesObjektZuAnzahlVomLetztenObjekt * c) ~ erstes Objekt
    // setze c auf 1, da es keine Rolle spielt
    // => a = (verhaeltnisAnzahlFuerWichtigstesObjektZuAnzahlVomLetztenObjekt - 1)/ ((anzahlObjekte - 1) * (anzahlObjekte - 1))

    long anzahlObjekte = objekte.size();
    if (anzahlObjekte < 1) {
      return new HashMap<>();
    }
    if (anzahlObjekte == 1) {
      return Collections.singletonMap(objekte.iterator().next(), 1d);
    }

    List<O> objekteSortiertNachWichtigkeitAbsteigend = objekte.stream()
        .sorted(comparatorSortiertNachWichtigkeitAufsteigend.reversed())
        .collect(Collectors.toList());

    double a = 1.0 * (verhaeltnisAnzahlFuerWichtigstesObjektZuAnzahlVomLetztenObjekt - 1)/ ((anzahlObjekte - 1) * (anzahlObjekte - 1));
    long c = 1;

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

    long x = 0;
    for (O o : objekteSortiertNachWichtigkeitAbsteigend) {
      Long wichtung = zusaetzlicheWichtungFunktoin.apply(o);
      if (wichtung == null || wichtung < 1) {
        wichtung = 1L;
      }
      double y = a * x * x + c;
      result.put(o, y * wichtung);
      x++;
    }

    return result;
  }

  private static long getAnzahlNennungenFuerListenkandidatur(Listenkandidatur listenkandidatur) {
    Long anzahlNennungen = listenkandidatur.getAnzahlNennungen();
    if (anzahlNennungen == null || anzahlNennungen < 1) {
      anzahlNennungen = 1L;
    }
    return anzahlNennungen;
  }

  private class PolitscheGruppeByRelativerPositionComparator implements Comparator<PolitischeGruppe>, Serializable {
    @Override
    public int compare(PolitischeGruppe o1, PolitischeGruppe o2) {
      return o1.getRelativePosition().compareTo(o2.getRelativePosition());
    }
  }
}
