
package de.ivu.elect.presentation.ergebnis;

import static java.util.Arrays.asList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.inject.Instance;
import javax.faces.convert.EnumConverter;
import javax.inject.Inject;

import de.ivu.elect.business.benutzerverwaltung.entity.Benutzer;
import de.ivu.elect.business.benutzerverwaltung.entity.BenutzerEinsatzort;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisSelectChangeEvent;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisSessionSelect;
import de.ivu.elect.business.ergebnis.control.AbstractKumulierteStimmenSearch;
import de.ivu.elect.business.ergebnis.control.AbstractKumulierteStimmenSearchParameters;
import de.ivu.elect.business.ergebnis.control.ErgebnisAuszaehlungsstandErmittler;
import de.ivu.elect.business.ergebnis.entity.Ergebnis;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.ergebnis.entity.KumulierteStimmen;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.gebietsbaum.boundary.GebietSelectChangeEvent;
import de.ivu.elect.business.gebietsbaum.boundary.GebietsgruppierungExtendedDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietSessionSelect;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebietsgruppierung;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.boundary.PolitischeGruppeExtendedDAO;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppeEigenschaften;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietExtendedDAO;
import de.ivu.elect.business.security.UseCasePermission;
import de.ivu.elect.business.security.control.ElectCommonPrincipal;
import de.ivu.elect.business.stimmeingabe.boundary.ErgebniseingangKonfiguration;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.jee.entity.EagerLoadingCollector;
import de.ivu.elect.jee.faces.FacesContextUtil;
import de.ivu.elect.jee.faces.PreConditionCheck;
import de.ivu.elect.jee.presentation.Icons;
import de.ivu.elect.jee.presentation.dynamicform.AdvancedDynaFormModel;
import de.ivu.elect.jee.presentation.dynamicform.DynamicFormSearchView;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.jee.presentation.dynamictable.AbstractColumnBuilder;
import de.ivu.elect.jee.presentation.redirect.Redirect;
import de.ivu.elect.jee.presentation.search.AbstractPaginationSearchView;
import de.ivu.elect.jee.presentation.security.StartViewResolver;
import de.ivu.elect.presentation.View;
import de.ivu.elect.presentation.gebietsbaum.GebietNummerFormatter;
import de.ivu.elect.presentation.gebietsbaum.tree.AbstractNoStoreGebietsbaumView;
import de.ivu.elect.util.ListSorter;

// NOTE, dass diese Klasse anhand des Templates
// /elect-common-jar/src/main/takatuka/templates/EntityPresentationAbstractSearchView.vm kopiert wurde!
public abstract class AbstractKumulierteStimmenSearchView<S extends AbstractKumulierteStimmenSearchParameters, R extends KumulierteStimmen> extends AbstractPaginationSearchView<R, S>
    implements
      DynamicFormSearchView,
      ViewHavingAuszaehlungsstand {

  private static final long serialVersionUID = 1L;

  @Inject
  private transient GebietsgruppierungExtendedDAO gebietsgruppierungExtendedDAO;

  @Inject
  private transient GruppeImGebietExtendedDAO gruppeImGebietExtendedDAO;

  @Inject
  private transient PolitischeGruppeExtendedDAO politischeGruppeExtendedDAO;

  @Inject
  private transient WahldurchfuehrungsgebietSessionSelect wahldurchfuehrungsgebietSessionSelect;

  @Inject
  private transient ErgebnisSessionSelect ergebnisSessionSelect;

  @Inject
  private transient ErgebnisAuszaehlungsstandErmittler ergebnisAuszaehlungsstandErmittler;

  @Inject
  private transient ErgebniseingangKonfiguration ergebniseingangKonfiguration;

  @Inject
  private transient AbstractNoStoreGebietsbaumView gebietsbaumView;

  @Inject
  private transient RequestHandlerAbstractKumulierteStimmenSearchView requestHandler;

  @Inject
  private transient ElectCommonPrincipal principal;

  @Inject
  private transient SystemGruppeEigenschaften systemGruppeEigenschaften;

  @Inject
  private transient StartViewResolver startViewResolver;

  @Inject
  private transient Icons icons;

  @Inject
  private transient GebietNummerFormatter gebietNummerFormatter;

  private transient List<PolitischeGruppe> politischeGruppen;

  private Date zeitpunktDerLetztenErgebnisBerechnung;

  private Long anzahlErfassungseinheitenGesamt;

  private Long anzahlErfassungseinheitenAusgezaehlt;

  private List<R> altesSearchResult;

  private AdvancedDynaFormModel searchParametersModel;

  protected AbstractKumulierteStimmenSearchView() {
    super();
  }

  @Nullable
  @PreConditionCheck
  public String checkPreCondition() {
    if (!isErgebnisSelected()) {
      return getStartViewId();
    }

    if (!isGebietSelected()) {
      return getStartViewId();
    }

    return null;
  }

  @Override
  public int getDefaultPageSize() {
    return getPageSizeMax();
  }

  protected String getStartViewId() {
    return startViewResolver.getStartViewId();
  }

  protected ElectCommonPrincipal getPrincipal() {
    return principal;
  }

  protected Benutzer getSelectedBenutzer() {
    return getPrincipal().getLogin();
  }

  protected String getSelectedBenutzerKennung() {
    return getPrincipal().getBenutzerKennung();
  }

  protected BenutzerEinsatzort getSelectedBenutzerEinsatzort() {
    return getPrincipal().getBenutzerEinsatzort();
  }

  protected String getSelectedBenutzerNameFormatted() {
    return getPrincipal().getFormattedName();
  }

  protected boolean hasPermission(UseCasePermission permission) {
    return getPrincipal().hasUseCasePermission(permission);
  }

  public boolean isErgebnisSelected() {
    return ergebnisSessionSelect.isSelected();
  }

  @Override
  public Ergebnis getErgebnis() {
    return ergebnisSessionSelect.getEntity();
  }

  @Override
  public String getErgebnisName() {
    Ergebnis ergebnis = getErgebnis();

    if (ergebnis == null) {
      return ""; // ELECT-8722
    }

    return ergebnis.getName();
  }

  public boolean isGebietSelected() {
    return wahldurchfuehrungsgebietSessionSelect.isSelected();
  }

  public Wahldurchfuehrungsgebiet getSelectedGebiet() {
    return wahldurchfuehrungsgebietSessionSelect.getEntity();
  }

  protected String getSelectedGebietNameFormatted() {
    return formatGebietName(getSelectedGebiet());
  }

  protected String formatGebietName(Gebiet gebiet) {
    return gebietNummerFormatter.getDarstellung(gebiet);
  }

  @Override
  public String getGebietName() {
    return getSelectedGebietNameFormatted();
  }

  public void onGebietSelectChangeEvent(@Observes(notifyObserver = Reception.IF_EXISTS) GebietSelectChangeEvent event) {
    Gebiet neueEntity = event.getNeueEntity();

    getSearchParameters().setGebietSelected(neueEntity);

    resetSearchResult();

    if (isRedirectNachGebietswechsel()) {
      if (neueEntity != null && getViewId().equals(FacesContextUtil.getCurrentViewId())) {
        gebietsbaumView.reset(); // explizit redirect, nicht nur teile neu laden, funktioniert leider nicht...

        FacesContextUtil.redirectToViewNoRedirectParameter(getViewId());
      }
    }
  }

  protected boolean isRedirectNachGebietswechsel() {
    return false; // true, wenn Fehler: primefaces.js.xhtml?ln=primefaces&loc=&v=5.2:1 Request return with error:parsererror.
  }

  public void onErgebnisSelectChangeEvent(@Observes(notifyObserver = Reception.IF_EXISTS) ErgebnisSelectChangeEvent event) {
    getSearchParameters().setErgebnisSelected(getErgebnis());

    resetSearchResult();
  }

  @Override
  public S getSearchParameters() {
    // Immer das aktuell ausgewählte Ergebnis setzen
    S result = getSearchParametersInternal();
    result.setErgebnisSelected(getErgebnis());

    return result;
  }

  protected abstract S getSearchParametersInternal();

  @Override
  public List<R> getSearchResult() {
    requestHandler.nothing(); // ELECT-248

    return super.getSearchResult();
  }

  @Override
  public void resetSearchResult() {
    politischeGruppen = null;

    anzahlErfassungseinheitenGesamt = null;
    anzahlErfassungseinheitenAusgezaehlt = null;

    zeitpunktDerLetztenErgebnisBerechnung = null;

    resetSearchParametersModel();

    List<R> searchResultComplete = getSearchResultComplete();

    if (searchResultComplete != null) {
      altesSearchResult = searchResultComplete;
    }

    super.resetSearchResult();
  }

  @Override
  protected List<R> runSearch() { // Cf. AbstractElectSearchView#runSearch!!!
    initSearchParametersIfNotInitialized();
    initSearchParametersByContext(); // NOTE ELECT-7671, den Context immer aktuell halten...

    List<R> result = callSearch();

    if (altesSearchResult != null) {
      result = ListSorter.sortiereErsteListeNachReihenfolgeZweiteListeWennZweiteListeAlleEintraegeDerErstenListeEnthaelt(result, altesSearchResult);

      altesSearchResult = null;
    }

    zeitpunktDerLetztenErgebnisBerechnung = getErgebnis().getZeitstempel();

    return result;
  }

  protected List<R> callSearch() {
    AbstractColumnBuilder<R> columnBuilder = getColumnBuilder();
    EagerLoadingCollector<R> eagerLoadingCollector = EagerLoadingCollector.create();

    if (columnBuilder != null) {
      columnBuilder.collectEagerLoadings(eagerLoadingCollector);
    }

    return getSearch().getSearchResult(getSearchParameters(), eagerLoadingCollector);
  }

  protected abstract AbstractKumulierteStimmenSearch<S, R> getSearch();

  @Nullable
  protected abstract AbstractColumnBuilder<R> getColumnBuilder();

  public Date getZeitpunktDerLetztenErgebnisBerechnung() {
    return zeitpunktDerLetztenErgebnisBerechnung;
  }

  protected boolean isWahlSelected() {
    return getSelectedWahl() != null;
  }

  @Nullable // ELECT-5583
  protected Wahl getSelectedWahl() {
    Gebiet gebiet = getSelectedGebiet();

    if (gebiet == null) {
      return null;
    }

    return gebiet.getWahl();
  }

  @Override
  public long getAnzahlErfassungseinheitenGesamt() {
    if (anzahlErfassungseinheitenGesamt == null) {
      anzahlErfassungseinheitenGesamt = berechneAnzahlErfassungseinheitenGesamt();
    }

    return anzahlErfassungseinheitenGesamt;
  }

  public long berechneAnzahlErfassungseinheitenGesamt() {
    if (!isGebietSelected() || !isWahlSelected()) {
      return 0;
    }

    if (getSelectedGebiet().isWurzelgebiet()) {
      return ergebnisAuszaehlungsstandErmittler.getAnzahlErfassungsgebieteFuerWurzelgebiet(getSelectedWahl());
    }

    return ergebnisAuszaehlungsstandErmittler.getAnzahlErfassungsgebieteFuerGebiet(getSelectedWahl(), getSelectedGebiet());
  }

  @Override
  public long getAnzahlErfassungseinheitenAusgezaehlt() {
    if (anzahlErfassungseinheitenAusgezaehlt == null) {
      anzahlErfassungseinheitenAusgezaehlt = berechneAnzahlErfassungseinheitenAusgezaehlt();
    }
    return anzahlErfassungseinheitenAusgezaehlt;
  }

  public long berechneAnzahlErfassungseinheitenAusgezaehlt() {
    if (!isGebietSelected() || !isErgebnisSelected()) {
      return 0;
    }

    if (getSelectedGebiet().isWurzelgebiet()) {
      return ergebnisAuszaehlungsstandErmittler.getAnzahlGueltigeErgebniseingaengeFuerErgebnisUndWurzelgebiet(getErgebnis());
    }

    return ergebnisAuszaehlungsstandErmittler.getAnzahlGueltigeErgebniseingaengeFuerErgebnisUndGebiet(getErgebnis(), getSelectedGebiet());
  }

  @Override
  public String getStyleClassAuszaehlungsstand() {
    return icons.getIconStatus((double) getAnzahlErfassungseinheitenAusgezaehlt() / (double) getAnzahlErfassungseinheitenGesamt());
  }

  @Override
  protected void initSearchParametersByContext() {
    super.initSearchParametersByContext();

    getSearchParameters().setErgebnisSelected(getErgebnis());
    getSearchParameters().setGebietSelected(getSelectedGebiet());
  }

  @Override
  public AdvancedDynaFormModel getSearchFormModel() {
    if (searchParametersModel == null) {
      searchParametersModel = createSearchParametersModel();
    }

    return searchParametersModel;
  }

  private AdvancedDynaFormModel createSearchParametersModel() {
    initSearchParametersIfNotInitialized();

    FormBuilder formBuilder = new FormBuilder();
    addProperties(formBuilder);

    return formBuilder.createModel();
  }

  @Override
  public void addProperties(FormBuilder formBuilder) {
    // Do nothing.
  }

  @Override
  public void resetSearchParametersModel() {
    searchParametersModel = null;
  }

  @Override
  @Redirect
  public String reset() {
    resetSearchParametersModel();

    // NOTE ELECT-3775 und RequestHandlerAbstractKumulierteStimmenSearchView.init(), dass ansonsten das Suchergebnis nicht
    // zurückgesetzt wird!
    zeitpunktDerLetztenErgebnisBerechnung = null;

    return super.reset();
  }

  protected List<PolitischeGruppe> getPolitischeGruppen() {
    if (politischeGruppen == null) {
      politischeGruppen = new ArrayList<>();

      if (isGebietSelected()) { // ELECT-3959
        politischeGruppen.addAll(findPolitischeGruppen(getSelectedGebiet()));
      }
    }

    return politischeGruppen;
  }

  protected List<PolitischeGruppe> findPolitischeGruppen(Gebiet gebiet) {
    return PolitischeGruppe.getPolitischeGruppenAufGebietSortiertNachPosition(gebiet, politischeGruppeExtendedDAO, gruppeImGebietExtendedDAO);
  }

  protected void resetPolitischeGruppen() {
    politischeGruppen = null;
  }

  @Override
  public boolean isSupportsLazyLoading() {
    return true;
  }

  @Override
  public boolean isRendered() {
    return super.isRendered() && isSearchResultAufDiesemGebietAnzeigen();
  }

  @Override
  public void setSearchReady() {
    resetSearchResult(); // nochmal alles neu...

    super.setSearchReady();
  }

  protected boolean isSearchResultAufDiesemGebietAnzeigen() {
    return true;
  }

  public Stimmabgabeart getStimmabgabeart() {
    return getSearchParameters().getStimmabgabeart();
  }

  public EnumConverter getStimmabgabeartConverter() {
    return new EnumConverter(Stimmabgabeart.class);
  }

  public void setStimmabgabeart(Stimmabgabeart stimmabgabeart) {
    if (isStimmabgabeartChanged(stimmabgabeart)) {
      resetSearchResult();
    }

    getSearchParameters().setStimmabgabeart(stimmabgabeart);
  }

  private boolean isStimmabgabeartChanged(Stimmabgabeart stimmabgabeart) {
    return !getSearchParameters().getStimmabgabeart().equals(stimmabgabeart);
  }

  public Ergebnisart getErgebnisartFuerDifferenz() {
    return getSearchParameters().getErgebnisartFuerDifferenz();
  }

  public void setErgebnisartFuerDifferenz(Ergebnisart ergebnisartFuerDifferenz) {
    if (isErgebnisartFuerDifferenzChanged(ergebnisartFuerDifferenz)) {
      resetSearchResult();
    }

    getSearchParameters().setErgebnisartFuerDifferenz(ergebnisartFuerDifferenz);
  }

  private boolean isErgebnisartFuerDifferenzChanged(Ergebnisart ergebnisartFuerDifferenz) {
    return !Objects.equals(getSearchParameters().getErgebnisartFuerDifferenz(), ergebnisartFuerDifferenz);
  }

  public EnumConverter getErgebnisartConverter() {
    return new EnumConverter(Ergebnisart.class);
  }

  public List<Stimmabgabeart> getStimmabgabearten() {
    return asList(Stimmabgabeart.values());
  }

  public List<Ergebnisart> getErgebnisartenFuerDifferenz() {
    List<Ergebnisart> result = new ArrayList<>();

    Ergebnis ergebnis = getErgebnis();

    if (ergebnis != null) {
      Wahl wahl = ergebnis.getWahl();
      Ergebnisart ergebnisart = ergebnis.getErgebnisart();

      if (ergebnisart.isVorlaeufig() && ergebniseingangKonfiguration.isErgebniseingangsartEndgueltigUnterstuetzt(wahl)) {
        result.add(Ergebnisart.ENDGUELTIG);
      }

      if (ergebnisart.isEndgueltig() && ergebniseingangKonfiguration.isErgebniseingangsartVorlaeufigUnterstuetzt(wahl)) {
        result.add(Ergebnisart.VORLAEUFIG);
      }
    }

    result.add(Ergebnisart.VORPERIODE); // Falls Ergebnis vorhanden, dann Ergebnisart als erste Option...

    return result;
  }

  public Gebietsgruppierung getGebietsgruppierung() {
    return getSearchParameters().getGebietsgruppierung();
  }

  public void setGebietsgruppierung(Gebietsgruppierung gebietsgruppierung) {
    if (isGebietsgruppierungChanged(gebietsgruppierung)) {
      resetSearchResult();
    }

    getSearchParameters().setGebietsgruppierung(gebietsgruppierungExtendedDAO.attach(gebietsgruppierung));
  }

  private boolean isGebietsgruppierungChanged(Gebietsgruppierung gebietsgruppierung) {
    return !getSearchParameters().hasGebietsgruppierung() && gebietsgruppierung != null
        || getSearchParameters().hasGebietsgruppierung() && !getSearchParameters().getGebietsgruppierung().equals(gebietsgruppierung);
  }

  public List<Gebietsgruppierung> getGebietsgruppierungen() {
    Wahl wahl = getSelectedWahl();

    if (wahl == null) {
      return Collections.emptyList();
    }

    return gebietsgruppierungExtendedDAO.findByWahl(wahl).stream() //
        .filter(gebietsgruppierung -> gebietsgruppierung.getGebiete().size() > 1) // ELECT-7322
        .sorted(Comparator.comparing(Gebietsgruppierung::getPosition)) //
        .collect(Collectors.toList());
  }

  protected SystemGruppeEigenschaften getSystemGruppeEigenschaften() {
    return systemGruppeEigenschaften;
  }

  @RequestScoped
  //  Das Suchergebnis soll nur einmal pro Request geladen werden, daher gibt es dieses "Konstrukt" aus "SearchView" und "RequestHandlerAbstract"
  public static class RequestHandlerAbstractKumulierteStimmenSearchView {

    @View
    @Inject
    private transient Instance<AbstractKumulierteStimmenSearchView> searchViews;

    @Inject
    private ErgebnisSessionSelect ergebnisSelect;

    @PostConstruct
    public void init() {
      // Daten nur löschen, wenn sich der Zeitpunkt der letzten Ergebnisberechnung geändert hat

      if (ergebnisSelect.isSelected()) {
        Date zeitstempelBerechnungAktuellesErgebnis = ergebnisSelect.getEntity().getZeitstempel();

        for (AbstractKumulierteStimmenSearchView searchView : searchViews) {
          Date zeitpunktDerLetztenErgebnisBerechnungDesViews = searchView.getZeitpunktDerLetztenErgebnisBerechnung();

          if (zeitpunktDerLetztenErgebnisBerechnungDesViews == null
              || zeitstempelBerechnungAktuellesErgebnis != null && zeitpunktDerLetztenErgebnisBerechnungDesViews.before(zeitstempelBerechnungAktuellesErgebnis)) {
            searchView.resetSearchResult();
          }
        }
      }
    }

    public void nothing() {
      // nichts zu tun
    }
  }
}
