
package de.ivu.elect.presentation.personenregister;

import static org.apache.commons.lang3.StringUtils.containsAny;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.replaceIgnoreCase;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.StringUtils.upperCase;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.faces.event.AjaxBehaviorEvent;
import javax.inject.Inject;

import de.ivu.elect.business.person.boundary.SearchParametersMitOrdnungsmerkmal;
import de.ivu.elect.business.person.boundary.SearchParametersMitPersonendaten;
import de.ivu.elect.business.person.entity.EntityMitPersonendaten;
import de.ivu.elect.business.security.UseCasePermission;
import de.ivu.elect.business.security.control.ElectCommonPrincipal;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.faces.PreConditionCheck;
import de.ivu.elect.jee.presentation.dynamicform.AdvancedDynaFormModel;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.jee.presentation.dynamicform.LambdaBooleanProperty;
import de.ivu.elect.jee.presentation.dynamicform.MaschinenlesbarerBereichMapper;
import de.ivu.elect.jee.presentation.dynamicform.MaschinenlesbarerBereichMapperResolver;
import de.ivu.elect.jee.presentation.dynamicform.Property;
import de.ivu.elect.jee.presentation.redirect.Redirect;
import de.ivu.elect.jee.presentation.view.AbstractView;
import de.ivu.elect.presentation.person.form.PersonendatenSearchFormBuilder;
import de.ivu.elect.presentation.personenregister.action.EntityMitPersonendatenSearchResultEntryActionMenu;
import de.ivu.elect.presentation.personenregister.action.EntityMitPersonendatenSearchResultEntryAuswaehlenAction;
import de.ivu.elect.presentation.personenregister.table.AbstractEntityMitPersonendatenSearchResultEntryColumnBuilder;

public abstract class AbstractEntityMitPersonendatenSelectionView<E extends EntityMitPersonendaten, R, T extends AbstractEntityMitPersonendatenSearchResultEntry> extends AbstractView {

  private static final long serialVersionUID = 1L;

  @Inject
  private transient ElectCommonPrincipal principal;

  @Inject
  private transient EntityMitPersonendatenSelectionViewContextParametersConversationSelect entityMitPersonendatenSelectionViewContextParametersConversationSelect;

  @Inject
  private transient EntityMitPersonendatenSearchResultEntryActionMenu entityMitPersonendatenSearchResultEntryActionMenu;

  @Inject
  private transient PersonendatenSearchFormBuilder entityMitPersonendatenSearchFormBuilder;

  @Inject
  private transient MaschinenlesbarerBereichMapperResolver maschinenlesbarerBereichMapperResolver;

  private AdvancedDynaFormModel searchByPersonendatenSearchParametersModel;

  private AdvancedDynaFormModel searchByMaschinenlesbarerBereichSearchParametersModel;

  private AdvancedDynaFormModel searchByOrdnungsmerkmalSearchParametersModel;

  private List<T> searchResultEntries;

  private T searchResultEntrySelection;

  // Cf. http://de.wikipedia.org/wiki/Maschinenlesbarer_Bereich.
  private String maschinenlesbarerBereich;

  public AbstractEntityMitPersonendatenSelectionView() {
    super();
  }

  public void preRenderView() {
    if (isPersonenregisterVorhanden() && !isPersonenregisterErreichbar()) {
      addMessagePersonenregisterNichtErreichbar();
    }
  }

  @PreConditionCheck
  public String checkPreCondition() {
    return getCreateViewHelper().checkPreCondition();
  }

  @PostConstruct
  public void init() {
    resetOnSelectionViewTemplateSelect();
  }

  public abstract String getViewId();

  public String getParentViewId() {
    return getCreateViewHelper().getParentViewId();
  }

  public String getOnSelectionViewId() {
    return getCreateViewHelper().getViewId();
  }

  protected abstract void resetOnSelectionViewTemplateSelect();

  protected abstract AbstractEntityMitPersonendatenSelectionViewCreateViewHelper<E> getCreateViewHelper();

  protected abstract boolean isPersonenregisterVorhanden();

  protected abstract boolean isPersonenregisterErreichbar();

  public abstract boolean isSearchByPersonendatenSupported();

  protected abstract SearchParametersMitPersonendaten getSearchByPersonendatenSearchParameters();

  protected abstract void initDefaultSearchByPersonendatenSearchParameters();

  private void resetSearchByPersonendatenSearchParameters() {
    // Erst zurücksetzen...
    getSearchByPersonendatenSearchParameters().setFamilienname(null);
    getSearchByPersonendatenSearchParameters().setVorname(null);

    getSearchByPersonendatenSearchParameters().setGeburtsdatum(null);
    getSearchByPersonendatenSearchParameters().setGeschlecht(null);

    // ... dann initiale Standardwerte setzen!
    initDefaultSearchByPersonendatenSearchParameters();
  }

  public AdvancedDynaFormModel getSearchByPersonendatenSearchParametersModel() {
    if (searchByPersonendatenSearchParametersModel == null) {
      searchByPersonendatenSearchParametersModel = createSearchByPersonendatenSearchParametersModel();
    }

    return searchByPersonendatenSearchParametersModel;
  }

  private void resetSearchByPersonendatenSearchParametersModel() {
    searchByPersonendatenSearchParametersModel = null;
  }

  private AdvancedDynaFormModel createSearchByPersonendatenSearchParametersModel() {
    FormBuilder formBuilder = new FormBuilder();

    if (isSearchByPersonendatenSupported()) {
      addSearchByPersonendatenSearchParametersModelProperties(formBuilder);
    }

    return formBuilder.createModel();
  }

  protected void addSearchByPersonendatenSearchParametersModelProperties(FormBuilder formBuilder) {
    SearchParametersMitPersonendaten searchParameters = getSearchByPersonendatenSearchParameters();

    entityMitPersonendatenSearchFormBuilder.addPersonendatenProperties(formBuilder, searchParameters);

    Property familiennameProperty = searchParameters.getFamiliennameProperty();

    if (familiennameProperty != null) { // ELECTMUC-2444
      Class< ? extends Property> familiennamePropertyClass = familiennameProperty.getClass();

      if (formBuilder.containsPropertyByClass(familiennamePropertyClass)) {
        formBuilder.selectPropertyByClass(familiennamePropertyClass).focused();
      }
    }
  }

  public abstract boolean isSearchByOrdnungsmerkmalSupported();

  protected abstract SearchParametersMitOrdnungsmerkmal getSearchByOrdnungsmerkmalSearchParameters();

  protected abstract void initDefaultSearchByOrdnungsmerkmalSearchParameters();

  private void resetSearchByOrdnungsmerkmalSearchParameters() {
    // Erst zurücksetzen...
    getSearchByOrdnungsmerkmalSearchParameters().setOrdnungsmerkmal(null);

    // ... dann initiale Standardwerte setzen!
    initDefaultSearchByOrdnungsmerkmalSearchParameters();
  }

  public AdvancedDynaFormModel getSearchByOrdnungsmerkmalSearchParametersModel() {
    if (searchByOrdnungsmerkmalSearchParametersModel == null) {
      searchByOrdnungsmerkmalSearchParametersModel = createSearchByOrdnungsmerkmalSearchParametersModel();
    }

    return searchByOrdnungsmerkmalSearchParametersModel;
  }

  private void resetSearchByOrdnungsmerkmalSearchParametersModel() {
    searchByOrdnungsmerkmalSearchParametersModel = null;
  }

  private AdvancedDynaFormModel createSearchByOrdnungsmerkmalSearchParametersModel() {
    FormBuilder formBuilder = new FormBuilder();

    if (isSearchByOrdnungsmerkmalSupported()) {
      if (isSearchByPersonendatenSupported()) {
        formBuilder.addSeparator("entity.mit.personendaten.selection.alternative.search.parameters.label");
      }

      addSearchByOrdnungsmerkmalSearchParametersModelProperties(formBuilder);
    }

    return formBuilder.createModel();
  }

  private void addSearchByOrdnungsmerkmalSearchParametersModelProperties(FormBuilder formBuilder) {
    formBuilder.addProperty(getSearchByOrdnungsmerkmalSearchParameters().getOrdnungsmerkmalProperty());
  }

  public MaschinenlesbarerBereichMapper getMaschinenlesbarerBereichMapper() {
    return maschinenlesbarerBereichMapperResolver.getMapper();
  }

  public boolean isSearchByMaschinenlesbarerBereichSupported() {
    // To be overridden!!!
    return false;
  }

  private void resetSearchByMaschinenlesbarerBereichSearchParameters() {
    resetMaschinenlesbarerBereich();
  }

  public AdvancedDynaFormModel getSearchByMaschinenlesbarerBereichSearchParametersModel() {
    if (searchByMaschinenlesbarerBereichSearchParametersModel == null) {
      searchByMaschinenlesbarerBereichSearchParametersModel = createSearchByMaschinenlesbarerBereichSearchParametersModel();
    }

    return searchByMaschinenlesbarerBereichSearchParametersModel;
  }

  private void resetSearchByMaschinenlesbarerBereichSearchParametersModel() {
    searchByMaschinenlesbarerBereichSearchParametersModel = null;
  }

  private AdvancedDynaFormModel createSearchByMaschinenlesbarerBereichSearchParametersModel() {
    FormBuilder formBuilder = new FormBuilder();

    if (isSearchByMaschinenlesbarerBereichSupported()) {
      addSearchByMaschinenlesbarerBereichSearchParametersModelProperties(formBuilder);
    }

    return formBuilder.createModel();
  }

  private void addSearchByMaschinenlesbarerBereichSearchParametersModelProperties(FormBuilder formBuilder) {
    // NOTE, dass in src/main/webapp/WEB-INF/templates/personenregister/entity-personenregister-personendaten-selection.xhtml noch
    // ein <e:inputMaschinenlesbarerBereich /> definiert ist!!!

    formBuilder.addProperty(new LambdaBooleanProperty(getMessage("component.input.maschinenlesbarer.bereich.autofocus.label"), false,
        entityMitPersonendatenSelectionViewContextParametersConversationSelect::setMaschinenlesbarerBereichFocused, () -> isMaschinenlesbarerBereichFocused()));
  }

  public boolean isMaschinenlesbarerBereichFocused() {
    // Cf. ELECTMUC-1318, ELECTMUC-2261.
    return entityMitPersonendatenSelectionViewContextParametersConversationSelect.getMaschinenlesbarerBereichFocusedOrDefault(isSearchByMaschinenlesbarerBereichSupported());
  }

  private boolean hasMaschinenlesbarerBereich() {
    return isNotBlank(getMaschinenlesbarerBereich());
  }

  public String getMaschinenlesbarerBereich() {
    return maschinenlesbarerBereich;
  }

  public void setMaschinenlesbarerBereich(String maschinenlesbarerBereich) {
    this.maschinenlesbarerBereich = maschinenlesbarerBereich;
  }

  private void resetMaschinenlesbarerBereich() {
    maschinenlesbarerBereich = null;
  }

  public void onMaschinenlesbarerBereichChanged(AjaxBehaviorEvent event) {
    if (hasMaschinenlesbarerBereich()) {
      MaschinenlesbarerBereichMapper maschinenlesbarerBereichMapper = getMaschinenlesbarerBereichMapper();

      String value = getMaschinenlesbarerBereich();
      boolean searchParametersMapped = maschinenlesbarerBereichMapper.matchesFormat(value);

      if (searchParametersMapped) {
        initDefaultSearchByPersonendatenSearchParameters();

        maschinenlesbarerBereichMapper.map(getSearchByPersonendatenSearchParameters(), value);
      } else {
        addWarningMessage("component.input.maschinenlesbarer.bereich.illegal.message", maschinenlesbarerBereichMapper.getDisplayFormat());
      }

      // Cf. in src/main/webapp/WEB-INF/templates/personenregister/entity-personenregister-personendaten-selection.xhtml!
      ajaxHelper.addOnCompleteCallbackParam("searchParametersMapped", searchParametersMapped);
      ajaxHelper.updateMessages();
    }
  }

  public List<T> getSearchResultEntries() {
    return searchResultEntries;
  }

  private void resetSearchResultEntries() {
    searchResultEntries = new ArrayList<>();
  }

  public T getSearchResultEntrySelection() {
    return searchResultEntrySelection;
  }

  public void setSearchResultEntrySelection(T searchResultEntrySelection) {
    this.searchResultEntrySelection = searchResultEntrySelection;
  }

  private void resetSearchResultEntrySelection() {
    searchResultEntrySelection = null;
  }

  public boolean isBtnTestErreichbarkeitRendered() {
    return isPersonenregisterVorhanden() && !isPersonenregisterErreichbar();
  }

  public boolean isBtnTestErreichbarkeitDisabled() {
    return isPersonenregisterErreichbar();
  }

  public String getBtnTestErreichbarkeitLabel() {
    return getMessage("entity.mit.personendaten.selection.action.test");
  }

  @Redirect
  public String testErreichbarkeit() {
    return getViewId();
  }

  public boolean isBtnSearchRendered() {
    return isPersonenregisterVorhanden() && isPersonenregisterErreichbar() && (isSearchByPersonendatenSupported() || isSearchByOrdnungsmerkmalSupported());
  }

  public boolean isBtnSearchDisabled() {
    return !isPersonenregisterErreichbar();
  }

  public abstract String getBtnSearchLabel();

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

    try {
      return searchInternal(createSearchResult());
    } catch (Exception ex) {
      addMessagePersonenregisterSearchException(ex);
    }

    return getViewId();
  }

  protected void resetSearch() {
    resetOnSelectionViewTemplateSelect();

    resetSearchResultEntries();
    resetSearchResultEntrySelection();
  }

  protected String searchInternal(R searchResult) {
    if (isSearchResultEmpty(searchResult)) {
      addMessageSearchResultEmpty();
    } else {
      boolean searchResultContainsUniqueSearchResultEntry = isSearchResultContainingUniqueSearchResultEntry(searchResult);

      if (searchResultContainsUniqueSearchResultEntry) {
        // NOTE ELECTMUC-2224, dass bei Aktionen immer auch die (automatisch bei jedem ActionEntry geprüfte) Sperrmerkmal-Berechtigung getestet werden muss!
        String outcome = getSearchResultEntryAuswaehlenAction().action(createUniqueSearchResultEntry(searchResult));

        if (isNotBlank(outcome)) {
          return outcome;
        }
      }

      getSearchResultEntries().addAll(createSearchResultEntries(searchResult));

      if (!searchResultContainsUniqueSearchResultEntry) {
        addMessageSearchResultNonUnique();
      }
    }

    return getViewId();
  }

  private R createSearchResult() {
    boolean searchByOrdnungsmerkmalRequested = isNotBlank(getSearchByOrdnungsmerkmalSearchParameters().getOrdnungsmerkmal());
    boolean searchByPersonendatenRequested = isNotBlank(getSearchByPersonendatenSearchParameters().getFamilienname());

    requireNonAmbiguousSearchParameters(searchByPersonendatenRequested, searchByOrdnungsmerkmalRequested);

    if (searchByOrdnungsmerkmalRequested) {
      return createSearchResultByOrdnungsmerkmal();
    }

    return createSearchResultByPersonendaten();
  }

  protected void requireNonAmbiguousSearchParameters(boolean searchByPersonendatenRequested, boolean searchByOrdnungsmerkmalRequested) {
    boolean searchByOrdnungsmerkmalSupported = isSearchByOrdnungsmerkmalSupported();
    boolean searchByPersonendatenSupported = isSearchByPersonendatenSupported();

    if (searchByOrdnungsmerkmalSupported && searchByPersonendatenSupported) {
      if (!searchByOrdnungsmerkmalRequested && !searchByPersonendatenRequested) {
        throw new ElectCommonException("entity.mit.personendaten.selection.search.parameters.empty.message");
      }

      if (searchByOrdnungsmerkmalRequested && searchByPersonendatenRequested) {
        throw new ElectCommonException("entity.mit.personendaten.selection.search.parameters.non.unique.message");
      }
    }

    if (searchByOrdnungsmerkmalSupported && !searchByPersonendatenSupported && !searchByOrdnungsmerkmalRequested) {
      throw new ElectCommonException("entity.mit.personendaten.selection.ordnungsmerkmal.empty.message");
    }

    if (!searchByOrdnungsmerkmalSupported && searchByPersonendatenSupported && !searchByPersonendatenRequested) {
      throw new ElectCommonException("entity.mit.personendaten.selection.familienname.empty.message");
    }
  }

  private R createSearchResultByOrdnungsmerkmal() {
    initDefaultSearchByOrdnungsmerkmalSearchParameters();

    return querySearchResultByOrdnungsmerkmal();
  }

  private R createSearchResultByPersonendaten() {
    initDefaultSearchByPersonendatenSearchParameters();

    R result = querySearchResultByPersonendaten();

    if (isSearchResultEmpty(result)) {
      // NOTE, dass Umlaute im maschinenlesbaren Bereich eines Personalausweises nach DIN 5007 Variante 2, also ä -> AE, normalisiert sind,
      // für die bei der Suche verwendeten normalisierten Namen aber ä -> A gilt, cf. EntityUtil#normalizeCaseAndDiacritics!
      if (isSearchByMaschinenlesbarerBereichSupported()) {
        SearchParametersMitPersonendaten searchByPersonendatenSearchParameters = getSearchByPersonendatenSearchParameters();

        String familienname = searchByPersonendatenSearchParameters.getFamilienname();
        String vorname = searchByPersonendatenSearchParameters.getVorname();

        if (containsAnyUmlautDin5007Var2(familienname) || containsAnyUmlautDin5007Var2(vorname)) {
          String familiennameNormalized = replaceAllUmlauteDin5007Var2(familienname);
          String vornameNormalized = replaceAllUmlauteDin5007Var2(vorname);

          searchByPersonendatenSearchParameters.setFamilienname(familiennameNormalized);

          // familiennameNormalized & vorname
          result = querySearchResultByPersonendaten();

          if (isSearchResultEmpty(result)) {
            searchByPersonendatenSearchParameters.setVorname(vornameNormalized);

            // familiennameNormalized & vornameNormalized
            result = querySearchResultByPersonendaten();
          }

          if (isSearchResultEmpty(result)) {
            searchByPersonendatenSearchParameters.setFamilienname(familienname);

            // familienname & vornameNormalized
            result = querySearchResultByPersonendaten();
          }

          searchByPersonendatenSearchParameters.setFamilienname(familienname);
          searchByPersonendatenSearchParameters.setVorname(vorname);
        }
      }
    }

    return result;
  }

  protected abstract R querySearchResultByPersonendaten();

  protected abstract R querySearchResultByOrdnungsmerkmal();

  protected abstract boolean isSearchResultEmpty(R searchResult);

  protected abstract boolean isSearchResultContainingUniqueSearchResultEntry(R searchResult);

  protected abstract T createUniqueSearchResultEntry(R searchResult);

  protected abstract List<T> createSearchResultEntries(R searchResult);

  private boolean containsAnyUmlautDin5007Var2(String value) {
    return containsAny(upperCase(value), "AE", "OE", "UE");
  }

  private String replaceAllUmlauteDin5007Var2(String value) {
    String result = trimToEmpty(value);
    result = replaceIgnoreCase(result, "AE", "Ä");
    result = replaceIgnoreCase(result, "OE", "Ö");
    result = replaceIgnoreCase(result, "UE", "Ü");

    return result;
  }

  public boolean isBtnCreateRendered() {
    return true;
  }

  public boolean isBtnCreateDisabled() {
    return false;
  }

  public String getBtnCreateLabel() {
    return getMessage("entity.mit.personendaten.selection.action.create");
  }

  @Redirect
  public abstract String create();

  public boolean isBtnResetRendered() {
    return true;
  }

  public boolean isBtnResetDisabled() {
    return false;
  }

  @Redirect
  public String reset() {
    resetSearchByPersonendatenSearchParameters();
    resetSearchByPersonendatenSearchParametersModel();

    resetSearchByMaschinenlesbarerBereichSearchParameters();
    resetSearchByMaschinenlesbarerBereichSearchParametersModel();

    resetSearchByOrdnungsmerkmalSearchParameters();
    resetSearchByOrdnungsmerkmalSearchParametersModel();

    resetSearch();

    return getViewId();
  }

  public boolean isBtnCancelRendered() {
    return true;
  }

  public boolean isBtnCancelDisabled() {
    return false;
  }

  @Redirect
  public String cancel() {
    reset();

    return goBack(getParentViewId());
  }

  public abstract String goToOnSelectionView(T searchResultEntry);

  protected void addMessageSearchResultEmpty() {
    addWarningMessage("entity.mit.personendaten.selection.search.result.empty.message");
  }

  protected void addMessageSearchResultNonUnique() {
    addInfoMessage("entity.mit.personendaten.selection.search.result.non.unique.message");
  }

  protected void addMessagePersonenregisterSearchException(Exception ex) {
    addErrorMessage(ex);
  }

  protected abstract void addMessagePersonenregisterNichtErreichbar();

  protected boolean hasPermission(UseCasePermission permisson) {
    return principal.hasUseCasePermission(permisson);
  }

  public abstract AbstractEntityMitPersonendatenSearchResultEntryColumnBuilder<T> getSearchResultEntryColumnBuilder();

  private EntityMitPersonendatenSearchResultEntryAuswaehlenAction getSearchResultEntryAuswaehlenAction() {
    return getSearchResultEntryActionMenu().getSearchResultEntryAuswaehlenAction();
  }

  public EntityMitPersonendatenSearchResultEntryActionMenu getSearchResultEntryActionMenu() {
    entityMitPersonendatenSearchResultEntryActionMenu.setSelectionView(this);

    return entityMitPersonendatenSearchResultEntryActionMenu;
  }
}
