
package de.ivu.elect.jee.presentation.search;

import static java.util.Arrays.stream;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;

import org.primefaces.event.ToggleEvent;
import org.primefaces.model.Visibility;
import org.primefaces.model.menu.MenuModel;
import org.slf4j.LoggerFactory;

import de.ivu.elect.jee.presentation.redirect.Redirect;
import de.ivu.elect.jee.presentation.view.AbstractView;

public abstract class AbstractSearchView<T, SP extends AbstractSearchParameters> extends AbstractView {

  private static final long serialVersionUID = 1L;

  private transient WeakReference<List<T>> searchResultComplete = new WeakReference<>(null);

  private boolean searchReady = false;

  private T searchResultSelection;

  private Set<String> updatesOnSearchResultLoaded;

  public AbstractSearchView() {
    super();
  }

  @PostConstruct
  public void init() {
    initSearchParametersIfNotInitialized();

    if (isSupportsLazyLoading() && stream(getClass().getAnnotations()).anyMatch(annotation -> annotation != null && RequestScoped.class.equals(annotation.annotationType()))) {
      LoggerFactory.getLogger("ELECT-8409").warn("FOUND lazy loading view annotated with @RequestScoped [view = " + getViewId() + ", class = " + getClass() + "]");
    }
  }

  protected void initSearchParametersIfNotInitialized() {
    if (hasSearchParameters()) {
      SP searchParameters = getSearchParameters();

      if (!searchParameters.isInitialized()) {
        initSearchParametersByDefaults();
        initSearchParametersByPrincipal();
        initSearchParametersByContext();

        initSearchParametersCollapsed();

        searchParameters.setInitialized(true);
      }
    }
  }

  /**
   * Initialisiert alle veränderbaren Standardsuchparameter, die ggf. noch durch AbstractSearchView#initSearchParametersByPrincipal und AbstractSearchView#initSearchParametersByContext überschrieben
   * werden.
   */
  protected void initSearchParametersByDefaults() {
    // To be overridden...
  }

  /**
   * Initialisiert alle veränderbaren Suchparameter, abhängig vom aktuellen Benutzer, also bspw. eine gespeicherte Standardsuchvorlage.
   */
  protected void initSearchParametersByPrincipal() {
    // To be overridden...
  }

  /**
   * Initialisiert alle NICHT veränderbaren Suchparameter, die IMMER abhängig vom aktuellen Context gesetzt sein müssen, also bspw. die aktuelle Wahl oder den aktuellen Wahltermin.
   */
  protected void initSearchParametersByContext() {
    // To be overridden...
  }

  protected void initSearchParametersCollapsed() {
    setSearchParametersCollapsed(formBuilderConfig.isSearchFormBuilderPropertiesHiddenOnPageLoad());
  }

  public boolean isSearchResultSelected() {
    return getSearchResultSelection() != null;
  }

  public T getSearchResultSelection() {
    return searchResultSelection;
  }

  public void setSearchResultSelection(T searchResultSelection) {
    this.searchResultSelection = searchResultSelection;
  }

  public boolean isBtnSearchRendered() {
    return true;
  }

  public String getBtnSearchLabel() {
    return getMessage("entity.search.action.apply");
  }

  @Redirect
  public String search() {
    resetSearchResult();

    getSearchParameters().resetFirst();

    return getViewId();
  }

  public boolean isSearchPartialUpdateSupported() {
    return true;
  }

  public void searchPartialUpdate() {
    search();
    setSearchReady(); // Partial-Update lädt schon lazy
  }

  public boolean isBtnResetRendered() {
    return true;
  }

  @Redirect
  public String reset() {
    getSearchParameters().reset();
    initSearchParametersIfNotInitialized();
    setSearchNotReady();

    return getViewId();
  }

  public boolean isResetPartialUpdateSupported() {
    return true;
  }

  public void resetPartialUpdate() {
    reset();
    resetSearchResult();
    setSearchReady(); // Partial-Update lädt schon lazy
  }

  protected abstract String getViewId();

  public boolean isSearchParametersRendered() {
    return hasSearchParameters();
  }

  public boolean isSearchParametersCollapsed() {
    return isSearchParametersRendered() && isSearchParametersToggleable() && getSearchParameters().isCollapsed();
  }

  protected void setSearchParametersCollapsed(boolean searchParametersCollapsed) {
    getSearchParameters().setCollapsed(searchParametersCollapsed);
  }

  public void onSearchParametersToggled(ToggleEvent event) {
    if (isSearchParametersRendered()) {
      setSearchParametersCollapsed(nonNull(event) && Visibility.HIDDEN.equals(event.getVisibility()));
    }
  }

  public boolean isSearchParametersToggleable() {
    return true;
  }

  protected boolean hasSearchParameters() {
    return getSearchParameters() != null;
  }

  public String getSearchParametersHeader() {
    return getMessage("component.filter.panel.header");
  }

  public abstract SP getSearchParameters();

  public List<T> getSearchResult() {
    if (isSearchReady()) {
      List<T> result = searchResultComplete.get();

      if (result == null) {
        result = runSearch();
        searchResultComplete = new WeakReference<>(result);
      }
      return result;
    }

    return new ArrayList<>();
  }

  protected List<T> getSearchResultComplete() {
    return searchResultComplete.get();
  }

  protected abstract List<T> runSearch();

  protected void resetSearchResult() {
    searchResultComplete = new WeakReference<>(null);

    setSearchNotReady();
  }

  public boolean isSupportsLazyLoading() {
    return false;
  }

  public boolean isSearchReady() {
    return !isSupportsLazyLoading() || searchReady; // immer wahr, wenn lazy-loading nicht unterstützt wird...
  }

  public void setSearchReady() {
    resetSearchResult(); // Beim schnellen Wechsel im Gebietsbaum kann es passieren, dass parallel bereits auf einem anderen Gebiet gesucht wurde => löschen
    searchReady = true;
  }

  public boolean isSearchNotReady() {
    return !isSearchReady();
  }

  protected void setSearchNotReady() {
    searchReady = false;
  }

  public boolean isAutoRun() {
    return isSupportsLazyLoading() && !isSearchReady();
  }

  public String getEmptyMessage() {
    if (isSearchReady()) {
      return getMessage("component.data.table.empty.message");
    }

    return null;
  }

  public boolean isRendered() {
    return isSearchReady();
  }

  public boolean isOptionMenuRendered() {
    return false;
  }

  @Nullable
  public MenuModel getOptionMenu() {
    return null;
  }

  @Override
  public boolean isBtnGoBackRendered() {
    return false; // Suchen haben 'normaler Weise' keinen Zurück-Knopf
  }

  protected boolean isEmptySearchResultDisplayed() {
    if (isSearchReady()) {
      return getSearchResult().isEmpty();
    }

    return true;
  }

  public void addUpdateOnSearchResultLoaded(String clientId) {
    getUpdatesOnSearchResultLoaded().add(clientId);
  }

  public Collection<String> getUpdatesOnSearchResultLoaded() {
    if (isNull(updatesOnSearchResultLoaded)) {
      updatesOnSearchResultLoaded = new HashSet<>();
    }

    return updatesOnSearchResultLoaded;
  }

  // NOTE, dass diese Methode per <p:remoteCommand /> aus dem XHTML-Code in searchTable.xhtml aufgerufen wird!
  public void onSearchResultLoaded() {
    ajaxHelper.update(getUpdatesOnSearchResultLoaded());
    ajaxHelper.updateMessagesDeferred(); // ELECT-7118
  }

  public TableExportMetaData getTableExportMetaData() {
    return new TableExportMetaData();
  }
}
