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

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
import static org.apache.commons.lang3.StringUtils.split;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

import javax.annotation.Nullable;
import javax.faces.application.FacesMessage;
import javax.faces.application.FacesMessage.Severity;
import javax.faces.convert.Converter;
import javax.faces.validator.Validator;

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

import de.ivu.elect.jee.faces.AbstractEntityFacesValidator;
import de.ivu.elect.jee.faces.Messages;

public class FormBuilder {

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

  public static final String DYNAMIC_ATTRIBUTES_KEY = "dynamicAttributes";

  private final List<FormBuilderProperty> formBuilderProperties;

  private final boolean formBuilderPropertiesHiddenOnPageLoad;

  private FormBuilderProperty formBuilderSeparatorEdited;

  private FormBuilderProperty formBuilderPropertyEdited;

  private FormBuilderProperty formBuilderPropertyFocused;

  private AdvancedDynaFormModel model;

  private String id;

  public FormBuilder() {
    this(false);
  }

  public FormBuilder(FormBuilderConfig formBuilderConfig) {
    this(formBuilderConfig != null && formBuilderConfig.isEditFormBuilderPropertiesHiddenOnPageLoad());
  }

  private FormBuilder(boolean formBuilderPropertiesHiddenOnPageLoad) {
    super();

    this.formBuilderProperties = new ArrayList<>();
    this.formBuilderPropertiesHiddenOnPageLoad = formBuilderPropertiesHiddenOnPageLoad;
  }

  public AdvancedDynaFormModel createModel() {
    if (model == null) {
      model = new AdvancedDynaFormModel(id);

      for (FormBuilderProperty formBuilderProperty : getFormBuilderProperties()) {
        formBuilderProperty.setVisibleOnPageLoad(!formBuilderPropertiesHiddenOnPageLoad || formBuilderProperty.isVisibleOnPageLoad()); // ELECTKOM-3

        model.addProperty(formBuilderProperty, isFocused(formBuilderProperty));
      }
    }

    return model;
  }

  public void resetModel() {
    model = null;
  }

  public FormBuilder addSeparator() {
    return addSeparator(Messages.LOCALIZED_MESSAGE_ONLY_MSG_KEY, "");
  }

  public FormBuilder addSeparator(String msgKey, Object... params) {
    return addProperty(new SeparatorPseudoProperty(msgKey, params));
  }

  protected FormBuilderProperty getLastFormBuilderProperty() {
    return formBuilderPropertyEdited;
  }

  public FormBuilder addProperty(Property property) {
    checkModelState();

    FormBuilderProperty formBuilderProperty = new FormBuilderProperty(property);
    formBuilderPropertyEdited = formBuilderProperty;

    if (property instanceof SeparatorPseudoProperty) {
      formBuilderSeparatorEdited = formBuilderProperty;
    } else {
      formBuilderPropertyEdited.addPropertyConnected(formBuilderSeparatorEdited);
    }

    getFormBuilderProperties().add(formBuilderProperty);

    Collection<Validator> validators = property.getValidators();

    for (Validator validator : validators) {
      withValidator(validator);
    }

    return this;
  }

  public FormBuilder after(Property otherProperty) {
    checkModelStateAndLastFormBuilderProperty();

    int otherPostion = getFormBuilderProperties().indexOf(getOtherFormBuilderProperty(otherProperty));

    getFormBuilderProperties().remove(getLastFormBuilderProperty());
    getFormBuilderProperties().add(otherPostion + 1, getLastFormBuilderProperty());

    return this;
  }

  public FormBuilder before(Property otherProperty) {
    checkModelStateAndLastFormBuilderProperty();

    int otherPostion = getFormBuilderProperties().indexOf(getOtherFormBuilderProperty(otherProperty));

    getFormBuilderProperties().remove(getLastFormBuilderProperty());
    getFormBuilderProperties().add(otherPostion, getLastFormBuilderProperty());

    return this;
  }

  public FormBuilder visibleOnPageLoad() {
    return withVisibleOnPageLoad(true);
  }

  public FormBuilder hiddenOnPageLoad() {
    return withVisibleOnPageLoad(false);
  }

  /**
   * @see {@link #visibleOnPageLoad()}/{@link #hiddenOnPageLoad()}
   */
  public FormBuilder withVisibleOnPageLoad(boolean visibleOnPageLoad) {
    checkModelStateAndLastFormBuilderProperty();

    FormBuilderProperty formBuilderProperty = getLastFormBuilderProperty();
    formBuilderProperty.setVisibleOnPageLoad(visibleOnPageLoad);

    if (!visibleOnPageLoad && formBuilderProperty.getProperty() instanceof AbstractProperty) {
      AbstractProperty property = (AbstractProperty) formBuilderProperty.getProperty();

      if (property.mustBeAccessableOnPageLoad()) {
        LOG.warn("FAILED to hide property on page load [" + "property = " + property.getClass().getSimpleName() + ", label = " + property.getLabel() + " readonly = " + property.isReadonly()
            + ", accessableOnPageLoadByRequired = " + property.mustBeAccessableOnPageLoadByRequired() + ", accessableOnPageLoadByValue = " + property.mustBeAccessableOnPageLoadByValue() + "]");
      }
    }

    return this;
  }

  public FormBuilder visible() {
    return withVisible(true);
  }

  public FormBuilder hidden() {
    return withVisible(false);
  }

  /**
   * @see {@link #visible()}/{@link #hidden()}
   */
  public FormBuilder withVisible(boolean visible) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setVisible(visible);
    getLastFormBuilderProperty().setVisibleOnPageLoad(visible);

    return this;
  }

  public FormBuilder required() {
    return withRequired(true);
  }

  public FormBuilder optional() {
    return withRequired(false);
  }

  /**
   * @see {@link #required()}/{@link #optional()}
   */
  public FormBuilder withRequired(boolean required) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setRequired(required);

    return this;
  }

  public FormBuilder readonly() {
    return withReadonly(true);
  }

  public FormBuilder writeable() {
    return withReadonly(false);
  }

  /**
   * @see {@link #readonly()}/{@link #writeable()}
   */
  public FormBuilder withReadonly(boolean readonly) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setReadonly(readonly);
    return this;
  }

  public FormBuilder withReadonly(Predicate<Property> readonly) {
    checkModelStateAndLastFormBuilderProperty();
    withReadonly(readonly.test(getLastFormBuilderProperty().getProperty()));
    return this;
  }

  public FormBuilder withWritable(Predicate<Property> writable) {
    return withReadonly(writable.negate());
  }

  public FormBuilder globalReadonly() {
    return withGlobalReadonly(true);
  }

  public FormBuilder globalWritable() {
    return withGlobalReadonly(false);
  }

  /**
   * @see {@link #globalReadonly()}/{@link #globalWritable()}
   */
  private FormBuilder withGlobalReadonly(boolean readonly) {
    checkModelState();

    for (FormBuilderProperty property : getFormBuilderProperties()) {
      property.setReadonly(readonly);
    }

    return this;
  }

  public FormBuilder withLabel(String label) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setLabel(label);
    return this;
  }

  public FormBuilder withKey(String key) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setKey(key);
    return this;
  }

  public FormBuilder withType(PropertyType type) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setType(type);
    return this;
  }

  public FormBuilder withMaxLength(int maxLength) {
    checkModelStateAndLastFormBuilderProperty();

    if (lastProperty() instanceof PropertyHavingMinAndMaxLength) {
      ((PropertyHavingMinAndMaxLength) lastProperty()).setMaxLength(maxLength);
    }

    return this;
  }

  public FormBuilder withMinLength(int minLength) {
    checkModelStateAndLastFormBuilderProperty();

    if (lastProperty() instanceof PropertyHavingMinAndMaxLength) {
      ((PropertyHavingMinAndMaxLength) lastProperty()).setMinLength(minLength);
    }

    return this;
  }

  public FormBuilder withInfoMessage(String detail) {
    return withMessage(FacesMessage.SEVERITY_INFO, "info", detail);
  }

  public FormBuilder withWarningMessage(String details) {
    return withMessage(FacesMessage.SEVERITY_WARN, "warn", details);
  }

  public FormBuilder withErrorMessage(String detail) {
    return withMessage(FacesMessage.SEVERITY_ERROR, "error", detail);
  }

  public FormBuilder withSuccessMessage(String detail) {
    // Um unsere OutputSeverity auf die FacesMessages abzubilden wird "fatal" als "success" missbraucht...
    return withMessage(FacesMessage.SEVERITY_FATAL, "success", detail);
  }

  private FormBuilder withMessage(Severity severity, String severityName, String detail) {
    return withMessage(severity, severityName, "", detail);
  }

  private FormBuilder withMessage(Severity severity, String severityName, String summary, String detail) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().addFacesMessage(severity, summary, detail);

    withStyleClass("having-msg-" + severityName); // ELECTVAPP-1719

    return this;
  }

  public FormBuilder withValidator(Validator facesValidator) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().addFacesValidator(facesValidator);

    return this;
  }

  public FormBuilder withConverter(Converter facesConverter) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setFacesConverter(facesConverter);

    return this;
  }

  public FormBuilder withAttribute(String key, Object value) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().addAttribute(key, value);

    return this;
  }

  public FormBuilder setValues(List< ? > selectableValues) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setValues(selectableValues);

    return this;
  }

  public boolean containsProperty(Property property) {
    return property != null && getFormBuilderProperties().contains(new FormBuilderProperty(property));
  }

  public FormBuilder selectProperty(Property property) {
    checkModelState();

    formBuilderPropertyEdited = getOtherFormBuilderProperty(property);

    return this;
  }

  public FormBuilder removeProperty(Property property) {
    checkModelState();

    getFormBuilderProperties().remove(new FormBuilderProperty(property));

    formBuilderPropertyEdited = null;

    return this;
  }

  @Deprecated // NICHT MEHR VERWENDEN(!) Properties lieber über Vererbung gleich richtig setzen
  public FormBuilder replaceProperty(Property property, Property replacement) {
    checkModelState();

    addProperty(replacement).after(property);
    removeProperty(property);

    selectProperty(replacement);

    return this;
  }

  public boolean containsPropertyByClass(Class< ? extends Property> clazz) {
    return findPropertyByClass(clazz) != null;
  }

  /**
   * Wählt die erste Property der angegebenen Klasse aus. Wird keine solche Property gefunden, so wird eine <code>IllegalStateException</code> geworfen.
   *
   * @param clazz
   * @return
   */
  @SuppressWarnings("rawtypes")
  public FormBuilder selectPropertyByClass(Class clazz) {
    checkModelState();

    boolean propertyFound = false;

    for (FormBuilderProperty formBuilderProperty : getFormBuilderProperties()) {
      if (formBuilderProperty.getProperty().getClass().equals(clazz)) {
        propertyFound = true;

        formBuilderPropertyEdited = formBuilderProperty;

        break;
      }
    }

    if (!propertyFound) {
      throw new IllegalStateException("Property of class not found: " + clazz);
    }

    return this;
  }

  /**
   * Entfernt alle Properties der angegebenen Klasse.
   *
   * @param clazz
   * @return
   */
  @SuppressWarnings("rawtypes")
  public FormBuilder removePropertyByClass(Class clazz) {
    checkModelState();

    for (int i = getFormBuilderProperties().size() - 1; i >= 0; i--) { // rückwärts wegen entfernen
      FormBuilderProperty formBuilderProperty = getFormBuilderProperties().get(i);

      if (formBuilderProperty.getProperty().getClass().equals(clazz)) {
        getFormBuilderProperties().remove(i);
      }
    }

    return this;
  }

  /**
   * Gibt die erste gefundene Property der angegebenen Klasse zurück. Wird keine solche Property gefunden, so wird <code>null</code> zurückgegeben.
   */
  @SuppressWarnings("unchecked")
  @Deprecated // Keine eigenen Klassen für Properties mehr verwenden
  public <T extends Property> T findPropertyByClass(Class<T> clazz) {
    for (FormBuilderProperty formBuilderProperty : getFormBuilderProperties()) {
      if (formBuilderProperty.getProperty().getClass().equals(clazz)) {
        return (T) formBuilderProperty.getProperty();
      }
    }

    return null;
  }

  protected List<FormBuilderProperty> getFormBuilderProperties() {
    return formBuilderProperties;
  }

  private boolean isModelCreated() {
    return model != null;
  }

  private void checkLastFormBuilderPropertySet() {
    if (getLastFormBuilderProperty() == null) {
      throw new IllegalStateException("Operation nicht möglich, kein Property vorhanden");
    }
  }

  private void checkModelState() {
    if (isModelCreated()) {
      throw new IllegalStateException("Modell already created");
    }
  }

  public Property lastProperty() {
    checkModelState();

    FormBuilderProperty formBuilderProperty = getLastFormBuilderProperty();

    if (formBuilderProperty != null) {
      return formBuilderProperty.getProperty();
    }

    return null;
  }

  private FormBuilderProperty getOtherFormBuilderProperty(Property property) {
    if (!containsProperty(property)) {
      throw new IllegalStateException("Property not found: " + property);
    }

    return getFormBuilderProperties().get(getFormBuilderProperties().indexOf(new FormBuilderProperty(property)));
  }

  public FormBuilder withValidatorAndPk(Validator facesValidator, Object pk) {
    return withValidator(facesValidator).withAttribute(AbstractEntityFacesValidator.ENTITY_PK_ATTRIBUTE_NAME, pk);
  }

  private void checkModelStateAndLastFormBuilderProperty() {
    checkModelState();
    checkLastFormBuilderPropertySet();
  }

  public FormBuilder withObservingProperties(Property... observingProperties) {
    if (observingProperties != null) {
      for (Property observingProperty : observingProperties) {
        withObservingProperty(observingProperty);
      }
    }

    return this;
  }

  public FormBuilder withObservingProperty(Property observingProperty) {
    return registerObservingPropertyForObservedProperty(lastProperty(), observingProperty);
  }

  public FormBuilder withObservedProperties(Property... observedProperties) {
    if (observedProperties != null) {
      for (Property observedProperty : observedProperties) {
        withObservedProperty(observedProperty);
      }
    }

    return this;
  }

  public FormBuilder withObservedProperty(Property observedProperty) {
    return registerObservingPropertyForObservedProperty(observedProperty, lastProperty());
  }

  private FormBuilder registerObservingPropertyForObservedProperty(Property observedProperty, Property observingProperty) {
    checkModelStateAndLastFormBuilderProperty();

    if (observedProperty instanceof AbstractProperty && observingProperty instanceof AbstractProperty) {
      ((AbstractProperty) observedProperty).addObservingProperty((AbstractProperty) observingProperty);
    }

    return this;
  }

  /**
   * Beeinflusst, ob die Komponente im Formular fokussiert wird. NOTE, dass möglicherweise noch die <code>dyna-form.xhtml</code> angepasst werden muss, um den Wert an die Eingabe-Komponente
   * weiterzureichen.
   */
  public FormBuilder withFocus(boolean focused) {
    checkModelStateAndLastFormBuilderProperty();

    if (focused) {
      formBuilderPropertyFocused = getLastFormBuilderProperty();
    } else if (isFocused(getLastFormBuilderProperty())) {
      formBuilderPropertyFocused = null;
    }

    return this;
  }

  /**
   * Fokussiert die Komponente im Formular. NOTE, dass möglicherweise noch die <code>dyna-form.xhtml</code> angepasst werden muss, um den Wert an die Eingabe-Komponente weiterzureichen.
   */
  public FormBuilder focused() {
    return withFocus(true);
  }

  public FormBuilder firstFocused() {
    if (!formBuilderProperties.isEmpty()) {
      formBuilderPropertyFocused = formBuilderProperties.get(0);
    }

    return this;
  }

  boolean isFocused(FormBuilderProperty property) {
    return formBuilderPropertyFocused != null && formBuilderPropertyFocused.equals(property);
  }

  public FormBuilder withTooltip(String tooltip) {
    checkModelStateAndLastFormBuilderProperty();
    getLastFormBuilderProperty().setTooltip(tooltip);

    return this;
  }

  public FormBuilder withStyleClass(String styleClass) {
    checkModelStateAndLastFormBuilderProperty();

    Set<String> styleClasses = new HashSet<>();

    String[] styleClassesSplitted = split(getLastFormBuilderProperty().getStyleClass());

    if (styleClassesSplitted != null) {
      styleClasses.addAll(asList(styleClassesSplitted));
    }

    styleClassesSplitted = split(styleClass);

    if (styleClassesSplitted != null) {
      styleClasses.addAll(asList(styleClassesSplitted));
    }

    getLastFormBuilderProperty().setStyleClass(styleClasses.stream().map(StringUtils::trimToEmpty).filter(StringUtils::isNotBlank).sorted().collect(joining(" ")));

    return this;
  }

  public boolean containsMessagesWithSeverity(Severity severity) {
    for (FormBuilderProperty formBuilderProperty : getFormBuilderProperties()) {
      if (formBuilderProperty.containsMessageWithSeverity(severity)) {
        return true;
      }
    }

    return false;
  }

  @Nullable
  public Property getPropertyFocused() {
    if (formBuilderPropertyFocused == null) {
      return null;
    }

    return formBuilderPropertyFocused.getProperty();
  }

  public FormBuilder withId(String id) {
    this.id = id;

    return this;
  }
}
