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

import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.function.BiFunction;

import javax.annotation.Nullable;
import javax.el.MethodExpression;
import javax.faces.context.FacesContext;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.jee.cdi.BeanManagerUtil;
import de.ivu.elect.jee.entity.AbstractEagerLoading;
import de.ivu.elect.jee.entity.EagerLoading;
import de.ivu.elect.jee.exception.SystemExceptionWriter;
import de.ivu.elect.jee.faces.FacesMessageUtil;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.util.ClassUtil;

public abstract class AbstractBaseColumnEntry<E> implements ColumnEntry<E> {

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

  private static final long serialVersionUID = 1L;

  private static final Map<ColumnType, String> SORT_BY_VALUE_COMPARATOR_COMPONENT_NAME_PER_COLUMN_TYPE = new EnumMap<>(ColumnType.class);

  private String nullValueOutput = null;

  static {
    SORT_BY_VALUE_COMPARATOR_COMPONENT_NAME_PER_COLUMN_TYPE.put(ColumnType.STRING, ColumnEntrySortByValueStringComparator.COMPONENT_NAME);
  }

  private BiFunction<E, Object, Object> displayFilter = (e, o) -> o;

  private final Messages messages;

  private final Map<E, Object> displayValueMap = new WeakHashMap<>();

  public AbstractBaseColumnEntry(Messages messages) {
    super();

    this.messages = messages;
  }

  @Nullable
  @Override
  public Object getDisplayValue(E entity) {
    if (entity != null) {
      return displayValueMap.computeIfAbsent(entity, ignore -> filterDisplayValue(entity, getValueNoExceptio(entity)));
    }

    return null;
  }

  protected Object filterDisplayValue(E entity, Object value) {
    return displayFilter.apply(entity, value);
  }

  public void setDisplayFilter(BiFunction<E, Object, Object> displayFilter) {
    this.displayFilter = displayFilter;
  }

  @Nullable
  protected Object getValueNoExceptio(E entity) {
    try {
      return getValue(entity);
    } catch (Exception e){
      handleException(entity, e); // ELECT-12469
    }
    return null;
  }

  public abstract Object getValue(E entity);

  @Override
  public boolean hasSortByValueComparator() {
    return SORT_BY_VALUE_COMPARATOR_COMPONENT_NAME_PER_COLUMN_TYPE.containsKey(getType());
  }

  @Nullable
  @Override
  public MethodExpression getSortByValueComparatorMethodExpression() {
    if (hasSortByValueComparator()) {
      return createSortByValueComparatorMethodExpression(SORT_BY_VALUE_COMPARATOR_COMPONENT_NAME_PER_COLUMN_TYPE.get(getType()));
    }

    // Default value.
    return null;
  }

  // Nerviger, aber notwendiger, Workaround, um das Attribut 'sortFunction' im XHTML-Template basicColumns.xhtml nur bedingt
  // per <f:attribute /> setzen zu können, cf. ELECTMUC-1495.
  private MethodExpression createSortByValueComparatorMethodExpression(String componentName) {
    FacesContext facesContext = FacesContext.getCurrentInstance();

    return facesContext.getApplication().getExpressionFactory().createMethodExpression(facesContext.getELContext(), "#{" + componentName + ".compare}", Integer.class,
        new Class[] {Object.class, Object.class});
  }

  @Nullable
  @Override
  public Object getSortByValue(E entity) {
    Object displayValue = getDisplayValue(entity);

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

    if (isTypePresentCollection()) {
      if (!((Collection) displayValue).isEmpty()) {
        return ((Collection) displayValue).iterator().next();
      }

      // ELECT-4071 nicht die leere Collection zurückgeben, diese implementiert Comparable nicht
      return null;
    }

    return displayValue;
  }

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

  @Nullable
  @Override
  public Integer getWidth() {
    return null;
  }

  @Nullable
  @Override
  public String getStyle() {
    return null;
  }

  @Override
  public String getStyleClass(E entity) {
    return "";
  }

  @Override
  public String getStyleClass() {
    return getPseudoStyleClass();
  }

  /**
   * NOTE, dass dieser Pseudo-Style erwartet wird, um eine Spalte typsicher per CSS-Rule oder XPath finden zu können!
   */
  private String getPseudoStyleClass() {
    return ClassUtil.getClassSimpleName(getClass());
  }

  @Override
  public boolean isIconPresent() {
    return false;
  }

  @Override
  public boolean isIconRendered(E entity) {
    return false;
  }

  @Nullable
  @Override
  public String getIcon(E entity) {
    return null;
  }

  @Nullable
  @Override
  public String getIconTitle(E entity) {
    return null;
  }

  @Override
  @Nullable
  public String getIconStyleClass(E entity) {
    return null;
  }

  @Override
  @Nullable
  public String getIconStyle(E entity) {
    return null;
  }

  @Override
  public String getTooltip(E entity) {
    return "";
  }

  private boolean isColumnTypePresent(ColumnType ct) {
    return ct.equals(getType());
  }

  @Override
  public boolean isTypePresentString() {
    return isColumnTypePresent(ColumnType.STRING);
  }

  @Override
  public boolean isTypePresentBild() {
    return isColumnTypePresent(ColumnType.BILD);
  }

  @Override
  public boolean isTypePresentLong() {
    return isColumnTypePresent(ColumnType.LONG);
  }

  @Override
  public boolean isTypePresentDecimalOneFractionDigit() {
    return isColumnTypePresent(ColumnType.DECIMAL_ONE_FRACTION_DIGIT);
  }

  @Override
  public boolean isTypePresentDecimalOneFractionDigitPercentage() {
    return isColumnTypePresent(ColumnType.DECIMAL_ONE_FRACTION_DIGIT_PERCENTAGE);
  }

  @Override
  public boolean isTypePresentDecimalOneFractionDigitPercentagePoints() {
    return isColumnTypePresent(ColumnType.DECIMAL_ONE_FRACTION_DIGIT_PERCENTAGE_POINTS);
  }

  @Override
  public boolean isTypePresentDecimalTwoFractionDigits() {
    return isColumnTypePresent(ColumnType.DECIMAL_TWO_FRACTION_DIGITS);
  }

  @Override
  public boolean isTypePresentDecimalTwoFractionDigitsPercentage() {
    return isColumnTypePresent(ColumnType.DECIMAL_TWO_FRACTION_DIGITS_PERCENTAGE);
  }

  @Override
  public boolean isTypePresentDecimalTwoFractionDigitsPercentagePoints() {
    return isColumnTypePresent(ColumnType.DECIMAL_TWO_FRACTION_DIGITS_PERCENTAGE_POINTS);
  }

  @Override
  public boolean isTypePresentDecimalThreeFractionDigits() {
    return isColumnTypePresent(ColumnType.DECIMAL_THREE_FRACTION_DIGITS);
  }

  @Override
  public boolean isTypePresentDecimalThreeFractionDigitsPercentage() {
    return isColumnTypePresent(ColumnType.DECIMAL_THREE_FRACTION_DIGITS_PERCENTAGE);
  }

  @Override
  public boolean isTypePresentDecimalThreeFractionDigitsPercentagePoints() {
    return isColumnTypePresent(ColumnType.DECIMAL_THREE_FRACTION_DIGITS_PERCENTAGE_POINTS);
  }

  @Override
  public boolean isTypePresentBoolean() {
    return isColumnTypePresent(ColumnType.BOOLEAN);
  }

  @Override
  public boolean isTypePresentDate() {
    return isColumnTypePresent(ColumnType.DATE);
  }

  @Override
  public boolean isTypePresentTimestamp() {
    return isColumnTypePresent(ColumnType.TIMESTAMP);
  }

  @Override
  public boolean isTypePresentTime() {
    return isColumnTypePresent(ColumnType.TIME);
  }

  @Override
  public boolean isTypePresentEnum() {
    return isColumnTypePresent(ColumnType.ENUM);
  }

  @Override
  public boolean isTypePresentCollection() {
    return isColumnTypePresent(ColumnType.COLLECTION);
  }

  @Override
  public boolean isTypePresentColor() {
    return isColumnTypePresent(ColumnType.COLOR);
  }

  @Override
  public @Nullable
  EagerLoading<E> getEagerLoading() {
    return DUMMY_EAGER_LOADING;
  }

  private boolean exceptionLogged;

  private final EagerLoading<E> DUMMY_EAGER_LOADING = new AbstractEagerLoading<E>() {

    @Override
    public E eagerLoad(E entity) {
      AbstractBaseColumnEntry<E> instance = AbstractBaseColumnEntry.this;
      instance.getValueNoExceptio(entity); // NICHT AbstractBaseColumnEntry#getDisplayValue, da dieser Wert gecached wird!
      try {
        instance.getTooltip(entity);
      } catch (Exception e) {
        // ELECT-12469
        handleException(entity, e);
      }
      try {
        instance.getIcon(entity);
      } catch (Exception e) {
        // ELECT-12469
        handleException(entity, e);
      }
      return entity;
    }
  };

  protected void handleException(E entity, Throwable e) {
    if (exceptionLogged) {
      return;
    }
    exceptionLogged = true;
    String message = "Fehler in: " + getClass() + " für Entity: " + entity + ", " + e.getMessage();
    try {
      LOG.error(message, e);
    } catch (Exception ex) {
      LOG.error("Fehler: " + e.getMessage(), e);
    }
    try {
      FacesMessageUtil.addErrorMessage("column.value.error", getClass(), entity, e.getMessage());
    } catch (Exception ex) {
      LOG.error("Fehler: " + e.getMessage(), e);
    }

    try {
      BeanManagerUtil.getBeanByClass(SystemExceptionWriter.class).writeSystemException(message, e);
    } catch (Exception ex) {
      LOG.error("Fehler: " + e.getMessage(), e);
    }
  }

  @Override
  public String getName() {
    return getClass().getName();
  }

  protected String getMessage(Enum<?> enumValue) {
    return getMessages().lokalisiere(enumValue);
  }

  protected String getMessage(String key, Object... params) {
    return getMessages().getMessageForMessageKey(key, params);
  }

  public Messages getMessages() {
    return messages;
  }

  @Override
  public boolean isSummeImExportAusgeben() {
    return isTypePresentLong(); // ELECTMUC-1652
  }

  @Nullable
  @Override
  public String getNullValueOutput() {
    return nullValueOutput;
  }

  public void setNullValueOutput(String nullValueOutput) {
    this.nullValueOutput = nullValueOutput;
  }

  @Override
  public boolean isNullValueOutputPresent() {
    return StringUtils.isNotBlank(getNullValueOutput());
  }
}
