
package de.ivu.elect.business.kandidatur.entity;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.persistence.Transient;

import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.gebietsbaum.entity.EntityMitGebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.EntityMitGruppe;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.GruppeArt;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.person.entity.PersonStatus;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.business.person.entity.VerknuepfungsStatus;
import de.ivu.elect.business.pruefung.boundary.ZulassungPrueferFactory;
import de.ivu.elect.business.security.entity.EntityMitSperrmerkmal;
import de.ivu.elect.business.stimmzettel.entity.EntityAufStimmzettelDaten;
import de.ivu.elect.business.wahl.boundary.WahlZulassungspruefung;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahlart;
import de.ivu.elect.business.wahlvorschlag.entity.EntityMitWahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.jee.entity.EntityUtil;
import de.ivu.elect.jee.entity.customdata.CustomData;

public abstract class AbstractKandidatur extends Personendaten implements KandidaturBase, EntityMitWahlvorschlag, EntityMitGruppe<PolitischeGruppe>, EntityMitGebiet, EntityMitSperrmerkmal {

  private static final long serialVersionUID = 1L;

  public AbstractKandidatur() {
    super();
  }

  public AbstractKandidatur(CustomData customData) {
    super(customData);
  }

  public boolean isEinzelbewerber() {
    return getWahlvorschlag() != null && getWahlvorschlag().isEinzelbewerber();
  }

  public boolean hasGewaehltVorperiode() {
    return getGewaehltVorperiode() != null;
  }

  public Gewaehlt getGewaehltVorperiode() {
    return getUniqueGewaehltByErgebnisart(Ergebnisart.VORPERIODE);
  }

  @Deprecated
  // Umbenennen, oder das Ergebnis mitgeben!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  @Nullable
  public Gewaehlt getGewaehlt() {
    return getUniqueGewaehltByErgebnisart(Ergebnisart.ENDGUELTIG);
  }

  public void setGewaehltVorperiode(Gewaehlt gewaehltVorperiode) {
    addUniqueGewaehltByErgebnisart(gewaehltVorperiode, Ergebnisart.VORPERIODE);
  }

  @Nullable
  private Gewaehlt getUniqueGewaehltByErgebnisart(Ergebnisart ergebnisart) {
    for (Gewaehlt gewaehltByErgebnisart : getGewaehlteByErgebnisart(ergebnisart)) {
      return gewaehltByErgebnisart;
    }

    return null;
  }

  private void addUniqueGewaehltByErgebnisart(Gewaehlt gewaehlt, Ergebnisart ergebnisart) {
    if (getUniqueGewaehltByErgebnisart(ergebnisart) != null) {
      throw new IllegalStateException(
          "Zur uebergebenen Ergebnisart [ergebnisart = <" + ergebnisart + ">] existiert bereits ein Ergebnis [gewaehlt = <" + getUniqueGewaehltByErgebnisart(
              ergebnisart) + ">]");
    }

    if (gewaehlt != null) {
      addGewaehlt(gewaehlt);
    }
  }

  public List<Gewaehlt> getGewaehlteByErgebnisart(Ergebnisart ergebnisart) {
    return getGewaehlte().stream().filter(gewaehlt -> ergebnisart.equals(gewaehlt.getErgebnisart())).collect(Collectors.toList());
  }

  @Deprecated // NICHT OHNE ERGEBNISART VERWENDEN!!!
  public List<Gewaehlt> getGewaehlte() {
    return new ArrayList<>(getGewaehltsByKandidatur());
  }

  public void addGewaehlt(Gewaehlt gewaehlt) {
    addGewaehltByKandidatur(gewaehlt);
  }

  public void removeGewaehlt(Gewaehlt gewaehlt) {
    removeGewaehltByKandidatur(gewaehlt);
  }

  public void removeAllGewaehlte() {
    removeAllGewaehltByKandidaturs();
  }

  @Override
  public boolean hasWahlvorschlag() {
    return getWahlvorschlag() != null;
  }

  public boolean hasZugeordneteErsatzkandidatur() {
    return getZugeordneteErsatzkandidatur() != null;
  }

  @Nullable
  public Ersatzkandidatur getZugeordneteErsatzkandidatur() {
    for (Ersatzkandidatur ersatzkandidatur : getErsatzkandidatursByUebergeordneteKandidatur()) {
      return ersatzkandidatur;
    }

    return null;
  }

  public void setZugeordneteErsatzkandidatur(Ersatzkandidatur ersatzkandidatur) {
    removeAllErsatzkandidaturByUebergeordneteKandidaturs();
    if (ersatzkandidatur != null) {
      addErsatzkandidaturByUebergeordneteKandidatur(ersatzkandidatur);
    }
  }

  public List<Ersatzkandidatur> getUntergeordneteErsatzkandidaturen() {
    List<Ersatzkandidatur> result = new ArrayList<>();

    if (hasZugeordneteErsatzkandidatur()) {
      result.add(getZugeordneteErsatzkandidatur());
      result.addAll(getZugeordneteErsatzkandidatur().getUntergeordneteErsatzkandidaturen());
    }

    return result;
  }

  @Transient
  private transient Collection<Kandidatur> identischVerknuepfteKandidaturen = null; // ELECT-11296

  public Collection<Kandidatur> getIdentischVerknuepfteKandidaturen() {
    if (identischVerknuepfteKandidaturen == null) {
      identischVerknuepfteKandidaturen = getIdentischVerknuepfteKandidaturenNoCache();
    }
    return identischVerknuepfteKandidaturen;
  }

  /**
   * @return Gibt alle weiteren Kandidaturen des Kandidaten dieser Kandidatur zurück.
   */
  public Collection<Kandidatur> getIdentischVerknuepfteKandidaturenNoCache() {
    return getVerknuepfteKandidaturenByVerknuepfungsStatusNoCache(VerknuepfungsStatus.IDENTISCH);
  }

  @Transient
  private transient Collection<Kandidatur> aehnlichVerknuepfteKandidaturen = null; // ELECT-11296

  public Collection<Kandidatur> getAehnlichVerknuepfteKandidaturen() {
    if (aehnlichVerknuepfteKandidaturen == null) {
      aehnlichVerknuepfteKandidaturen = getAehnlichVerknuepfteKandidaturenNoCache();
    }
    return aehnlichVerknuepfteKandidaturen;
  }

  public Collection<Kandidatur> getAehnlichVerknuepfteKandidaturenNoCache() {
    return getVerknuepfteKandidaturenByVerknuepfungsStatusNoCache(VerknuepfungsStatus.AEHNLICH);
  }

  @Transient
  private transient Map<VerknuepfungsStatus, Collection<Kandidatur>> verknuepfteKandidaturenByVerknuepfungsStatus = null; // ELECT-11296

  public Collection<Kandidatur> getVerknuepfteKandidaturenByVerknuepfungsStatus(VerknuepfungsStatus verknuepfungsStatus) {
    if (verknuepfteKandidaturenByVerknuepfungsStatus == null) {
      verknuepfteKandidaturenByVerknuepfungsStatus = new HashMap<>();
    }

    return verknuepfteKandidaturenByVerknuepfungsStatus.computeIfAbsent(verknuepfungsStatus,
        x -> getVerknuepfteKandidaturenByVerknuepfungsStatusNoCache(verknuepfungsStatus));
  }

  public Collection<Kandidatur> getVerknuepfteKandidaturenByVerknuepfungsStatusNoCache(VerknuepfungsStatus verknuepfungsStatus) {
    return getVerknuepftePersonendatenByVerknuepfungsStatus(verknuepfungsStatus, Kandidatur.class);
  }

  public GruppeArt getPolitischeGruppeArt() {
    return getGruppe().getGruppeArt();
  }

  @Override
  public PolitischeGruppe getGruppe() {
    GruppeImGebiet gruppeImGebiet = getGruppeImGebiet();

    if (gruppeImGebiet == null) {
      return null; // ELECT-4673
    }

    Gruppe gruppe = gruppeImGebiet.getGruppe();

    if (gruppe == null || !gruppe.isPolitischeGruppe()) {
      return null;
    }

    return EntityUtil.castTo(gruppe, PolitischeGruppe.class);
  }

  @Override
  public void setGruppe(PolitischeGruppe politischeGruppe) {
    GruppeImGebiet gruppeImGebiet = getGruppeImGebiet();

    if (gruppeImGebiet != null) {
      gruppeImGebiet.setGruppe(politischeGruppe);
    }
  }

  @Override
  public Gebiet getGebiet() {
    if (getGruppeImGebiet() == null) {
      return null; // ELECT-4673
    }
    return getGruppeImGebiet().getGebiet();
  }

  public GruppeImGebiet getGruppeImGebiet() {
    if (getWahlvorschlag() == null) {
      return null; // ELECT-4673
    }
    return getWahlvorschlag().getGruppeImGebiet();
  }

  protected Wahlart getWahlart() {
    return getWahl().getWahlart();
  }

  public KandidaturArt getKandidaturArt() {
    if (isListenkandidatur()) {
      return KandidaturArt.LISTE;
    }

    if (isDirektkandidatur()) {
      return KandidaturArt.DIREKT;
    }

    if (isErsatzkandidatur()) {
      return KandidaturArt.ERSATZ;
    }

    throw new IllegalStateException("Unbekannte Art:" + toString());
  }

  public KandidaturExtendedTK getKandidaturExtendedTK() {
    return new KandidaturExtendedTK((Kandidatur) this);
  }

  protected KandidaturStatus preProcessKandidaturStatus(KandidaturStatus neuerWert) {
    return neuerWert;
  }

  public static Map<KandidaturExtendedTK, Kandidatur> mappeKandidaturenNachExtendedTK(Collection<Kandidatur> entities) {
    Map<KandidaturExtendedTK, Kandidatur> result = new HashMap<>(entities.size() * 2);

    for (Kandidatur entity : entities) {
      result.put(entity.getKandidaturExtendedTK(), entity);
    }

    return result;
  }

  public boolean isUngeprueftOderAufStimmzettel() {
    return isUngeprueft() || isAufStimmzettel();
  }

  public boolean isUngeprueft() {
    return getKandidaturStatus().isUngeprueft();
  }

  /**
   * siehe auch isAufStimmzettel()
   */
  public boolean isZugelassen() {
    return getKandidaturStatus().isZugelassen();
  }

  public boolean isAufStimmzettel() {
    if (hasKandidaturArtAufStimmzettel()) {
      if (KandidaturQuelle.VOM_WAEHLER_BENANNT.equals(getKandidaturQuelle())) {
        return true;
      }

      return hasKandidaturStatusAufStimmzettel();
    }

    return false;
  }

  public boolean hasKandidaturArtAufStimmzettel() {
    return !isErsatzkandidatur() && !isListenkandidaturMarkiertAlsErsatzkandidatur();
  }

  private boolean hasKandidaturStatusAufStimmzettel() {
    return getKandidaturStatus() != null && getKandidaturStatus().isKandidaturAufStimmzettel();
  }

  /**
   * siehe auch isUngeprueftOderAufStimmzettel()
   */
  public boolean isUngeprueftOderZugelassen() {
    return isUngeprueft() || isZugelassen();
  }

  @Deprecated // isNichtZugelassenImSinneAbgelehnt verwenden
  public boolean isNichtZugelassen() {
    return isNichtZugelassenImSinneAbgelehnt();
  }

  public boolean isNichtZugelassenImSinneAbgelehnt() {
    return getKandidaturStatus().isNichtZugelassenImSinneAbgelehnt();
  }

  protected void preSetKandidaturStatus(KandidaturStatus alterWert, KandidaturStatus neuerWert) {
    // nicht benötigt, muss aber implementiert sein
  }

  protected void postSetKandidaturStatus(KandidaturStatus alterWert, KandidaturStatus neuerWert) {
    // nichts zu tun
  }

  @Override
  protected void postSetPersonStatus(PersonStatus alterWert, PersonStatus neuerWert) {
    super.postSetPersonStatus(alterWert, neuerWert);

    // ist die Personenprüfung negativ ausgefallen und die Kandidatur bereits zugelassen, setze den KandidaturStatus wieder auf ungeprüft
    if (wertHatSichGeaendert(alterWert, neuerWert) && PersonStatus.NICHT_GEEIGNET.equals(neuerWert) && getKandidaturStatus().isZugelassen()) {
      KandidaturStatus kandidaturStatusInitial = getKandidaturStatusInitial();

      if (!kandidaturStatusInitial.isZugelassen()) {
        setKandidaturStatus(kandidaturStatusInitial);
      }
    }
  }

  protected <T> boolean wertHatSichGeaendert(T alterWert, T neuerWert) {
    return !Objects.equals(alterWert, neuerWert);
  }

  @Deprecated // mit wahlZulassungspruefung nutzen
  public KandidaturStatus getKandidaturStatusInitial() {
    return getKandidaturStatusInitial(getWahlZulassungspruefung());
  }

  public KandidaturStatus getKandidaturStatusInitial(WahlZulassungspruefung wahlZulassungspruefung) {
    return wahlZulassungspruefung.getZulassungKandidaturStatusInitial();
  }

  /**
   * NOTE, dass diese Instanz NUR INTERN verwendet werden darf, da die Instanzen von ZulassungPruefer und WaehlerBenannteKandidaturKonfiguration eigentlich injiziert werden müssen!!!
   *
   * @see CurrentWahlZulassungspruefung
   */
  private WahlZulassungspruefung getWahlZulassungspruefung() {
    Wahl wahl = getWahl();

    if (wahl == null && hasWahlvorschlag()) {
      wahl = getWahlvorschlag().getWahl();
    }

    return new WahlZulassungspruefung(wahl, ZulassungPrueferFactory.DEAKTIVIERT_NUR_ZUGELASSENE_WAHLVORSCHLAEGE);
  }

  /**
   * Kandidat auf Stimmzettel vor Zulassung verstorben/weggezogen, d.h. war nicht zulassungsfähig
   * <p/>
   * Siehe auf jeden Fall ELECTMUC-844
   */
  public boolean isZugelassenObwohlNichtZulassungsfaehig() {
    return getKandidaturStatus().isNichtZugelassenNachtraeglichKeineZulassungsfaehigkeitBeimZulassungsausschuss();
  }

  /**
   * Kandidat auf Stimmzettel hat NACH der Zulassung seine Wählberkeit verloren
   * <p/>
   * Siehe auf jeden Fall auch ELECTMUC-849
   */
  public boolean isZulassungNachtraeglichWeggefallen() {
    return getKandidaturStatus().isNichtZugelassenNachtraeglichKeineZulassungsfaehigkeitNachZulassungsausschuss();
  }

  @Override
  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public Wahlvorschlag getWahlvorschlag() {
    return null;
  }

  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public boolean isDirektkandidatur() {
    return false;
  }

  @Deprecated
  //  castTo(Direktkandidatur.class).getEntity() verwenden! Siehe https://support.elect-it.com/wiki/display/ELECT/Entity+Cast+und+InstanceOf
  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public Direktkandidatur getDirektkandidatur() {
    return null;
  }

  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public boolean isListenkandidatur() {
    return false;
  }

  public boolean isListenkandidaturMarkiertAlsErsatzkandidatur() {
    // wird in AbstractListenkandidatur überschrieben
    return false;
  }

  @Deprecated
  //  castTo(Listenkandidatur.class).getEntity() verwenden! Siehe https://support.elect-it.com/wiki/display/ELECT/Entity+Cast+und+InstanceOf
  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public Listenkandidatur getListenkandidatur() {
    return null;
  }

  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public boolean isErsatzkandidatur() {
    return false;
  }

  @Deprecated
  //  castTo(Ersatzkandidatur.class).getEntity() verwenden! Siehe https://support.elect-it.com/wiki/display/ELECT/Entity+Cast+und+InstanceOf
  // ELECT-3216 Kandidatur nicht mehr abstrakt
  public Ersatzkandidatur getErsatzkandidatur() {
    return null;
  }

  public Gebiet getWahlvorschlagsgebiet() {
    Wahlvorschlag wahlvorschlag = getWahlvorschlag();
    if (wahlvorschlag == null) {
      return null;
    }
    return wahlvorschlag.getGebiet();
  }

  public boolean isGewaehlt(Ergebnisart ergebnisart) {
    for (Gewaehlt gewaehlt : getGewaehlteByErgebnisart(ergebnisart)) {
      if (gewaehlt.getGewaehltStatus().isGewaehlt()) {
        return true;
      }
    }
    return false;
  }

  public KandidaturStimmzettelPersonendaten getStimmzettelPersonendaten(EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return getStimmzettelPersonendaten(getGebiet(), entityAufStimmzettelDaten);
  }

  public KandidaturStimmzettelPersonendaten getStimmzettelPersonendaten(Gebiet gebiet, EntityAufStimmzettelDaten entityAufStimmzettelDaten) {
    return entityAufStimmzettelDaten.formatStimmzettelPersonendaten(gebiet, (Kandidatur) this);
  }

  public void setWahlvorschlag(Wahlvorschlag wahlvorschlag) {
    throw new UnsupportedOperationException(); // siehe überschriebene Methoden
  }
}
