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

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 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.control.StimmergebnisDaten;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
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.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.HochrechneralgorithmusAusfuehrbarkeitStatus;
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;


import static java.util.Collections.sort;

/**
 *  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);

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

    List<StimmeingabeAuffaelligkeit> auffaelligkeiten = stimmeingabeAuffaelligkeitSearch.getSearchResult(searchParameters.getStimmeingabeAuffaelligkeitSearchParameters(), stimmeingabeAuffaelligkeitCache);
    HochrechneralgorithmusAusfuehrbarkeitStatus hochrechneralgorithmusAusfuehrbarkeitStatus = getHochrechneralgorithmusAusfuehrbarkeitStatus(auffaelligkeiten);

    Map<Gebiet, Map<Stimmenart, Map<Gruppe, Stimmen>>> vorperiodeMap = erstelleVorperiodeMap(ergebnisDaten);

    long counter = 0L;

    gueltige = ergebnisDaten.getGruppeGueltige();
    AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer = getAnzahlStimmenProWaehlerNachStimmenartBestimmer();

    for (StimmeingabeAuffaelligkeit stimmeingabeAuffaelligkeit : auffaelligkeiten) {
      List<Stimmenart> stimmenarten = extrahiereUndSortiereStimmenarten(stimmeingabeAuffaelligkeit, anzahlStimmenProWaehlerNachStimmenartBestimmer, searchParameters);
      List<Gruppe> gruppen = extrahiereUndSortiereGruppen(stimmeingabeAuffaelligkeit, anzahlStimmenProWaehlerNachStimmenartBestimmer, gruppeSuchparameter);
      Wahldurchfuehrungsgebiet gebiet = stimmeingabeAuffaelligkeit.getWahldurchfuehrungsgebiet();

      Map<Stimmenart, Map<Gruppe, Stimmen>> aktuelleStimmen = mappe(stimmeingabeAuffaelligkeit.getAktuelleStimmen(gueltige));
      Map<Stimmenart, Map<Gruppe, Stimmen>> hochgerechneteStimmen = mappe(stimmeingabeAuffaelligkeit.getHochgerechneteStimmen(
          anzahlStimmenProWaehlerNachStimmenartBestimmer, gueltige));
      for (Gruppe gruppe : gruppen) {
        for (Stimmenart stimmenart : stimmenarten) {
          Long anzahlVorperiode = bestimme(vorperiodeMap, gebiet, gruppe, stimmenart);
          Long anzahlHochgerechnet = bestimme(hochgerechneteStimmen, gruppe, stimmenart);
          Long anzahlAktuell = bestimme(aktuelleStimmen, gruppe, stimmenart);

          Long gueltigeVorperiode = null;
          Long gueltigeHochgerechnet = null;
          Long gueltigeAktuell = null;
          if (!gruppe.equals(gueltige)) {
            gueltigeVorperiode = bestimme(vorperiodeMap, gebiet, gueltige, stimmenart);
            gueltigeHochgerechnet = bestimme(hochgerechneteStimmen, gueltige, stimmenart);
            gueltigeAktuell = bestimme(aktuelleStimmen, gueltige, stimmenart);
          }

          if (anzahlAktuell != null || anzahlHochgerechnet != null) {
            result.add(new StimmeingabeHochgerechnet(counter++, gebiet, gruppe, stimmenart, anzahlVorperiode, anzahlAktuell, anzahlHochgerechnet, gueltigeVorperiode, gueltigeHochgerechnet, gueltigeAktuell,
                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 HochrechneralgorithmusAusfuehrbarkeitStatus getHochrechneralgorithmusAusfuehrbarkeitStatus(List<StimmeingabeAuffaelligkeit> auffaelligkeitenSearchResult) {
    StimmeingabeAuffaelligkeit stimmeingabeAuffaelligkeit = auffaelligkeitenSearchResult.stream().filter(Objects::nonNull).findAny().orElse(null);
    if (stimmeingabeAuffaelligkeit == null) {
      return HochrechneralgorithmusAusfuehrbarkeitStatus.BERECHNUNG_NOCH_NICHT_AUSGEFUEHRT;
    }
    return stimmeingabeAuffaelligkeit.getGesamtStimmenhochrechnerAusgabedaten().getAusfuehrbarkeitStatus();
  }

  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 Long bestimme(Map<Gebiet, Map<Stimmenart, Map<Gruppe, Stimmen>>> map, Wahldurchfuehrungsgebiet gebiet, Gruppe gruppe, Stimmenart stimmenart) {
    Map<Stimmenart, Map<Gruppe, Stimmen>> stimmenImGebiet = map.get(gebiet);
    if (stimmenImGebiet == null) {
      return null;
    }
    return bestimme(stimmenImGebiet, gruppe, stimmenart);
  }

  private Map<Gebiet, Map<Stimmenart, Map<Gruppe, Stimmen>>> erstelleVorperiodeMap(StimmergebnisDaten ergebnisDaten) {
    LOG.info("Erstelle Vorperiode Map");
    Map<Gebiet, Map<Stimmenart, Map<Gruppe, Stimmen>>> result = new HashMap<>();
    for (Stimmen stimmen : ergebnisDaten.getStimmenVorperiode()) {
      if (!result.containsKey(stimmen.getGebiet())) {
        result.put(stimmen.getGebiet(), new HashMap<>());
      }

      Map<Stimmenart, Map<Gruppe, Stimmen>> stimmenImGebiet = result.get(stimmen.getGebiet());

      if (!stimmenImGebiet.containsKey(stimmen.getStimmenart())) {
        stimmenImGebiet.put(stimmen.getStimmenart(), new HashMap<>());
      }
      stimmenImGebiet.get(stimmen.getStimmenart()).put(stimmen.getGruppe(), stimmen);
    }
    LOG.info("Erstelle Vorperiode Map - done");
    return result;
  }

  private List<Gruppe> extrahiereUndSortiereGruppen(StimmeingabeAuffaelligkeit stimmeingabeAuffaelligkeit, AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer, Set<Gruppe> suchParameterGruppen) {

    Set<Gruppe> gruppen = new HashSet<>();

    for (Stimmen stimmen : stimmeingabeAuffaelligkeit.getHochgerechneteStimmen(anzahlStimmenProWaehlerNachStimmenartBestimmer, gueltige)) {
      addIfMatches(gruppen, stimmen, suchParameterGruppen);
    }

    for (Stimmen stimmen : stimmeingabeAuffaelligkeit.getAktuelleStimmen(gueltige)) {
      addIfMatches(gruppen, stimmen, suchParameterGruppen);
    }

    List<Gruppe> result = new ArrayList<>(gruppen);
    sort(result, (u, v) -> u.getId().compareTo(v.getId()));
    sort(result, new GruppeByIsSystemGruppeComparator());
    return result;
  }

  private void addIfMatches(Set<Gruppe> gruppen, Stimmen stimmen, Set<Gruppe> suchParameterGruppen) {
    Gruppe gruppe = stimmen.getGruppe();
    if (suchParameterGruppen == null || suchParameterGruppen.isEmpty() || suchParameterGruppen.contains(gruppe)) {
      gruppen.add(gruppe);
    }
  }

  private List<Stimmenart> extrahiereUndSortiereStimmenarten(StimmeingabeAuffaelligkeit stimmeingabeAuffaelligkeit, AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer, StimmeingabeHochgerechnetSearchParameters searchParameters) {

    Set<Stimmenart> stimmenarten = new HashSet<>();

    for (Stimmen stimmen : stimmeingabeAuffaelligkeit.getHochgerechneteStimmen(anzahlStimmenProWaehlerNachStimmenartBestimmer, gueltige)) {
      addIfMatches(searchParameters, stimmenarten, stimmen);
    }

    for (Stimmen stimmen : stimmeingabeAuffaelligkeit.getAktuelleStimmen(gueltige)) {
      addIfMatches(searchParameters, stimmenarten, stimmen);
    }

    List<Stimmenart> result = new ArrayList<>(stimmenarten);
    sort(result);
    return result;
  }

  private void addIfMatches(StimmeingabeHochgerechnetSearchParameters searchParameters, Set<Stimmenart> stimmenarten, Stimmen stimmen) {
    Stimmenart stimmenart = stimmen.getStimmenart();
    if (searchParameters.getStimmenart() == null || searchParameters.getStimmenart().equals(stimmenart)) {
      stimmenarten.add(stimmenart);
    }
  }

  private Map<Stimmenart, Map<Gruppe, Stimmen>> mappe(Set<Stimmen> stimmenSet) {

    Map<Stimmenart, Map<Gruppe, Stimmen>> result = new HashMap<>();

    for (Stimmen stimmen : stimmenSet) {
      if (!result.containsKey(stimmen.getStimmenart())) {
        result.put(stimmen.getStimmenart(), new HashMap<>());
      }
      result.get(stimmen.getStimmenart()).put(stimmen.getGruppe(), stimmen);
    }

    return result;
  }

  private Long bestimme(Map<Stimmenart, Map<Gruppe, Stimmen>> mapped, Gruppe gruppe, Stimmenart stimmenart) {
    Map<Gruppe, Stimmen> gruppeStimmen = mapped.get(stimmenart);
    if (gruppeStimmen == null) {
      return null;
    }
    Stimmen stimmen = gruppeStimmen.get(gruppe);
    if (stimmen == null) {
      return null;
    }
    return stimmen.getAnzahl();
  }

}
