
package de.ivu.elect.presentation.entschaedigung.form;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.capitalize;

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

import javax.inject.Inject;

import de.ivu.elect.business.entschaedigung.boundary.AuszahlungExtendedDAO;
import de.ivu.elect.business.entschaedigung.boundary.EntschaedigungCalculator;
import de.ivu.elect.business.entschaedigung.boundary.EntschaedigungsartCreateParameters;
import de.ivu.elect.business.entschaedigung.entity.AbstractEntschaedigungsart;
import de.ivu.elect.business.entschaedigung.entity.Auszahlung;
import de.ivu.elect.business.entschaedigung.entity.AuszahlungConstants;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungConstants;
import de.ivu.elect.business.entschaedigung.entity.Entschaedigungsart;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartAnzahlBooleanProperty;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartBerechnungsart;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartEinzelbetragMoneyProperty;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumEinsatzfunktion;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumEinsatzfunktionProperty;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumEinsatzort;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumEinsatzortProperty;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumPersonendatenArt;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartKriteriumPersonendatenArtProperty;
import de.ivu.elect.business.entschaedigung.entity.EntschaedigungsartProperties;
import de.ivu.elect.business.person.entity.PersonendatenArt;
import de.ivu.elect.jee.presentation.dynamicform.FormBuilder;
import de.ivu.elect.jee.presentation.dynamicform.LambdaEnumProperty;
import de.ivu.elect.jee.presentation.dynamicform.MoneyProperty;
import de.ivu.elect.jee.presentation.dynamicform.Property;
import de.ivu.elect.jee.presentation.dynamicform.ReadOnlyStringProperty;
import de.ivu.elect.presentation.ViewFormBuilder;

public class EntschaedigungsartViewFormBuilder extends ViewFormBuilder {

  private static final long serialVersionUID = 1L;

  @Inject
  private transient AuszahlungExtendedDAO auszahlungDao;

  @Inject
  private transient EntschaedigungCalculator entschaedigungCalculator;

  @Inject
  private transient AuszahlungViewFormBuilder auszahlungViewFormBuilder;

  private Collection<PersonendatenArt> personendatenArten;

  public EntschaedigungsartViewFormBuilder() {
    super();
  }

  public boolean isBearbeitbar(Entschaedigungsart entschaedigungsart) {
    return entschaedigungsart.isBearbeitbar(auszahlungDao);
  }

  public void addProperties(FormBuilder formBuilder, Entschaedigungsart entschaedigungsart, boolean readonly) {
    Predicate<Entschaedigungsart> kriteriumPersonendatenArtVariabel = instance -> true;

    Predicate<Entschaedigungsart> berechnungsartVariabel = instance -> true;

    Predicate<Entschaedigungsart> einzelbetragVariabel = instance -> true;
    Predicate<Entschaedigungsart> einzelbetragNurGewaehrbar = instance -> false;

    addProperties(formBuilder, entschaedigungsart, kriteriumPersonendatenArtVariabel, berechnungsartVariabel, einzelbetragVariabel, einzelbetragNurGewaehrbar, readonly);
  }

  public void addProperties(FormBuilder formBuilder, Entschaedigungsart entschaedigungsart, EntschaedigungsartCreateParameters entschaedigungsartCreateParameters) {
    Predicate<Entschaedigungsart> kriteriumPersonendatenArtVariabel = instance -> !entschaedigungsartCreateParameters.hasPersonendatenArt();

    Predicate<Entschaedigungsart> berechnungsartVariabel = instance -> !entschaedigungsartCreateParameters.hasBerechnungsart();

    Predicate<Entschaedigungsart> einzelbetragVariabel = instance -> entschaedigungsartCreateParameters.getEinzelbetragVariabel();
    Predicate<Entschaedigungsart> einzelbetragNurGewaehrbar = instance -> entschaedigungsartCreateParameters.getEinzelbetragNurGewaehrbar();

    addProperties(formBuilder, entschaedigungsart, kriteriumPersonendatenArtVariabel, berechnungsartVariabel, einzelbetragVariabel, einzelbetragNurGewaehrbar, false);
  }

  private void addProperties(FormBuilder formBuilder, Entschaedigungsart entschaedigungsart, Predicate<Entschaedigungsart> kriteriumPersonendatenArtVariabel,
      Predicate<Entschaedigungsart> berechnungsartVariabel, Predicate<Entschaedigungsart> einzelbetragVariabel, Predicate<Entschaedigungsart> einzelbetragNurGewaehrbar, boolean readonly) {
    boolean readonlyOrDefault = readonly || !isBearbeitbar(entschaedigungsart);

    addEntschaedigungsartProperties(formBuilder, entschaedigungsart, kriteriumPersonendatenArtVariabel, berechnungsartVariabel, einzelbetragVariabel, einzelbetragNurGewaehrbar, readonlyOrDefault);

    Collection<Auszahlung> auszahlungen = entschaedigungsart.getAuszahlungen(auszahlungDao);

    if (!auszahlungen.isEmpty()) {
      formBuilder.addSeparator(AuszahlungConstants.MESSAGE_KEY_ENTITY_MEHRZAHL);

      List<Auszahlung> auszahlungenSorted = new ArrayList<>(auszahlungen);
      auszahlungenSorted.sort(AuszahlungViewFormBuilder.AUSZAHLUNG_DEFAULT_COMPARATOR);

      for (Iterator<Auszahlung> auszahlungenIter = auszahlungenSorted.iterator(); auszahlungenIter.hasNext();) {
        addEntschaedigungsartInAuszahlungProperties(formBuilder, auszahlungenIter.next(), entschaedigungsart);

        if (auszahlungenIter.hasNext()) {
          formBuilder.addSeparator();
        }
      }
    }
  }

  private void addEntschaedigungsartProperties(FormBuilder formBuilder, Entschaedigungsart entschaedigungsart, Predicate<Entschaedigungsart> kriteriumPersonendatenArtVariabel,
      Predicate<Entschaedigungsart> berechnungsartVariabel, Predicate<Entschaedigungsart> einzelbetragVariabel, Predicate<Entschaedigungsart> einzelbetragNurGewaehrbar, boolean readonly) {
    boolean readonlyOrDefault = readonly || entschaedigungsart.isPersistent();

    EntschaedigungsartProperties entschaedigungsartProperties = entschaedigungsart.getProperties();

    formBuilder.addProperty(entschaedigungsartProperties.getBezeichnungProperty()).withReadonly(readonly);
    formBuilder.addProperty(entschaedigungsartProperties.getBeschreibungProperty()).withReadonly(readonly);

    formBuilder.addProperty(new EntschaedigungsartKriteriumPersonendatenArtProperty(entschaedigungsart, entschaedigungCalculator))
        .withReadonly(readonlyOrDefault || !kriteriumPersonendatenArtVariabel.test(entschaedigungsart));

    LambdaEnumProperty<EntschaedigungsartBerechnungsart> berechnungsartProperty = entschaedigungsartProperties.getBerechnungsartProperty();

    if (isBerechnungsartRendered(entschaedigungsart, readonlyOrDefault)) {
      formBuilder.addProperty(berechnungsartProperty).withReadonly(readonlyOrDefault || !berechnungsartVariabel.test(entschaedigungsart));
    }

    if (isEntschaedigungsartKriteriumEinsatzortRendered(entschaedigungsart, readonlyOrDefault)) {
      formBuilder.addProperty(createEntschaedigungsartKriteriumEinsatzortProperty(entschaedigungsart)).withObservedProperties(berechnungsartProperty).withReadonly(readonlyOrDefault);
    }

    if (isEntschaedigungsartKriteriumEinsatzfunktionRendered(entschaedigungsart, readonlyOrDefault)) {
      formBuilder.addProperty(createEntschaedigungsartKriteriumEinsatzfunktionProperty(entschaedigungsart)).withObservedProperties(berechnungsartProperty).withReadonly(readonlyOrDefault);
    }

    boolean anzahlVariabel = !(readonlyOrDefault && entschaedigungsart.usesAnzahlMaximal(1L) || einzelbetragNurGewaehrbar.test(entschaedigungsart));

    if (anzahlVariabel) {
      formBuilder.addSeparator(EntschaedigungConstants.MESSAGE_KEY_ANZAHL_EINZAHL);

      formBuilder.addProperty(entschaedigungsartProperties.getAnzahlMinimalProperty()).withReadonly(readonlyOrDefault);
      formBuilder.addProperty(entschaedigungsartProperties.getAnzahlMaximalProperty()).withReadonly(readonlyOrDefault);
      formBuilder.addProperty(entschaedigungsartProperties.getAnzahlInitialProperty()).withReadonly(readonlyOrDefault);
    }

    formBuilder.addSeparator(EntschaedigungConstants.MESSAGE_KEY_EINZELBETRAG_EINZAHL);

    if (einzelbetragVariabel.test(entschaedigungsart)) {
      String waehrungAlsIsoCode = entschaedigungsart.getWaehrungAlsIsoCode();

      Property einzelbetragMaximalProperty = new MoneyProperty(entschaedigungsartProperties.getEinzelbetragMaximalProperty(), waehrungAlsIsoCode);

      if (readonly && !einzelbetragMaximalProperty.hasValue()) {
        einzelbetragMaximalProperty = new ReadOnlyStringProperty(einzelbetragMaximalProperty.getLabel(),
            capitalize(getMessage(AbstractEntschaedigungsart.MESSAGE_KEY_EINZELBETRAG_MAXIMAL_EINZAHL_UNBESCHRAENKT)));
      }

      formBuilder.addProperty(new MoneyProperty(entschaedigungsartProperties.getEinzelbetragMinimalProperty(), waehrungAlsIsoCode)).withReadonly(readonly);
      formBuilder.addProperty(einzelbetragMaximalProperty).withReadonly(readonly);
      formBuilder.addProperty(new MoneyProperty(entschaedigungsartProperties.getEinzelbetragInitialProperty(), waehrungAlsIsoCode)).withReadonly(readonly);
    } else {
      formBuilder.addProperty(new EntschaedigungsartEinzelbetragMoneyProperty(entschaedigungsart)).withReadonly(readonly);
    }

    if (!anzahlVariabel) {
      formBuilder.addProperty(new EntschaedigungsartAnzahlBooleanProperty(entschaedigungsart)).withReadonly(readonlyOrDefault);
    }
  }

  private boolean isBerechnungsartRendered(Entschaedigungsart entschaedigungsart, boolean readonly) {
    return !readonly || !EntschaedigungsartBerechnungsart.PER_ANZAHL.equals(entschaedigungsart.getBerechnungsart());
  }

  private boolean isEntschaedigungsartKriteriumEinsatzortRendered(Entschaedigungsart entschaedigungsart, boolean readonly) {
    return !readonly || !entschaedigungsart.usesKriteriumEinsatzort(EntschaedigungsartKriteriumEinsatzort.IRRELEVANT)
        || EntschaedigungsartBerechnungsart.PER_EINSATZ.equals(entschaedigungsart.getBerechnungsart())
            && extractPersonendatenArt(entschaedigungsart).map(this::isEntschaedigungsartKriteriumEinsatzortRendered).orElse(false);
  }

  protected boolean isEntschaedigungsartKriteriumEinsatzortRendered(PersonendatenArt personendatenArt) {
    return false;
  }

  protected EntschaedigungsartKriteriumEinsatzortProperty createEntschaedigungsartKriteriumEinsatzortProperty(Entschaedigungsart entschaedigungsart) {
    return new EntschaedigungsartKriteriumEinsatzortProperty(entschaedigungsart, entschaedigungCalculator);
  }

  private boolean isEntschaedigungsartKriteriumEinsatzfunktionRendered(Entschaedigungsart entschaedigungsart, boolean readonly) {
    return !readonly || !entschaedigungsart.usesKriteriumEinsatzfunktion(EntschaedigungsartKriteriumEinsatzfunktion.IRRELEVANT)
        || EntschaedigungsartBerechnungsart.PER_EINSATZ.equals(entschaedigungsart.getBerechnungsart())
            && extractPersonendatenArt(entschaedigungsart).map(this::isEntschaedigungsartKriteriumEinsatzfunktionRendered).orElse(false);
  }

  protected boolean isEntschaedigungsartKriteriumEinsatzfunktionRendered(PersonendatenArt personendatenArt) {
    return false;
  }

  protected EntschaedigungsartKriteriumEinsatzfunktionProperty createEntschaedigungsartKriteriumEinsatzfunktionProperty(Entschaedigungsart entschaedigungsart) {
    return new EntschaedigungsartKriteriumEinsatzfunktionProperty(entschaedigungsart, entschaedigungCalculator);
  }

  private void addEntschaedigungsartInAuszahlungProperties(FormBuilder formBuilder, Auszahlung auszahlung, Entschaedigungsart entschaedigungsart) {
    auszahlungViewFormBuilder.addAuszahlungProperties(formBuilder, auszahlung, entschaedigungsart);
  }

  protected <T extends PersonendatenArt & EntschaedigungsartKriteriumPersonendatenArt> boolean matchesAnyPersonendatenArt(PersonendatenArt expected, T one, T... more) {
    Set<T> values = new HashSet<>();
    values.add(one);
    values.addAll(asList(more));

    return values.stream().anyMatch(actual -> actual.matchesPersonendatenArt(expected));
  }

  private Optional<PersonendatenArt> extractPersonendatenArt(Entschaedigungsart entschaedigungsart) {
    return getPersonendatenArten().stream() //
        .filter(entschaedigungsart::usesKriteriumPersonendatenArt) //
        .findFirst();
  }

  private Collection<PersonendatenArt> getPersonendatenArten() {
    if (personendatenArten == null) {
      personendatenArten = new HashSet<>();
      personendatenArten.addAll(entschaedigungCalculator.getSupportedPersonendatenArten().stream().map(personendatenArt -> (PersonendatenArt) personendatenArt).collect(toList()));
    }

    return personendatenArten;
  }
}
