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

import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.util.List;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.faces.convert.Converter;

public abstract class AbstractAutoCompleteProperty<T, V> extends AbstractProperty {

  protected static final int UNLIMITED = 1000000;

  protected static final String PLACEHOLDER_MSG_KEY_PREFIX = "component.autocomplete.placeholder.";

  protected static final String STYLE_CLASS_UI_AUTOCOMPLETE_WIDE = "ui-autocomplete-wide";

  protected static final String STYLE_CLASS_UI_AUTOCOMPLETE_DROPDOWN = "ui-autocomplete-dropdown";

  private final AbstractAutoCompleteOptionsProvider<T> optionsProvider;

  // NOTE ELECT-2751, dass dieser Wert die zuletzt eingegebenen Zeichen, zu denen ggf. noch keine Option ausgewählt wurde, speichert.
  private String recentQuery;

  public AbstractAutoCompleteProperty(AbstractAutoCompleteOptionsProvider<T> optionsProvider) {
    super();

    this.optionsProvider = optionsProvider;
  }

  @Override
  public PropertyType getDefaultType() {
    return PropertyType.AUTO_COMPLETE;
  }

  @Override
  public Converter getDefaultConverter() {
    return optionsProvider.getConverter();
  }

  @Override
  public String getDefaultLabel() {
    return getMessage(getDefaultLabelMsgKey());
  }

  protected abstract String getDefaultLabelMsgKey();

  public abstract boolean isMultipleEnabled();

  public abstract boolean isDropdownEnabled();

  public abstract boolean isCacheEnabled();

  // NOTE, dass in /elect-base-war/src/main/webapp/WEB-INF/templates/dyna-form.xhtml immer <p:autoComplete forceSelection="false" /> gesetzt ist,
  // da ansonsten die zuletzt eingegebenen Zeichen vor dem Submit automatisch gelöscht werden. Dieser Wert steuert, ob beim Versuch, die zuletzt
  // eingegebenen Zeichen zu interpretieren, eine existierende (gefilterte) Option gefunden werden muss.
  protected abstract boolean isForceOptionSelection();

  public abstract int getMaxQueryLength();

  public abstract int getMinQueryLength();

  public String getRecentQuery() {
    return recentQuery;
  }

  public void setRecentQuery(String query) {
    this.recentQuery = query;
  }

  public abstract V getDefaultValue();

  public V getValue() {
    return getValueInternal();
  }

  protected abstract V getValueInternal();

  public void setValue(V selection) {
    String query = getRecentQuery();

    if (isNotBlank(query)) {
      setValueInternalAndHandleNonBlankRecentQuery(selection, query);
    } else {
      setValueInternal(selection);
    }
  }

  protected abstract void setValueInternal(V selection);

  private void setValueInternalAndHandleNonBlankRecentQuery(V selection, String recentQuery) {
    T optionByRecentQuery = tryCreateOptionByQuery(recentQuery);

    if (nonNull(optionByRecentQuery)) {
      setValueInternal(handleNonNullOptionByRecentQuery(selection, optionByRecentQuery));
    } else {
      setValueInternal(selection);
    }
  }

  protected abstract V handleNonNullOptionByRecentQuery(V selection, T optionByRecentQuery);

  public String getValueLabel(@Nullable T value) {
    if (value == null) {
      return "";
    }

    return formatValueLabel(value);
  }

  protected String formatValueLabel(@Nonnull T value) {
    return formatOptionLabel(value);
  }

  protected boolean hasOptions() {
    return !getOptions().isEmpty();
  }

  protected List<T> getOptions() {
    return optionsProvider.getOptions();
  }

  public List<T> getOptionsFiltered(String currentQuery) {
    List<T> optionsFilteredByCurrentQuery = getOptionsFilteredInternal(currentQuery);

    if (!isForceOptionSelection()) {
      T optionByCurrentQuery = tryCreateOptionByQuery(currentQuery);

      if (nonNull(optionByCurrentQuery) && !optionsFilteredByCurrentQuery.contains(optionByCurrentQuery)) {
        optionsFilteredByCurrentQuery.add(0, optionByCurrentQuery);
      }
    }

    return optionsFilteredByCurrentQuery;
  }

  protected List<T> getOptionsFilteredInternal(String query) {
    int maxOptionsFilteredCount = getMaxOptionsReturnedCount();

    List<T> optionsFiltered = optionsProvider.streamOptionsFiltered(query, !isDropdownEnabled()).stream().limit(maxOptionsFilteredCount + 1).collect(Collectors.toList());
    int optionsFilteredCount = optionsFiltered.size();

    if (optionsFilteredCount > maxOptionsFilteredCount) {
      handleMaxOptionsReturnedCountExceeded(optionsFilteredCount);
    }

    return optionsFiltered.subList(0, Math.min(optionsFilteredCount, maxOptionsFilteredCount));
  }

  protected int getMaxOptionsReturnedCount() {
    return UNLIMITED;
  }

  protected void handleMaxOptionsReturnedCountExceeded(int optionsFilteredCount) {
    // Do nothing, cf. de.ivu.elect.presentation.person.form.BerufAutoCompleteProperty
  }

  public abstract int getMaxOptionsSelectedCount();

  private T tryCreateOptionByQuery(String query) {
    return optionsProvider.tryGetOptionByQuery(query);
  }

  public String getOptionLabel(@Nullable T option) {
    if (option == null) {
      return "";
    }

    return formatOptionLabel(option);
  }

  protected abstract String formatOptionLabel(@Nonnull T option);

  public String getPlaceholder() {
    if (hasOptions()) {
      return getMessage(PLACEHOLDER_MSG_KEY_PREFIX + "default", getMinQueryLength());
    }

    return getMessage(PLACEHOLDER_MSG_KEY_PREFIX + "empty");
  }

  public String getStyleClasses() {
    StringJoiner styleClasses = new StringJoiner(" ");

    if (isDropdownEnabled()) {
      styleClasses.add(STYLE_CLASS_UI_AUTOCOMPLETE_DROPDOWN);
    }

    return styleClasses.toString();
  }
}
