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

import static java.util.Arrays.asList;
import static org.apache.commons.lang3.StringUtils.normalizeSpace;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.faces.application.FacesMessage;
import javax.faces.validator.LengthValidator;
import javax.faces.validator.Validator;

import de.ivu.elect.util.ClassUtil;

public class FormData implements Serializable {

  private final Property property;

  private final List<FacesMessage> facesMessages;

  private final List<Validator> facesValidators;

  private final Map<String, Object> attributes;

  private final List< ? > selectableValues;

  private final boolean focused;

  public FormData(Property property, List<FacesMessage> facesMessages, List<Validator> facesValidators, Map<String, Object> attributes, List< ? > selectableValues, boolean focused) {
    super();

    this.property = property;

    this.facesMessages = facesMessages;
    this.facesValidators = facesValidators;

    if (property instanceof PropertyHavingMinAndMaxLength) {
      PropertyHavingMinAndMaxLength propertyHavingMinAndMaxLength = (PropertyHavingMinAndMaxLength) property;

      LengthValidator lengthValidator = new LengthValidator();
      lengthValidator.setMinimum(propertyHavingMinAndMaxLength.getMinLength());
      lengthValidator.setMaximum(propertyHavingMinAndMaxLength.getMaxLength());

      this.facesValidators.add(lengthValidator);
    }

    this.attributes = attributes;

    this.selectableValues = selectableValues;

    this.focused = focused;
  }

  public Property getProperty() {
    return property;
  }

  public boolean isObservingProperty() {
    return !getObservedProperties().isEmpty();
  }

  public Collection<Property> getObservedProperties() {
    return getProperty().getObservedProperties();
  }

  public boolean isObservedProperty() {
    return !getObservingProperties().isEmpty();
  }

  public Collection<Property> getObservingProperties() {
    return getProperty().getObservingProperties();
  }

  public List<FacesMessage> getFacesMessages() {
    return facesMessages;
  }

  public List<Validator> getFacesValidators() {
    return facesValidators;
  }

  public Map<String, Object> getAttributes() {
    return attributes;
  }

  public List< ? > getValues() {
    return selectableValues;
  }

  public String getStyleClass() {
    StringBuilder result = new StringBuilder();

    result.append(trimToEmpty(getProperty().getStyleClass()));
    result.append(" ");

    if (getProperty().isVisible()) {
      if (!getProperty().isAccessableOnPageLoad()) {
        result.append("ui-state-hidden-on-page-load");
        result.append(" ");
      }
    } else if (!getProperty().isRequired() || getProperty().isReadonly()) {
      result.append("ui-state-hidden");
      result.append(" ");
    }

    if (isProcessedOnObservedPropertyValueChanged()) {
      result.append(joinStyleClasses(collectPropertyIsObservedByStyleClasses(getObservingProperties())));
      result.append(" ");
    }

    result.append(joinStyleClasses(collectPropertyIsObservingStyleClasses(getObservedProperties())));
    result.append(" ");

    return normalizeSpace(result.toString());
  }

  private boolean isProcessedOnObservedPropertyValueChanged() {
    return isObservedProperty() && !getProperty().isReadonly();
  }

  private String joinStyleClasses(Collection<String> styleClasses) {
    StringBuilder result = new StringBuilder();

    for (String styleClass : styleClasses) {
      result.append(styleClass);
      result.append(" ");
    }

    return result.toString();
  }

  /*
   * ELECT-11891, ELECT-937, "p:ajax has built-in resetValues attribute to reset values of processed components during a request."
   */
  public boolean isObservedPropertyResetValues() {
    return true;
  }

  public String getObservedPropertyProcessSelector() {
    // Process all properties having a style class indicating that they share the same observing property.
    return createPropertyIsObservedBySelector(getProperty());
  }

  public static String createPropertyIsObservedBySelector(Property property) {
    return createStyleClassesSelector(collectPropertyIsObservedByStyleClasses(property.getObservingProperties()));
  }

  public String getObservedPropertyUpdateSelector() {
    // Update all properties having a style class indicating that they observe the current property.
    return createPropertyIsObservingSelector(getProperty());
  }

  public static String createPropertyIsObservingSelector(Property property) {
    return createStyleClassesSelector(asList(createPropertyIsObservingStyleClass(property)));
  }

  private static Collection<String> collectPropertyIsObservedByStyleClasses(Collection<Property> observingProperties) {
    Collection<String> result = new HashSet<>();

    addPropertyIsObservedByStyleClassesRecursive(result, observingProperties, new HashSet<>());

    return result;
  }

  // Cf. de.ivu.elect.jee.presentation.dynamicform.FormDataPropertyValueChangeListener.notifyAllRecursive(Property, Collection<Property>)
  private static void addPropertyIsObservedByStyleClassesRecursive(Collection<String> result, Collection<Property> observingProperties, Set<Property> visited) {
    observingProperties.forEach(observingProperty -> {

      result.add(createPropertyIsObservedByStyleClass(observingProperty));
      // ELECTMUC-2604
      if (visited.add(observingProperty)) {
        addPropertyIsObservedByStyleClassesRecursive(result, observingProperty.getObservingProperties(), visited);
      }
    });
  }

  private static Collection<String> collectPropertyIsObservingStyleClasses(Collection<Property> observedProperties) {
    Collection<String> result = new HashSet<>();

    addPropertyIsObservingStyleClassesRecursive(result, observedProperties, new HashSet<>());

    return result;
  }

  // Cf. de.ivu.elect.jee.presentation.dynamicform.FormDataPropertyValueChangeListener.notifyAllRecursive(Property, Collection<Property>)
  private static void addPropertyIsObservingStyleClassesRecursive(Collection<String> result, Collection<Property> observedProperties, Set<Property> visited) {
    observedProperties.forEach(observedProperty -> {
      result.add(createPropertyIsObservingStyleClass(observedProperty));
      // ELECTMUC-2604
      if (visited.add(observedProperty)) {
        addPropertyIsObservingStyleClassesRecursive(result, observedProperty.getObservedProperties(), visited);
      }
    });
  }

  private static String createPropertyIsObservedByStyleClass(Property property) {
    return createPropertyStyleClass("ui-state-observed-by", property);
  }

  private static String createPropertyIsObservingStyleClass(Property property) {
    return createPropertyStyleClass("ui-state-observing", property);
  }

  private static String createPropertyStyleClass(String styleClassPrefix, Property property) {
    return styleClassPrefix + "-" + ClassUtil.getClassSimpleName(property);
  }

  /*
   * NOTE that PFS (PrimeFaces Selectors) integrates jQuery Selector API with JSF component referencing model, so for partial update and process, referencing JSF components can be done using jQuery
   * Selector API instead of regular JSF model.
   *
   * Cf. http://api.jquery.com/category/selectors/
   */
  private static String createStyleClassesSelector(Collection<String> styleClasses) {
    StringBuilder result = new StringBuilder();

    if (!styleClasses.isEmpty()) {
      result.append("@(");

      for (Iterator<String> iter = styleClasses.iterator(); iter.hasNext();) {
        result.append(".");
        result.append(iter.next());

        if (iter.hasNext()) {
          result.append(", ");
        }

      }

      result.append(")");
    }

    // E.g. @(.ui-state-observed-by-PropertyA, .ui-state-observed-by-PropertyB)
    return result.toString();
  }

  public boolean isFocused() {
    return focused;
  }
}
