package de.ivu.elect.business.stimmeingabe.boundary;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.ergebnis.boundary.ErgebnisExtendedDAO;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenCreatorExtended;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietExtendedDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.boundary.GruppeExtendedDAO;
import de.ivu.elect.business.gruppe.control.GruppeByIsSystemGruppeComparator;
import de.ivu.elect.business.gruppe.control.GruppeByRelativerPositionComparator;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.hochrechnung.entity.AnzahlStimmenProWaehlerNachStimmenartBestimmer;
import de.ivu.elect.business.hochrechnung.entity.EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.HochrechneralgorithmusAusfuehrbarkeitStatus;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmen;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerAusgabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerGebietAusgabedaten;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeAuffaelligkeit;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeHochgerechnet;
import de.ivu.elect.business.stimmzettel.boundary.AnzahlStimmenProWaehlerBestimmer;
import de.ivu.elect.business.wahlvorschlag.boundary.DirektkandidatExtendedDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.ListeExtendedDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.jee.entity.EagerLoadingCollector;
import de.ivu.elect.presentation.stimmeingabe.StimmeingabeAuffaelligkeitCache;

/**
 *  on 29.03.2016.
 */
public class StimmeingabeHochgerechnetSearch {

  private static final Logger LOG = LoggerFactory.getLogger(StimmeingabeHochgerechnetSearch.class);

  @Inject
  StimmeingabeAuffaelligkeitSearch stimmeingabeAuffaelligkeitSearch;

  @Inject
  ErgebnisExtendedDAO ergebnisExtendedDAO;

  @Inject
  WahldurchfuehrungsgebietExtendedDAO wahldurchfuehrungsgebietExtendedDAO;

  @Inject
  GruppeExtendedDAO gruppeExtendedDAO;

  @Inject
  private transient ErgebnisDatenCreatorExtended ergebnisDatenCreator;

  @Inject
  private transient AnzahlStimmenProWaehlerBestimmer anzahlStimmenProWaehlerBestimmer;
  @Inject
  protected transient DirektkandidatExtendedDAO direktkandidatExtendedDAO;
  @Inject
  protected transient ListeExtendedDAO listeExtendedDAO;

  private Gruppe gueltige;

  public List<StimmeingabeHochgerechnet> getSearchResult(StimmeingabeHochgerechnetSearchParameters searchParameters, EagerLoadingCollector<StimmeingabeHochgerechnet> eagerLoadingCollector, StimmeingabeAuffaelligkeitCache stimmeingabeAuffaelligkeitCache) {

    Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = wahldurchfuehrungsgebietExtendedDAO.attach(searchParameters.getWahldurchfuehrungsgebiet());
    Ergebnis ergebnis = ergebnisExtendedDAO.attach(searchParameters.getErgebnis());
    Set<Gruppe> gruppeSuchparameter = new HashSet<>(gruppeExtendedDAO.attachAll(searchParameters.getGruppen()));

    if (wahldurchfuehrungsgebiet == null || ergebnis == null) {
      return Collections.emptyList();
    }

    ErgebnisDaten ergebnisDaten = ergebnisDatenCreator.createErgebnisDatenSitzberechnung(ergebnis);
    gueltige = ergebnisDaten.getGruppeGueltige();

    StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten = getStimmenhochrechnerAusgabedaten(searchParameters, stimmeingabeAuffaelligkeitCache);


    List<StimmeingabeHochgerechnet> result = createStimmeingabenHochgerechnet(wahldurchfuehrungsgebiet, stimmenhochrechnerAusgabedaten, ergebnisDaten, searchParameters, gruppeSuchparameter);
    return result;
  }

  private StimmenhochrechnerAusgabedaten getStimmenhochrechnerAusgabedaten(StimmeingabeHochgerechnetSearchParameters searchParameters, StimmeingabeAuffaelligkeitCache stimmeingabeAuffaelligkeitCache) {
    List<StimmeingabeAuffaelligkeit> auffaelligkeiten = stimmeingabeAuffaelligkeitSearch.getSearchResult(searchParameters.getStimmeingabeAuffaelligkeitSearchParameters(), stimmeingabeAuffaelligkeitCache);
    StimmeingabeAuffaelligkeit stimmeingabeAuffaelligkeit = auffaelligkeiten.stream().filter(Objects::nonNull).findAny().orElse(null);
    if (stimmeingabeAuffaelligkeit == null) {
      return null;
    }
    return stimmeingabeAuffaelligkeit.getGesamtStimmenhochrechnerAusgabedaten();
  }

  private List<StimmeingabeHochgerechnet> createStimmeingabenHochgerechnet(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten, ErgebnisDaten ergebnisDaten, StimmeingabeHochgerechnetSearchParameters searchParameters, Set<Gruppe> gruppeSuchparameter) {
    GebietDaten<Long> vorperiode = getVorperiodeAnzahlen(wahldurchfuehrungsgebiet, stimmenhochrechnerAusgabedaten);
    GebietDaten<Long> aktuell = getAktuellAnzahlen(wahldurchfuehrungsgebiet, stimmenhochrechnerAusgabedaten);

    AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer = getAnzahlStimmenProWaehlerNachStimmenartBestimmer();
    List<GebietDaten<Long>> hochgerechnetFuerGebieteUnterhalbVon = getHochgerechnetFuerGebieteUnterhalbVon(wahldurchfuehrungsgebiet, stimmenhochrechnerAusgabedaten, ergebnisDaten, anzahlStimmenProWaehlerNachStimmenartBestimmer);

    HochrechneralgorithmusAusfuehrbarkeitStatus hochrechneralgorithmusAusfuehrbarkeitStatus = getHochrechneralgorithmusAusfuehrbarkeitStatus(stimmenhochrechnerAusgabedaten);
    return createStimmeingabenHochgerechnet(wahldurchfuehrungsgebiet, vorperiode, aktuell, hochgerechnetFuerGebieteUnterhalbVon, searchParameters, gruppeSuchparameter, hochrechneralgorithmusAusfuehrbarkeitStatus);
  }

  private HochrechneralgorithmusAusfuehrbarkeitStatus getHochrechneralgorithmusAusfuehrbarkeitStatus(StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    if (stimmenhochrechnerAusgabedaten == null) {
      return HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUS;
    }
    return stimmenhochrechnerAusgabedaten.getAusfuehrbarkeitStatus();
  }

  private List<GebietDaten<Long>> getHochgerechnetFuerGebieteUnterhalbVon(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten, ErgebnisDaten ergebnisDaten, AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer) {
    if (wahldurchfuehrungsgebiet.isErfassungsgebiet()) {
      return getHochgerechnetVonErfassungsgebiet(wahldurchfuehrungsgebiet, stimmenhochrechnerAusgabedaten, anzahlStimmenProWaehlerNachStimmenartBestimmer);
    }
    List<Wahldurchfuehrungsgebiet> erfassungsgebieteUnterhalbVon = ergebnisDaten.getErfassungsgebieteUnterhalbVon(wahldurchfuehrungsgebiet);
    List<GebietDaten<Long>> result = new ArrayList<>();
    erfassungsgebieteUnterhalbVon.forEach(erfassungsgebiet ->
        result.addAll(getHochgerechnetFuerGebieteUnterhalbVon(erfassungsgebiet, stimmenhochrechnerAusgabedaten, ergebnisDaten, anzahlStimmenProWaehlerNachStimmenartBestimmer)));
    return result;
  }

  private List<GebietDaten<Long>> getHochgerechnetVonErfassungsgebiet(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten, AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer) {
    List<GebietDaten<Long>> result = new ArrayList<>();
    if (stimmenhochrechnerAusgabedaten == null) {
      return result;
    }
    StimmenhochrechnerGebietAusgabedaten ausgabedatenVomGebiet = stimmenhochrechnerAusgabedaten.getAusgabedatenVomGebiet(wahldurchfuehrungsgebiet);
    if (ausgabedatenVomGebiet == null) {
      return result;
    }
    GebietDaten<Long> aktuellAnzahlen = ausgabedatenVomGebiet.getAktuellAnzahlen();
    if (aktuellAnzahlen != null && aktuellAnzahlen.isErgebnisVorhanden()) {
      // dann hochgerechnet = aktuell
      result.add(aktuellAnzahlen);
      return result;
    }
    result.add(ausgabedatenVomGebiet.getHochgerechnetAlsAnzahlen(anzahlStimmenProWaehlerNachStimmenartBestimmer));
    return result;
  }

  private GebietDaten<Long> getAktuellAnzahlen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    if (stimmenhochrechnerAusgabedaten == null) {
      return null;
    }
    StimmenhochrechnerGebietAusgabedaten ausgabedatenVomGebiet = stimmenhochrechnerAusgabedaten.getAusgabedatenVomGebiet(wahldurchfuehrungsgebiet);
    if (ausgabedatenVomGebiet == null) {
      return null;
    }
    return ausgabedatenVomGebiet.getAktuellAnzahlen();
  }

  private GebietDaten<Long> getVorperiodeAnzahlen(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    if (stimmenhochrechnerAusgabedaten == null) {
      return null;
    }
    StimmenhochrechnerGebietAusgabedaten ausgabedatenVomGebiet = stimmenhochrechnerAusgabedaten.getAusgabedatenVomGebiet(wahldurchfuehrungsgebiet);
    if (ausgabedatenVomGebiet == null) {
      return null;
    }
    return ausgabedatenVomGebiet.getVorperiodeAnzahlen();
  }

  private List<StimmeingabeHochgerechnet> createStimmeingabenHochgerechnet(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, GebietDaten<Long> vorperiode, GebietDaten<Long> aktuell, List<GebietDaten<Long>> hochgerechnetFuerGebieteUnterhalbVon, StimmeingabeHochgerechnetSearchParameters searchParameters, Set<Gruppe> gruppeSuchparameter, HochrechneralgorithmusAusfuehrbarkeitStatus hochrechneralgorithmusAusfuehrbarkeitStatus) {

    List<Stimmenart> stimmenarten = extrahiereUndSortiereStimmenarten(aktuell, hochgerechnetFuerGebieteUnterhalbVon, searchParameters);
    List<Gruppe> gruppen = extrahiereUndSortiereGruppen(aktuell, hochgerechnetFuerGebieteUnterhalbVon, gruppeSuchparameter);

    List<StimmeingabeHochgerechnet> result = new ArrayList<>();

    Map<Stimmenart, Long> gueltigeVorperiode = new HashMap<>();
    Map<Stimmenart, Long> gueltigeAktuell = new HashMap<>();
    Map<Stimmenart, Long> gueltigeHochgerechnet = new HashMap<>();

    long counter = 0L;
    for (Gruppe gruppe : gruppen) {
      for (Stimmenart stimmenart : stimmenarten) {

        gueltigeVorperiode.putIfAbsent(stimmenart, bestimme(vorperiode, gueltige, stimmenart));
        gueltigeAktuell.putIfAbsent(stimmenart, bestimme(aktuell, gueltige, stimmenart));
        gueltigeHochgerechnet.putIfAbsent(stimmenart, bestimme(hochgerechnetFuerGebieteUnterhalbVon, gueltige, stimmenart));

        Long anzahlVorperiode = bestimme(vorperiode, gruppe, stimmenart);
        Long anzahlAktuell = bestimme(aktuell, gruppe, stimmenart);
        Long anzahlHochgerechnet = bestimme(hochgerechnetFuerGebieteUnterhalbVon, gruppe, stimmenart);

        if (anzahlAktuell != null || anzahlHochgerechnet != null) {
          result.add(new StimmeingabeHochgerechnet(counter++, wahldurchfuehrungsgebiet, gruppe, stimmenart, anzahlVorperiode, anzahlAktuell, anzahlHochgerechnet, gueltigeVorperiode.get(stimmenart), gueltigeHochgerechnet.get(stimmenart), gueltigeAktuell.get(stimmenart),
              hochrechneralgorithmusAusfuehrbarkeitStatus));
        }
      }
    }

    if (result.isEmpty()) {
      // damit der Fehlergrund in hochrechneralgorithmusAusfuehrbarkeitStatus weitergegeben werden kann
      result.add(new StimmeingabeHochgerechnet(0L, wahldurchfuehrungsgebiet, gueltige, Stimmenart.GESAMT, null, null, null, null, null, null, hochrechneralgorithmusAusfuehrbarkeitStatus));
    }

    return result;
  }

  private List<Stimmenart> extrahiereUndSortiereStimmenarten(GebietDaten<Long> aktuell, List<GebietDaten<Long>> hochgerechnetFuerGebieteUnterhalbVon, StimmeingabeHochgerechnetSearchParameters searchParameters) {
    Set<Stimmenart> stimmenarten = new HashSet<>();

    List<GebietDaten<Long>> alleGebietDaten = new ArrayList<>(hochgerechnetFuerGebieteUnterhalbVon);
    alleGebietDaten.add(aktuell);

    alleGebietDaten.forEach(gebietDaten -> stimmenarten.addAll(extrahiereStimmenarten(gebietDaten)));

    List<Stimmenart> result = stimmenarten.stream()
        .filter(stimmenart -> isStimmenartMatches(searchParameters, stimmenart))
        .sorted().collect(Collectors.toList());
    return result;
  }

  private Set<Stimmenart> extrahiereStimmenarten(GebietDaten<Long> gebietDaten) {
    Set<Stimmenart> result = new HashSet<>();
    if (gebietDaten == null) {
      return result;
    }

    result.addAll(gebietDaten.getGueltige().keySet());
    gebietDaten.getParteiStimmen().values().forEach(parteistimme -> {
      if (parteistimme != null) {
        result.addAll(parteistimme.getStimmen().keySet());
      }
    });

    return result;
  }

  private List<Gruppe> extrahiereUndSortiereGruppen(GebietDaten<Long> aktuell, List<GebietDaten<Long>> hochgerechnetFuerGebieteUnterhalbVon, Set<Gruppe> gruppeSuchparameter) {
    Set<Gruppe> gruppen = new HashSet<>();

    List<GebietDaten<Long>> alleGebietDaten = new ArrayList<>(hochgerechnetFuerGebieteUnterhalbVon);
    alleGebietDaten.add(aktuell);

    gruppen.add(gueltige);
    alleGebietDaten.forEach(gebietDaten -> gruppen.addAll(extrahiereGruppen(gebietDaten)));

    List<Gruppe> result = gruppen.stream()
        .filter(gruppe -> isGruppeMatches(gruppeSuchparameter, gruppe))
        .sorted(Comparator.comparing(Gruppe::getId))
        .sorted(new GruppeByRelativerPositionComparator())
        .sorted(new GruppeByIsSystemGruppeComparator())
        .collect(Collectors.toList());
    return result;
  }

  private Set<Gruppe> extrahiereGruppen(GebietDaten<Long> gebietDaten) {
    Set<Gruppe> result = new HashSet<>();
    if (gebietDaten == null) {
      return result;
    }

    result.addAll(gebietDaten.getParteiStimmen().keySet());

    result.addAll(gebietDaten.getReferendumFragen().keySet());

    return result;
  }

  private AnzahlStimmenProWaehlerNachStimmenartBestimmer getAnzahlStimmenProWaehlerNachStimmenartBestimmer() {
    EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer einfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer = new EinfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer(
        anzahlStimmenProWaehlerBestimmer, getWahlvorschlaegeInDirektwahlgebietFct(), getWahlvorschlaegeInListenwahlgebietFct());
    return einfacheWahlAnzahlStimmenProWaehlerNachStimmenartBestimmer;
  }

  public Function<Gebiet, Collection<Direktkandidat>> getWahlvorschlaegeInDirektwahlgebietFct() {
    if (direktkandidatExtendedDAO == null) {
      return gebiet -> new ArrayList<>();
    }
    return direktkandidatExtendedDAO::findByGebiet;
  }
  public Function<Gebiet, Collection<Liste>> getWahlvorschlaegeInListenwahlgebietFct() {
    if (listeExtendedDAO == null) {
      return gebiet -> new ArrayList<>();
    }
    return listeExtendedDAO::findByGebiet;
  }

  private boolean isGruppeMatches(Set<Gruppe> suchParameterGruppen, Gruppe gruppe) {
    return suchParameterGruppen == null || suchParameterGruppen.isEmpty() || suchParameterGruppen.contains(gruppe);
  }

  private boolean isStimmenartMatches(StimmeingabeHochgerechnetSearchParameters searchParameters, Stimmenart stimmenart) {
    return searchParameters.getStimmenart() == null || searchParameters.getStimmenart().equals(stimmenart);
  }

  private Long bestimme(List<GebietDaten<Long>> gebietDaten, Gruppe gruppe, Stimmenart stimmenart) {
    List<Long> entsprechendeStimmen = gebietDaten.stream()
        .map(g -> bestimme(g, gruppe, stimmenart))
        .filter(Objects::nonNull)
        .collect(Collectors.toList());
    if (entsprechendeStimmen.isEmpty()) {
      return null;
    }
    return entsprechendeStimmen.stream().mapToLong(x -> x)
        .sum();
  }

  private Long bestimme(GebietDaten<Long> gebietDaten, Gruppe gruppe, Stimmenart stimmenart) {
    if (gebietDaten == null) {
      return null;
    }
    if (gruppe.isPolitischeGruppe()) {
      return bestimmePolitischeGruppeStimmen(gebietDaten, gruppe, stimmenart);
    }
    if (gruppe == gueltige) {
      return bestimmeGueltigeSystemGruppeStimmen(gebietDaten, stimmenart);
    }

    return null;
  }

  private Long bestimmeGueltigeSystemGruppeStimmen(GebietDaten<Long> gebietDaten, Stimmenart stimmenart) {
    if (gebietDaten == null) {
      return null;
    }
    return gebietDaten.getGueltige().get(stimmenart);
  }

  private Long bestimmePolitischeGruppeStimmen(GebietDaten<Long> gebietDaten, Gruppe gruppe, Stimmenart stimmenart) {
    if (gebietDaten == null) {
      return null;
    }
    ParteiStimmen<Long> parteiStimmen = gebietDaten.getParteiStimmen().get(gruppe);
    if (parteiStimmen == null) {
      return null;
    }
    return parteiStimmen.getStimmen(stimmenart);
  }

}
