
package de.ivu.elect.dokumente.kandidatur.statistik;

import java.util.*;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.ergebnis.entity.Gewaehlt;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.GruppeArt;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.kandidatur.entity.Ersatzkandidatur;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.person.entity.Geschlecht;
import de.ivu.elect.business.person.entity.Personendaten;
import de.ivu.elect.util.CaseAndDiacriticIgnoringComparator;

/**
 * Fasst die Kandidaturen eines Kandidaten zusammen, d.h. wenn es mehrere "identisch"-verknüpfte Kandidaturen gibt, wird daraus ein gemeinsames Kandidat-Objekt.
 */
public class Kandidat {

  public static final Comparator<Kandidat> SORT_BY_ALPHABET; // Sortiert nach Familienname, dann nach Vorname, dann nach Geburtsdatum

  static {
    SORT_BY_ALPHABET = initSortByAlphabet();
  }

  private static Comparator<Kandidat> initSortByAlphabet() {
    Comparator<String> nameComparator = CaseAndDiacriticIgnoringComparator.DIN_5007_VAR_2;
    Comparator<Kandidat> compareFamilienname = (x, y) -> nameComparator.compare(x.getFamilienname(), y.getFamilienname());
    Comparator<Kandidat> compareVorname = (x, y) -> nameComparator.compare(x.getVorname(), y.getVorname());

    Date fehlendesGeburtsdatum = new Date();
    Comparator<Kandidat> compareGeburtsdatum = Comparator.comparing(k -> k.getGeburtsdatum() == null ? fehlendesGeburtsdatum : k.getGeburtsdatum());

    // Sortiert nach Familienname, dann nach Vorname, dann nach Geburtsdatum
    return compareFamilienname.thenComparing(compareVorname).thenComparing(compareGeburtsdatum);
  }

  private final Set<Listenkandidatur> listenkandidaturen = new HashSet<>(1);
  private final Set<Direktkandidatur> direktkandidaturen = new HashSet<>(1);
  private final Set<Ersatzkandidatur> ersatzkandidaturen = new HashSet<>(1);
  private final Set<Gebiet> zugehoerigeGebiete = new HashSet<>(1);

  private KandidatArt kandidatArt;

  public Kandidat(Listenkandidatur kandidatur, List<Gebiet> relevanteGebiete) {
    listenkandidaturen.add(kandidatur);
    addZugehoerigeGebiete(relevanteGebiete, kandidatur.getGebiet());
  }

  public Kandidat(Direktkandidatur kandidatur, List<Gebiet> relevanteGebiete) {
    direktkandidaturen.add(kandidatur);
    addZugehoerigeGebiete(relevanteGebiete, kandidatur.getGebiet());
  }

  public Kandidat(Ersatzkandidatur kandidatur, List<Gebiet> relevanteGebiete) {
    ersatzkandidaturen.add(kandidatur);
    addZugehoerigeGebiete(relevanteGebiete, kandidatur.getGebiet());
  }

  private void addZugehoerigeGebiete(List<Gebiet> relevanteGebiete, Gebiet kandidaturGebiet) {
    for (Gebiet gebiet : relevanteGebiete) {
      if (kandidaturGebiet.isInGebietshierarchieVon(gebiet)) {
        zugehoerigeGebiete.add(gebiet);
      }
    }
  }

  public void merge(Kandidat other) {
    if (other == this) {
      return;
    }

    this.listenkandidaturen.addAll(other.listenkandidaturen);
    this.direktkandidaturen.addAll(other.direktkandidaturen);
    this.ersatzkandidaturen.addAll(other.ersatzkandidaturen);
    this.zugehoerigeGebiete.addAll(other.zugehoerigeGebiete);
    this.kandidatArt = null;

    // Nicht wirklich nötig, aber so ist jede Kandidatur in nur einem Kandidaten
    other.listenkandidaturen.clear();
    other.direktkandidaturen.clear();
    other.ersatzkandidaturen.clear();
    other.zugehoerigeGebiete.clear();
    other.kandidatArt = null;
  }

  private Set<Listenkandidatur> getListenkandidaturen() {
    return listenkandidaturen;
  }

  private Set<Direktkandidatur> getDirektkandidaturen() {
    return direktkandidaturen;
  }

  private Set<Ersatzkandidatur> getErsatzkandidaturen() {
    return ersatzkandidaturen;
  }

  private Stream<Kandidatur> getKandidaturenStream() {
    return Stream.concat(Stream.concat(getListenkandidaturen().stream(), getDirektkandidaturen().stream()), getErsatzkandidaturen().stream());
  }

  public Kandidatur getAnyKandidatur() {
    return getKandidaturenStream().findFirst().orElseThrow(() -> new IllegalStateException("Kandidaten ohne Kandidatur dürfen nicht verwendet werden"));
  }

  public Listenkandidatur getAnyListenkandidatur() {
    return getListenkandidaturen().stream().findAny().orElse(null);
  }

  public Direktkandidatur getAnyDirektkandidatur() {
    return getDirektkandidaturen().stream().findAny().orElse(null);
  }

  public Ersatzkandidatur getAnyErsatzkandidatur() {
    return getErsatzkandidaturen().stream().findAny().orElse(null);
  }

  /**
   * @return die PolitischeGruppe, zu der irgendeiner der Kandidaturen gehört. Es wird davon ausgegangen, dass alle zur gleichen gehören.
   */
  public PolitischeGruppe getPolitischeGruppe() {
    return getAnyKandidatur().getGruppe();
  }

  /**
   * @return das Geschlecht irgendeines der Kandidaturen. Es wird davon ausgegangen, dass alle das gleiche Geschlecht haben.
   */
  public Geschlecht getGeschlecht() {
    return getAnyKandidatur().getGeschlecht();
  }

  /**
   * @return das Geburtsdatum irgendeines der Kandidaturen. Es wird davon ausgegangen, dass alle das gleiche Geburtsdatum haben.
   */
  public @Nullable Date getGeburtsdatum() {
    return getAnyKandidatur().getGeburtsdatum();
  }

  /*
   * NOTE: Es werden teils verschiedene Berufe für die verschiedenen Kandidaturen eines Kandidaten verwendet. Die
   * Direktkandidatur geht vor. (WASDE-2248)
   */
  public @Nullable String getBerufsschluessel() {
    Direktkandidatur direktkandidatur = getAnyDirektkandidatur();
    if (direktkandidatur != null) {
      return direktkandidatur.getBerufsschluessel();
    }
    return getAnyKandidatur().getBerufsschluessel();
  }

  public @Nullable String getBerufsschluesselVonGewaehlterKandidatur(Ergebnisart ergebnisart) {
    return Optional.ofNullable(getGewaehlteKandidatur(ergebnisart)).map(Personendaten::getBerufsschluessel).orElse(null);
  }

  public String getFamilienname() {
    return getAnyKandidatur().getFamilienname();
  }

  public String getVorname() {
    return getAnyKandidatur().getRufname();
  }

  /**
   * @return die Teilmenge der beim Initialisieren übergebenen Gebiete, zu deren Gebietshierarchien die Gebiete gehören, in denen der Kandidat antritt
   */
  public Set<Gebiet> getZugehoerigeGebiete() {
    return zugehoerigeGebiete;
  }

  public KandidatArt getKandidatArt() {
    if (kandidatArt == null) {
      int count = 0;
      if (isListenkandidat()) {
        kandidatArt = KandidatArt.NUR_LISTE;
        count++;
      }
      if (isDirektkandidat()) {
        kandidatArt = KandidatArt.NUR_DIREKT;
        count++;
      }
      if (isErsatzkandidat()) {
        kandidatArt = KandidatArt.NUR_ERSATZ;
        count++;
      }
      if (count > 1) {
        kandidatArt = KandidatArt.DOPPELBEWERBER;
        if (getDirektkandidaturen().stream().anyMatch(direktkandidatur -> GruppeArt.EINZELBEWERBER.equals(direktkandidatur.getPolitischeGruppeArt()))) {
          kandidatArt = KandidatArt.DOPPELBEWERBER_ALS_EINZEL_UND_LANDESLISTENBEWERBER;
        }
      }
    }
    return kandidatArt;
  }

  public boolean isDirektkandidat() {
    return !getDirektkandidaturen().isEmpty();
  }

  public boolean isListenkandidat() {
    return !getListenkandidaturen().isEmpty();
  }

  public boolean isErsatzkandidat() {
    return !getErsatzkandidaturen().isEmpty();
  }

  public boolean isAufMehrerenListen() {
    return getListenkandidaturen().size() >= 2;
  }

  public boolean hatEinzigesGebiet(Gebiet gebiet) {
    return zugehoerigeGebiete.size() == 1 && zugehoerigeGebiete.contains(gebiet);
  }

  public boolean isGebietsuebergreifend() {
    return zugehoerigeGebiete.size() > 1;
  }

  public boolean kandidiertImWurzelgebiet() {
    return getKandidaturenStream().anyMatch(k -> k.getGebiet().isWurzelgebiet());
  }

  public Gebiet getDirektkandidaturGebiet() {
    if (getDirektkandidaturen().isEmpty()) {
      return null;
    } else {
      return getDirektkandidaturen().iterator().next().getGebiet();
    }
  }

  public boolean isInVorperiodeGewaehlt() {
    return isGewaehltInErgebnisart(Ergebnisart.VORPERIODE);
  }

  public boolean isGewaehltInErgebnisart(Ergebnisart ergebnisart) {
    return getGewaehltStream(ergebnisart).anyMatch(gewaehlt -> gewaehlt.getGewaehltStatus().isGewaehlt());
  }

  /**
   * @return eine Kandidatur, mit der der Kandidat im Ergebnis der gegebenen Ergebnisart gewählt wurde. null, falls es keine gibt.
   */
  public @Nullable Kandidatur getGewaehlteKandidatur(Ergebnisart ergebnisart) {
    return getGewaehltStream(ergebnisart).filter(gewaehlt -> gewaehlt.getGewaehltStatus().isGewaehlt()).map(gewaehlt -> gewaehlt.getKandidatur()).findAny().orElse(null);
  }

  /**
   * @return wenn getGewaehlteKandidatur(ergebnisart) != null, dann antworte - falls vorhanden - eine weitere Kandidatur der ergebnisart
   */
  public @Nullable Kandidatur getNichtGewaehlteKandidatur(Ergebnisart ergebnisart) {
    Kandidatur gewaehlteKandidatur = getGewaehlteKandidatur(ergebnisart);
    if (gewaehlteKandidatur == null) {
      return null;
    }
    if (getListenkandidaturen().contains(gewaehlteKandidatur)) {
      return Stream.concat(getDirektkandidaturen().stream(), getErsatzkandidaturen().stream()).findAny().orElse(null);
    }
    if (getDirektkandidaturen().contains(gewaehlteKandidatur)) {
      return Stream.concat(getListenkandidaturen().stream(), getErsatzkandidaturen().stream()).findAny().orElse(null);
    }
    if (getErsatzkandidaturen().contains(gewaehlteKandidatur)) {
      return Stream.concat(getListenkandidaturen().stream(), getDirektkandidaturen().stream()).findAny().orElse(null);
    }
    return null; // Fallback
  }

  private Stream<Gewaehlt> getGewaehltStream(Ergebnisart ergebnisart) {
    return getKandidaturenStream().map(k -> k.getGewaehlteByErgebnisart(ergebnisart)).flatMap(g -> g.stream());
  }

  private boolean isInVorperiodeGewaehlt(Kandidatur kandidatur) {
    Gewaehlt gewaehltVorperiode = kandidatur.getGewaehltVorperiode();

    return gewaehltVorperiode != null && gewaehltVorperiode.getGewaehltStatus().isGewaehlt();
  }
}
