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

import static java.util.Collections.emptySet;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;

import javax.annotation.Nullable;

import com.google.common.math.BigIntegerMath;

import de.ivu.elect.business.ergebniskonflikt.control.util.SublistList;
import de.ivu.elect.business.gebietsbaum.boundary.AbstractWahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.GebietPK;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.boundary.AbstractPolitischeGruppeDAO;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.GruppePK;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.kandidatur.boundary.AbstractKandidaturDAO;
import de.ivu.elect.business.kandidatur.entity.Kandidatur;
import de.ivu.elect.business.person.entity.PersonendatenPK;
import de.ivu.elect.business.wahlvorschlag.boundary.AbstractListenverbindungDAO;
import de.ivu.elect.business.wahlvorschlag.boundary.AbstractWahlvorschlagDAO;
import de.ivu.elect.business.wahlvorschlag.entity.Listenverbindung;
import de.ivu.elect.business.wahlvorschlag.entity.ListenverbindungPK;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import de.ivu.elect.business.wahlvorschlag.entity.WahlvorschlagPK;
import de.ivu.elect.jee.entity.AbstractNoPersistentEntity;
import de.ivu.elect.jee.entity.EntityLongPK;
import de.ivu.elect.jee.entity.EntityPK;
import de.ivu.elect.jee.entity.tk.EntityUK;

public class SitzberechnungsalgorithmusOptionenAuswaehlenTypisiertErgebnisKonfliktCustomData<T extends SitzberechnungsalgorithmusOption<T>> extends AbstractErgebnisKonfliktLosoptionenCustomData
    implements
      SitzberechnungsalgorithmusOptionenAuswaehlenErgebnisKonfliktCustomData<T> {

  private static final long serialVersionUID = 1L;

  private Enum< ? > losentscheidTyp;

  private Set<GebietPK> gebietPKs;

  private Set<GruppePK> gruppePKs;

  private Set<WahlvorschlagPK> wahlvorschlagPKs;

  private Set<PersonendatenPK> kandidaturPKs;

  private final Map<Long, T> verfuegbareOptionPerId;

  private Set<Long> beteiligteOptionIds;

  private Set<Long> gelosteOptionIds;

  public SitzberechnungsalgorithmusOptionenAuswaehlenTypisiertErgebnisKonfliktCustomData() {
    super();

    this.losentscheidTyp = null;

    this.gebietPKs = new LinkedHashSet<>();
    this.gruppePKs = new LinkedHashSet<>();
    this.wahlvorschlagPKs = new LinkedHashSet<>();
    this.kandidaturPKs = new LinkedHashSet<>();

    this.verfuegbareOptionPerId = new HashMap<>();

    this.beteiligteOptionIds = new LinkedHashSet<>();
    this.gelosteOptionIds = new LinkedHashSet<>();
  }

  @Override
  public void setContext(SitzberechnungsalgorithmusOptionContext<T> context) {
    setLosentscheidTyp(context.getLosentscheidTyp());

    setGebietPKs(mapEntityPerPK(context.getGebiete()).keySet());
    setGruppePKs(mapEntityPerPK(context.getGruppen()).keySet());
    setWahlvorschlagPKs(mapEntityPerPK(context.getWahlvorschlaege()).keySet());
    setKandidaturPKs(mapEntityPerPK(context.getKandidaturen()).keySet());

    setVerfuegbareOptionPerId(collectOptionPerId(context.getVerfuegbareOptionen()));

    notifyEntity();
  }

  public Enum< ? > getLosentscheidTyp() {
    return losentscheidTyp;
  }

  private void setLosentscheidTyp(Enum< ? > losentscheidTyp) {
    this.losentscheidTyp = losentscheidTyp;
  }

  public List<EntityPK< ? >> getBeteiligteEntityPKs() {
    List<EntityPK< ? >> result = new ArrayList<>();
    result.addAll(getKandidaturPKs());
    result.addAll(getWahlvorschlagPKs());
    result.addAll(getGruppePKs());
    result.addAll(getGebietPKs());

    if (result.isEmpty()) {
      throw new IllegalStateException("Keine Entity gefunden, für die Optionen auszuwählen sind [losentscheidTyp = " + getLosentscheidTyp() + ", optionen = " + getVerfuegbareOptionen()
          + ", kandidaturen = " + getKandidaturPKs() + ", wahlvorschläge = " + getWahlvorschlagPKs() + ", gruppen = " + getGruppePKs() + ", gebiete = " + getGebietPKs() + "]");
    }

    return result;
  }

  private Collection<GebietPK> getGebietPKs() {
    return gebietPKs;
  }

  private void setGebietPKs(Collection<GebietPK> gebietPKs) {
    this.gebietPKs = new LinkedHashSet<>(gebietPKs);
  }

  private Collection<GruppePK> getGruppePKs() {
    return gruppePKs;
  }

  private void setGruppePKs(Collection<GruppePK> gruppePKs) {
    this.gruppePKs = new LinkedHashSet<>(gruppePKs);
  }

  private Collection<WahlvorschlagPK> getWahlvorschlagPKs() {
    return wahlvorschlagPKs;
  }

  private void setWahlvorschlagPKs(Collection<WahlvorschlagPK> wahlvorschlagPKs) {
    this.wahlvorschlagPKs = new LinkedHashSet<>(wahlvorschlagPKs);
  }

  private Collection<PersonendatenPK> getKandidaturPKs() {
    return kandidaturPKs;
  }

  private void setKandidaturPKs(Collection<PersonendatenPK> kandidaturPKs) {
    this.kandidaturPKs = new LinkedHashSet<>(kandidaturPKs);
  }

  public Collection<T> getVerfuegbareOptionen() {
    return attachOptionen(getVerfuegbareOptionPerId().keySet());
  }

  private Map<Long, T> getVerfuegbareOptionPerId() {
    return verfuegbareOptionPerId;
  }

  private void setVerfuegbareOptionPerId(Map<Long, T> verfuegbareOptionPerId) {
    this.verfuegbareOptionPerId.putAll(verfuegbareOptionPerId);
  }

  public Collection<T> getBeteiligteOptionen() {
    return attachOptionen(getBeteiligteOptionIds());
  }

  @Override
  public void setBeteiligteOptionen(Collection<T> beteiligteOptionen) {
    setBeteiligteOptionIds(extractIds(beteiligteOptionen));

    setVerfuegbareOptionPerId(collectOptionPerId(beteiligteOptionen));

    notifyEntity();
  }

  private Collection<Long> getBeteiligteOptionIds() {
    return beteiligteOptionIds;
  }

  private void setBeteiligteOptionIds(Collection<Long> beteiligteOptionIds) {
    this.beteiligteOptionIds = new LinkedHashSet<>(beteiligteOptionIds);
  }

  @Override
  public Collection<T> extractGelosteOptionen(Collection<T> beteiligteOptionen) {
    List<T> result = new ArrayList<>();

    Map<Long, T> beteiligteOptionPerId = collectOptionPerId(beteiligteOptionen);

    for (Long gelosteOptionenId : getGelosteOptionIds()) {
      T gelosteOption = beteiligteOptionPerId.get(gelosteOptionenId);

      if (gelosteOption == null) {
        throw new IllegalStateException("Geloste Option nicht in den beteiligten Optionen gefunden! [gelost = " + gelosteOptionenId + ", beteiligt = " + beteiligteOptionPerId.entrySet() + "]");
      }

      result.add(gelosteOption);
    }

    return result;
  }

  public Collection<T> getGelosteOptionen() {
    return attachOptionen(getGelosteOptionIds());
  }

  public void setGelosteOptionen(Collection<T> gelosteOptionen) {
    setGelosteOptionIds(extractIds(gelosteOptionen));

    setVerfuegbareOptionPerId(collectOptionPerId(gelosteOptionen));

    notifyEntity();
  }

  private Collection<Long> getGelosteOptionIds() {
    return gelosteOptionIds;
  }

  private void setGelosteOptionIds(Collection<Long> gelosteOptionIds) {
    this.gelosteOptionIds = new LinkedHashSet<>(gelosteOptionIds);
  }

  @Override
  public String createLoesungBeschreibung(ErgebnisKonfliktCustomDataFormatter ergebnisKonfliktCustomDataFormatter, Collection<Wahlvorschlag> wahlvorschlaege, Collection<Kandidatur> kandidaturen,
      Collection<Listenverbindung> listenverbindungen, Collection<PolitischeGruppe> politischeGruppen, Collection<Wahldurchfuehrungsgebiet> gebiete) {
    return SitzberechnungsalgorithmusOptionenAuswaehlenErgebnisKonfliktCustomData.formatKonfliktLosoptionen(ergebnisKonfliktCustomDataFormatter, getGelosteOptionen());
  }

  @Override
  public List<ErgebnisKonfliktLosoption> getLosoptionenVerfuegbar(ErgebnisKonfliktCustomDataFormatter ergebnisKonfliktCustomDataFormatter, AbstractWahlvorschlagDAO wahlvorschlagDAO,
      AbstractKandidaturDAO kandidaturDAO, AbstractListenverbindungDAO listenverbindungDAO, AbstractPolitischeGruppeDAO politischeGruppeDAO,
      AbstractWahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO) {
    return createLosoptionen(ergebnisKonfliktCustomDataFormatter, wahldurchfuehrungsgebietDAO, politischeGruppeDAO, wahlvorschlagDAO, kandidaturDAO, getBeteiligteOptionIds());
  }

  @Override
  public List<ErgebnisKonfliktLosoption> getLosoptionenGelost(ErgebnisKonfliktCustomDataFormatter ergebnisKonfliktCustomDataFormatter, AbstractWahlvorschlagDAO wahlvorschlagDAO,
      AbstractKandidaturDAO kandidaturDAO, AbstractListenverbindungDAO listenverbindungDAO, AbstractPolitischeGruppeDAO politischeGruppeDAO,
      AbstractWahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO) {
    return createLosoptionen(ergebnisKonfliktCustomDataFormatter, wahldurchfuehrungsgebietDAO, politischeGruppeDAO, wahlvorschlagDAO, kandidaturDAO, getGelosteOptionIds());
  }

  @Override
  public void setLosoptionenGelost(List<ErgebnisKonfliktLosoption> losoptionenGelost) {
    setGelosteOptionIds(losoptionenGelost.stream().map(ErgebnisKonfliktLosoption::getId).collect(toList()));

    notifyEntity();
  }

  private List<ErgebnisKonfliktLosoption> createLosoptionen(ErgebnisKonfliktCustomDataFormatter ergebnisKonfliktCustomDataFormatter, AbstractWahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO,
      AbstractPolitischeGruppeDAO politischeGruppeDAO, AbstractWahlvorschlagDAO wahlvorschlagDAO, AbstractKandidaturDAO kandidaturDAO, Collection<Long> optionIds) {
    List<ErgebnisKonfliktLosoption> result = new ArrayList<>();

    SitzberechnungsalgorithmusOptionContext<T> context = createContext(wahldurchfuehrungsgebietDAO, politischeGruppeDAO, wahlvorschlagDAO, kandidaturDAO);

    Gebiet gebiet = null;

    if (context.getGebiete().size() == 1) {
      gebiet = context.getGebiete().iterator().next();
    } else if (context.getWahlvorschlaege().size() == 1) {
      gebiet = context.getWahlvorschlaege().iterator().next().getGebiet();
    } else if (context.getKandidaturen().size() == 1) {
      gebiet = context.getKandidaturen().iterator().next().getGebiet();
    }

    for (Long optionId : optionIds) {
      T option = attachOption(optionId);

      result.add(new ErgebnisKonfliktLosoption(new AbstractNoPersistentEntity<EntityLongPK, EntityUK>() {

        private static final long serialVersionUID = 1L;

        @Override
        public Long getId() {
          return optionId;
        }

        @Override
        public EntityLongPK getPk() {
          return new EntityLongPK(getId());
        }
      }, SitzberechnungsalgorithmusOptionenAuswaehlenErgebnisKonfliktCustomData.formatKonfliktLosoption(ergebnisKonfliktCustomDataFormatter, optionId, option), gebiet));
    }

    return result;
  }

  private SitzberechnungsalgorithmusOptionContext<T> createContext(AbstractWahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO, AbstractPolitischeGruppeDAO politischeGruppeDAO,
      AbstractWahlvorschlagDAO wahlvorschlagDAO, AbstractKandidaturDAO kandidaturDAO) {
    return new SitzberechnungsalgorithmusOptionContext<>(getLosentscheidTyp(), attachGebiete(wahldurchfuehrungsgebietDAO), attachGruppen(politischeGruppeDAO), attachWahlvorschlaege(wahlvorschlagDAO),
        attachKandidaturen(kandidaturDAO), getVerfuegbareOptionen());
  }

  private Map<Long, T> collectOptionPerId(Collection<T> optionen) {
    return optionen.stream().collect(toMap(SitzberechnungsalgorithmusOption::getId, Function.identity()));
  }

  private Collection<Long> extractIds(Collection<T> optionen) {
    return optionen.stream().map(this::extractId).filter(Objects::nonNull).collect(toList());
  }

  @Nullable
  private Long extractId(T option) {
    if (option != null) {
      return option.getId();
    }

    return null;
  }

  private Collection<T> attachOptionen(Collection<Long> ids) {
    return attachEntities(this::attachOption, ids);
  }

  @Nullable
  private T attachOption(Long id) {
    return attachEntity(getVerfuegbareOptionPerId()::get, id);
  }

  private Collection<Gebiet> attachGebiete(AbstractWahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO) {
    return attachEntities(wahldurchfuehrungsgebietDAO::findByPrimaryKey, getGebietPKs());
  }

  private Collection<Gruppe> attachGruppen(AbstractPolitischeGruppeDAO politischeGruppeDAO) {
    return attachEntities(politischeGruppeDAO::findByPrimaryKey, getGruppePKs());
  }

  private Collection<Wahlvorschlag> attachWahlvorschlaege(AbstractWahlvorschlagDAO wahlvorschlagDAO) {
    return attachEntities(wahlvorschlagDAO::findByPrimaryKey, getWahlvorschlagPKs());
  }

  private Collection<Kandidatur> attachKandidaturen(AbstractKandidaturDAO kandidaturDAO) {
    return attachEntities(kandidaturDAO::findByPrimaryKey, getKandidaturPKs());
  }

  private <E, K> Collection<E> attachEntities(Function<K, E> attachFct, Collection<K> pks) {
    return pks.stream().map(pk -> attachEntity(attachFct, pk)).filter(Objects::nonNull).collect(toList());
  }

  @Nullable
  private <E, K> E attachEntity(Function<K, E> attachFct, K pk) {
    if (pk != null) {
      return attachFct.apply(pk);
    }

    return null;
  }

  @Override
  public void loeseKonfliktAutomatisch() {
    setGelosteOptionen(waehleDieErstenAus(getAnzahlZuLosen(), getBeteiligteOptionen()));
  }

  @Override
  public void loeseKonfliktAutomatisch(int indexDerLoesung) {
    setGelosteOptionen(new SublistList<>(asSortedList(getBeteiligteOptionen()), (int) getAnzahlZuLosen()).get(indexDerLoesung));
  }

  @Override
  public void loeseKonfliktAutomatischZyklisch(int indexDerLoesung) {
    setGelosteOptionen(waehleZyklischAus(indexDerLoesung, getAnzahlZuLosen(), getBeteiligteOptionen()));
  }

  @Override
  public BigInteger getAnzahlLoesungen() {
    return BigIntegerMath.binomial(getAnzahlZyklischerLoesungen(), (int) getAnzahlZuLosen());
  }

  @Override
  public int getAnzahlZyklischerLoesungen() {
    return getBeteiligteOptionen().size();
  }

  @Override
  public String getClassName() {
    return SitzberechnungsalgorithmusOptionenAuswaehlenTypisiertErgebnisKonfliktCustomData.class.getName();
  }

  @Override
  public int hashCode() {
    return Objects.hash(gebietPKs, gruppePKs, kandidaturPKs, losentscheidTyp, wahlvorschlagPKs);
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }

    if (obj == null) {
      return false;
    }

    if (getClass() != obj.getClass()) {
      return false;
    }

    SitzberechnungsalgorithmusOptionenAuswaehlenTypisiertErgebnisKonfliktCustomData< ? > other = (SitzberechnungsalgorithmusOptionenAuswaehlenTypisiertErgebnisKonfliktCustomData< ? >) obj;

    if (!Objects.equals(gebietPKs, other.gebietPKs)) {
      return false;
    }

    if (!Objects.equals(gruppePKs, other.gruppePKs)) {
      return false;
    }

    if (!Objects.equals(kandidaturPKs, other.kandidaturPKs)) {
      return false;
    }

    if (!Objects.equals(losentscheidTyp, other.losentscheidTyp)) {
      return false;
    }

    if (!Objects.equals(wahlvorschlagPKs, other.wahlvorschlagPKs)) {
      return false;
    }

    return true;
  }

  /*
   * Default-Implementierungen...
   */

  @Override
  protected Collection<GebietPK> getBeteiligteGebietPKs() {
    return emptySet();
  }

  @Override
  protected Collection<GebietPK> getGelosteGebietPKs() {
    return emptySet();
  }

  @Override
  protected Collection<GruppePK> getBeteiligtePolitischeGruppenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<GruppePK> getGelostePolitischeGruppenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<WahlvorschlagPK> getBeteiligteListenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<WahlvorschlagPK> getGelosteListenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<ListenverbindungPK> getBeteiligteListenverbindungenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<ListenverbindungPK> getGelosteListenverbindungenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<PersonendatenPK> getBeteiligteKandidaturenPKs() {
    return emptySet();
  }

  @Override
  protected Collection<PersonendatenPK> getGelosteKandidaturenPKs() {
    return emptySet();
  }
}
