
package de.ivu.elect.business.internet.control;

import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;

import javax.annotation.Nullable;

import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.gebietsbaum.entity.AbstractGebietsgruppierung;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebietsgruppierung;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;

/**
 * <p>
 * Bei dieser Klasse handelt es sich nicht nur um ein Paar aus Wahldurchführungsgebiet und Stimmabgabeart, sondern es kann zusätzlich eine Gebietsgruppierung vorhanden sein.
 * </p>
 * <p>
 * Die Gebietsgruppierung und die Stimmabgabeart wirken als Filter auf den Erfassungsgebieten, die zu dem Wahldurchfuehrungsgebiet gehören. Diese Filterung übernimmt das Prädikat, das man mit der
 * Methode #filterGebiete() erhält. Damit repräsentiert ein GebietUndStimmabgabeart letztlich die Menge der Erfassungsgebiete des Wahldurchfuehrungsgebiets, für die das Prädikat true ergibt.
 * </p>
 * <p>
 *
 * Komplizierter wird es noch einmal durch die nicht ergebnis-relevanten Gebiete. Grundsätzlich werden nicht ergebnis-relevanten Gebiete ausgeschlossen. Von dem Grundsatz gibt es aber zwei Ausnahmen:
 * 1. Ist das Wahldurchführungsgebiet selbst nicht ergebnis-relevant, dann wird es nicht ausgeschlossen. 2. Ist ein Gebiet (a) nicht ergebnis-relevant, (b) ein Untergebiet des Wahldurchführungsgebiets
 * und (c) eines der Elemente der Gebietsgruppierung, dann wird es nicht ausgeschlossen.
 * </p>
 * <p>
 * Mit anderen Worten: nicht ergebnis-relevante Gebiete in der Definition einer GebietUndStimmabgabeart (also entweder
 * als wahldurchfuehrungsgebiet der Instanz oder als eines der definierenden Gebiets der gebietsgruppierung) führen
 * dazu, dass deren Untergebiete in die Aggregation einfließen (sofern sie nicht aus anderem Grund ausgeschlossen sind).
 * Alle anderen nicht ergebnis-relevanten Gebiete werden bei der Aggregation nicht berücksichtigt, d.h. deren
 * Untergebiete fließen nicht in die Aggregation ein (sofern sie nicht aus anderem Grund eingeschlossen sind). Genaueres
 * kann man noch in ELECT-7810 im Kommentar vom 02.10.2018 14:39 nachlesen
 * (https://support.elect-it.com/ticket/browse/ELECT-7810?focusedCommentId=799074&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-799074)
 * </p>
 * ELECT-7810 Abschnitt zu "Ergebnisrelevanz" in Kombination mit Gebietsgruppierungen kann ignoriert werden, das Flag "Ergebnisrelevant" gibt es nur für Destatis, dort werden aber keine Gebietsgruppierungen genutzt
 * <p>
 * Zusammenfassend besteht die Filterung also aus vier mit UND verknüpften Bedigungen:
 * <ul>
 * <li>Ist das Gebiet ein Untergebiet des Wahldurchführungsgebiets?</li>
 * <li>Ist es Untergebiet eines der Gebiete, die die Gebietsgruppierung definieren (falls die Gebietsgruppierung nicht null ist)?</li>
 * <li>Ist das Gebiets wegen der Stimmabgabeart mit eingeschlossen?</li>
 * <li>Ist das Gebiet ergebnis-relevant oder selbst das Wahldurchführungsgebiet oder eines der Gebiete, die die Gebietsgruppierung definieren? Das "Warum" wird in ELECT-7810 beschrieben</li>
 * </ul>
 * Die letzten drei werden durch die Methode #filterGebiete() implementiert. Die erste muss durch den Kontext sichergestellt werden, z.B. indem man gleich nur über die Untergebiete des
 * Wahldurchführungsgebiets iteriert.
 * </p>
 */
public class GebietUndStimmabgabeart {

  public static final HashablePredicate<Gebiet> FALSE_TEST = new HashablePredicate<Gebiet>() {
    @Override
    protected int hashCodeInternal() {
      return 9999;
    }

    @Override
    protected boolean equalsInternal(Object other) {
      return this == other;
    }

    @Override
    public boolean test(Gebiet gebiet) {
      return false;
    }
  };

  public static final HashablePredicate<Gebiet> TRUE_TEST = new HashablePredicate<Gebiet>() {
    @Override
    protected int hashCodeInternal() {
      return 8888;
    }

    @Override
    protected boolean equalsInternal(Object other) {
      return this == other;
    }

    @Override
    public boolean test(Gebiet gebiet) {
      return true;
    }
  };

  private final Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet;

  private final @Nullable Gebietsgruppierung gebietsgruppierung;

  private final Stimmabgabeart stimmabgabeart;

  private final HashablePredicate<Gebiet> gebietImmerAlsErfasstUndRelevantWerten; // Für Destatis-Sondergebiet leider notwendig...

  private final HashablePredicate<Gebiet> customGebietFilter; // Für Kreisauspraegung in DestatisCustomData leider notwendig...

  GebietUndStimmabgabeart(GebietUndStimmabgabeart other) {
    this(other.getWahldurchfuehrungsgebiet(), other.getGebietsgruppierung(), other.getStimmabgabeart(), other.getGebietImmerAlsErfasstUndRelevantWerten(), other.getCustomGebietFilter());
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart) {
    this(wahldurchfuehrungsgebiet, null, stimmabgabeart, FALSE_TEST);
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart, HashablePredicate<Gebiet> gebietImmerAlsErfasstUndRelevantWerten) {
    this(wahldurchfuehrungsgebiet, null, stimmabgabeart, gebietImmerAlsErfasstUndRelevantWerten);
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Stimmabgabeart stimmabgabeart, HashablePredicate<Gebiet> gebietImmerAlsErfasstUndRelevantWerten, HashablePredicate<Gebiet> customGebietFilter) {
    this(wahldurchfuehrungsgebiet, null, stimmabgabeart, gebietImmerAlsErfasstUndRelevantWerten, customGebietFilter);
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Gebietsgruppierung gebietsgruppierung, Stimmabgabeart stimmabgabeart) {
    this(wahldurchfuehrungsgebiet, gebietsgruppierung, stimmabgabeart, FALSE_TEST);
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Gebietsgruppierung gebietsgruppierung, Stimmabgabeart stimmabgabeart, HashablePredicate<Gebiet> gebietImmerAlsErfasstUndRelevantWerten) {
    this(wahldurchfuehrungsgebiet, gebietsgruppierung, stimmabgabeart, gebietImmerAlsErfasstUndRelevantWerten, TRUE_TEST);
  }

  public GebietUndStimmabgabeart(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, @Nullable Gebietsgruppierung gebietsgruppierung, Stimmabgabeart stimmabgabeart,
                                 HashablePredicate<Gebiet> gebietImmerAlsErfasstUndRelevantWerten, HashablePredicate<Gebiet> customGebietFilter) {
    super();

    this.wahldurchfuehrungsgebiet = wahldurchfuehrungsgebiet;
    this.gebietsgruppierung = gebietsgruppierung;
    this.gebietImmerAlsErfasstUndRelevantWerten = gebietImmerAlsErfasstUndRelevantWerten;
    this.stimmabgabeart = stimmabgabeart;
    this.customGebietFilter = customGebietFilter;
  }

  /**
   * Darf nur dann null sein, wenn durch die Methode {@link #alsSchluesselOhneGebiet()} erzeugt.
   */
  @Nullable
  public Wahldurchfuehrungsgebiet getWahldurchfuehrungsgebiet() {
    return wahldurchfuehrungsgebiet;
  }

  @Nullable
  public Gebietsgruppierung getGebietsgruppierung() {
    return gebietsgruppierung;
  }

  public Stimmabgabeart getStimmabgabeart() {
    return stimmabgabeart;
  }

  public HashablePredicate<Gebiet> getGebietImmerAlsErfasstUndRelevantWerten() {
    return gebietImmerAlsErfasstUndRelevantWerten;
  }

  public HashablePredicate<Gebiet> getCustomGebietFilter() {
    return customGebietFilter;
  }

  /**
   * @param ausgezaehlteGebieteSupplier
   *          liefert die Menge der ausgezählten Gebiete. Wird nur für Stimmabgabeart.AUSGEZAEHLT gebraucht, weil ein Gebiet von alleine nicht weiß, ob es ausgezählt ist
   * @return ein Filter für Gebiete nach Stimmabgabeart und ggf. nach Gebietsgruppierung
   */
  public Predicate<Gebiet> filterGebiete(Supplier<Set<Gebiet>> ausgezaehlteGebieteSupplier) {
    return getFilterNachStimmabgabeart(ausgezaehlteGebieteSupplier).and(getFilterNachGebietsgruppierung().and(getFilterNachErgebnisRelevanz())).and(getCustomGebietFilter());
  }

  /**
   * @param ausgezaehlteGebieteSupplier
   *          liefert die Menge der ausgezählten Gebiete. Wird nur für Stimmabgabeart.AUSGEZAEHLT gebraucht, weil ein Gebiet von alleine nicht weiß, ob es ausgezählt ist
   * @return ein Filter für Gebiete nach Stimmabgabeart
   */
  public Predicate<Gebiet> getFilterNachStimmabgabeart(Supplier<Set<Gebiet>> ausgezaehlteGebieteSupplier) {
    return gebietImmerAlsErfasstUndRelevantWerten.or(stimmabgabeart.filterGebiete(ausgezaehlteGebieteSupplier));
  }

  /**
   * @return ein Filter für Gebiete danach, ob es ergebnis-relevant ist (mit den zwei im Klassenkommentar beschriebenen Ausnahmen)
   */
  public Predicate<Gebiet> getFilterNachErgebnisRelevanz() {
    return gebietImmerAlsErfasstUndRelevantWerten.or( //
        gebiet -> gebiet.isErgebnisRelevant() /* Trifft bis auf Destatis immer, Rest der Methode ist irrelevant */
            || gebiet.equals(getWahldurchfuehrungsgebiet()) //
            || getGebietsgruppierung() != null &&
            /* warum hier nicht gebietsgruppierung.isEnthaeltGebietOderUebergeordnetesGebietVon genutzt wird, wird ausfürlich in ELECT-7810 beschrieben */
                getGebietsgruppierung().getGebiets().contains(gebiet));
  }

  /**
   * @return ein Filter, der nur Gebiete betrachtet, die zur Gebietsgruppierung gehörten (Zugehörigkeit eines Gebiets vererbt sich auf Untergebiete)
   */
  public Predicate<Gebiet> getFilterNachGebietsgruppierung() {
    return AbstractGebietsgruppierung.getGebietsgruppierungFilter(getGebietsgruppierung());
  }

  public GebietUndStimmabgabeart copy(Stimmabgabeart stimmabgabeart) {
    return copyInternal(getWahldurchfuehrungsgebiet(), getGebietsgruppierung(), stimmabgabeart);
  }

  public GebietUndStimmabgabeart copy(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return copyInternal(wahldurchfuehrungsgebiet, getGebietsgruppierung(), getStimmabgabeart());
  }

  public GebietUndStimmabgabeart copy(Gebietsgruppierung gebietsgruppierung) {
    return copyInternal(getWahldurchfuehrungsgebiet(), gebietsgruppierung, getStimmabgabeart());
  }

  // ELECT-11713, immer Konstruktor mit ALLEN Parametern verwenden...
  protected GebietUndStimmabgabeart copyInternal(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet, Gebietsgruppierung gebietsgruppierung, Stimmabgabeart stimmabgabeart) {
    return new GebietUndStimmabgabeart(wahldurchfuehrungsgebiet, gebietsgruppierung, stimmabgabeart, getGebietImmerAlsErfasstUndRelevantWerten(), getCustomGebietFilter());
  }

  /**
   * @return eine Kopie von mir ohne Gebiet. Reduziert es damit auf die Filter-Funktion.
   */
  public GebietUndStimmabgabeart alsSchluesselOhneGebiet() {
    return copy((Wahldurchfuehrungsgebiet) null);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) {
      return true;
    }
    if (o == null || getClass() != o.getClass()) {
      return false;
    }

    GebietUndStimmabgabeart that = (GebietUndStimmabgabeart) o;

    if (wahldurchfuehrungsgebiet != null ? !wahldurchfuehrungsgebiet.equals(that.wahldurchfuehrungsgebiet) : that.wahldurchfuehrungsgebiet != null) {
      return false;
    }
    if (gebietsgruppierung != null ? !gebietsgruppierung.equals(that.gebietsgruppierung) : that.gebietsgruppierung != null) {
      return false;
    }
    if (stimmabgabeart != that.stimmabgabeart) {
      return false;
    }
    if (gebietImmerAlsErfasstUndRelevantWerten != null ? !gebietImmerAlsErfasstUndRelevantWerten.equals(that.gebietImmerAlsErfasstUndRelevantWerten) : that.gebietImmerAlsErfasstUndRelevantWerten != null) {
      return false;
    }
    return customGebietFilter != null ? customGebietFilter.equals(that.customGebietFilter) : that.customGebietFilter != null;
  }

  @Override
  public int hashCode() {
    int result = wahldurchfuehrungsgebiet != null ? wahldurchfuehrungsgebiet.hashCode() : 0;
    result = 31 * result + (gebietsgruppierung != null ? gebietsgruppierung.hashCode() : 0);
    result = 31 * result + stimmabgabeart.hashCode();
    result = 31 * result + (gebietImmerAlsErfasstUndRelevantWerten != null ? gebietImmerAlsErfasstUndRelevantWerten.hashCode() : 0);
    result = 31 * result + (customGebietFilter != null ? customGebietFilter.hashCode() : 0);
    return result;
  }

  @Override
  public String toString() {
    return "GebietUndStimmabgabeart{" +
        "wahldurchfuehrungsgebiet=" + wahldurchfuehrungsgebiet +
        ", gebietsgruppierung=" + gebietsgruppierung +
        ", stimmabgabeart=" + stimmabgabeart +
        ", gebietImmerAlsErfasstUndRelevantWerten=" + gebietImmerAlsErfasstUndRelevantWerten +
        ", customGebietFilter=" + customGebietFilter +
        '}';
  }
}
