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

import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.control.GruppeByRelativerPositionComparator;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietDAO;
import de.ivu.elect.business.gruppeimgebiet.boundary.GruppeImGebietExtendedDAO;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebiet;
import de.ivu.elect.business.gruppeimgebiet.entity.GruppeImGebietTK;
import de.ivu.elect.business.mandant.entity.EntityMitMandant;
import de.ivu.elect.business.wahlvorschlag.entity.Wahlvorschlag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.persistence.Transient;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static org.apache.commons.lang3.StringUtils.isBlank;

public abstract class AbstractGruppe implements GruppeBase, EntityMitMandant {

  private static final Logger LOG = LoggerFactory.getLogger(AbstractGruppe.class);

  private static final long serialVersionUID = 1L;

  protected AbstractGruppe() {
    super();
  }

  public boolean hasKurzname() {
    return !isBlank(getKurzname());
  }

  public boolean hasName() {
    return !isBlank(getName());
  }

  public boolean hasSchluessel() {
    return !isBlank(getSchluessel());
  }

  public boolean isInReihenfolgeEnthalten(GruppeImGebietDAO gruppeImGebietDAO) {
    return gruppeImGebietDAO.findByCountPositionenFuerGruppe((Gruppe) this) > 0;
  }

  public boolean isInReihenfolgeEnthalten(GruppeImGebietExtendedDAO gruppeImGebietExtendedDAO) {
    return gruppeImGebietExtendedDAO.findByCountPositionenFuerGruppe((Gruppe) this) > 0;
  }

  // Zur Anzeige an der Oberfläche
  @Override
  public Boolean getInPraesentation() {
    // "System-Gruppen" werden gesondert in der Präsentation behandelt (für politische Gruppen wird diese Methode überschrieben)
    return null;
  }

  // Zur Anzeige an der Oberfläche
  public Boolean getMinderheit() {
    // "System-Gruppen" sind nie Minderheit (für politische Gruppen wird diese Methode überschrieben)
    return null;
  }

  @Transient
  private transient Supplier<Map<Gebiet, GruppeImGebiet>> gruppeImGebietNachGebiet;

  @Nullable
  @Deprecated // HANDLE WITH CARE
  public Supplier<Map<Gebiet, GruppeImGebiet>> getGruppeImGebietNachGebiet() {
    return gruppeImGebietNachGebiet;
  }

  public void setGruppeImGebietNachGebiet(Supplier<Map<Gebiet, GruppeImGebiet>> gruppeImGebietNachGebiet) {
    this.gruppeImGebietNachGebiet = gruppeImGebietNachGebiet;
  }

  @Transient
  private transient Set<GruppeImGebiet> gruppenImGebietLoaded = null;

  public boolean isGruppenImGebietLoaded() {
    return gruppenImGebietLoaded != null;
  }

  public void setGruppenImGebietLoaded(Set<GruppeImGebiet> gruppenImGebietLoaded) {
    this.gruppenImGebietLoaded = gruppenImGebietLoaded;
  }

  @Deprecated // teure Operation, möglichst nicht nutzen, es sei denn setGruppenImGebietLoaded wird vorher verwendet
  public Set<GruppeImGebiet> getGruppenImGebiet() {
    if (gruppenImGebietLoaded == null) {
      // Für Analyse aktivieren new RuntimeException().printStackTrace();
      LOG.info("*************************************************************************************************");
      LOG.info("******** GruppenImGebiet werden geladen: " + this + " ******************************");
      LOG.info("*************************************************************************************************");
      gruppenImGebietLoaded = new HashSet<>(getGruppeImGebietsByGruppe()); // ELECT-12124
      LOG.info("GruppenImGebiet werden geladen - done: " + gruppenImGebietLoaded.size());
      if (gruppenImGebietLoaded.isEmpty()) {
        gruppenImGebietLoaded = null; // Implementierung von getGruppeImGebietsByGruppe liefert initial nicht die Collection, die an Hibernate geht
        return Collections.emptySet();
      }
    }
    return gruppenImGebietLoaded;
  }

  public void addGruppeImGebiet(GruppeImGebiet gruppeImGebiet) {
    addGruppeImGebietByGruppe(gruppeImGebiet);
  }

  public void removeGruppeImGebiet(GruppeImGebiet gruppeImGebiet) {
    removeGruppeImGebietByGruppe(gruppeImGebiet);
  }

  public void removeAllGruppenImGebiet() {
    removeAllGruppeImGebietByGruppes();
  }

  @Deprecated // teuer, greift auf getGruppeImGebietsByGruppe zu
  public String getNameAufGebiet(Gebiet gebiet) {
    // Abkürzung bei Systemgruppen
    if (isSystemGruppe()) {
      return getName();
    }
    return getPropertyAufGebietResolver().getNameAufGebiet(gebiet);
  }

  @Deprecated // teuer, greift auf getGruppeImGebietsByGruppe zu
  public String getKurznameAufGebiet(Gebiet gebiet) {
    // Abkürzung bei Systemgruppen
    if (isSystemGruppe()) {
      return getKurzname();
    }
    return getPropertyAufGebietResolver().getKurznameAufGebiet(gebiet);
  }

  @Deprecated // teuer, greift auf getGruppeImGebietsByGruppe zu
  public String getZusatzbezeichnungAufGebiet(Gebiet gebiet) {
    // Abkürzung bei Systemgruppen
    if (isSystemGruppe()) {
      return getZusatzbezeichnung();
    }
    return getPropertyAufGebietResolver().getZusatzbezeichnungAufGebiet(gebiet);
  }

  @Deprecated // teuer, greift auf getGruppeImGebietsByGruppe zu nur für Destatis relevant
  public GruppeImGebiet getGruppeImGebietInDerDerNameOderKurznameWechselt(Gebiet gebiet) {
    return getPropertyAufGebietResolver().getGruppeImGebietInDerDerNameOderKurznameWechselt(gebiet);
  }

  public boolean isParteiWaehlergruppeOderVereinigung() {
    return isPartei() || isWaehlergruppe() || isVereinigung();
  }

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

  public boolean isWaehlergruppe() {
    return isPolitischeGruppe() && getGruppeArt().isWaehlergruppe();
  }

  public boolean isPartei() {
    return isPolitischeGruppe() && GruppeArt.PARTEI.equals(getGruppeArt());
  }

  public boolean isVereinigung() {
    return isPolitischeGruppe() && GruppeArt.VEREINIGUNG.equals(getGruppeArt());
  }

  public boolean isReferendumVorlage() {
    return isPolitischeGruppe() && GruppeArt.REFERENDUM_VORLAGE.equals(getGruppeArt());
  }

  public boolean isSystemGruppe() {
    // ELECT-9435
    if (getGruppeArt() != null) {
      if (!getGruppeArt().isPolitischeGruppe()) {
        return true;
      }
    }

    return !isPolitischeGruppeInternal();
  }

  public boolean isPolitischeGruppe() {
    return !isSystemGruppe(); // ELECT-9446
  }

  /**
   * Hinweis: Wird von PolitischeGruppe überschrieben
   */
  protected boolean isPolitischeGruppeInternal() {
    return false;
  }

  public boolean isGruppeEqualsSystemGruppe(SystemGruppe systemGruppe, SystemGruppeEigenschaften systemGruppeEigenschaften) {
    return isSystemGruppe() && systemGruppeEigenschaften.isGruppeEqualsSystemGruppe(this, systemGruppe);
  }

  private transient SystemGruppe systemGruppe;

  public SystemGruppe getSystemGruppe(SystemGruppeEigenschaften systemGruppeEigenschaften) {
    if (systemGruppe == null) {
      systemGruppe = Arrays.stream(SystemGruppe.values())
          .filter(systemGruppenKandidat -> getSchluessel().equals(systemGruppeEigenschaften.getSchluessel(systemGruppenKandidat))).findFirst()
          .orElse(systemGruppe);

    }
    if (systemGruppe == null) {
      throw new IllegalStateException("Kann keine System-Gruppe aus Schluessel: '" + getSchluessel() + "' ableiten");
    }
    return systemGruppe;
  }

  private transient GruppePropertyAufGebietResolver gruppePropertyAufGebietResolver;

  @Deprecated // teuer, greift auf getGruppeImGebietsByGruppe zu
  private GruppePropertyAufGebietResolver getPropertyAufGebietResolver() {
    if (gruppePropertyAufGebietResolver == null || hasChangedGruppeImGebietsByGruppe()) {
      gruppePropertyAufGebietResolver = new GruppePropertyAufGebietResolver((Gruppe) this, this::getGruppeImGebietNackTk);
    }

    return gruppePropertyAufGebietResolver;
  }

  @Transient
  private transient Map<GruppeImGebietTK, GruppeImGebiet> gruppeImGebietNackTk;

  @Deprecated // teure Operation
  private Map<GruppeImGebietTK, GruppeImGebiet> getGruppeImGebietNackTk() {
    if (gruppeImGebietNackTk == null || hasChangedGruppeImGebietsByGruppe()) {
      gruppeImGebietNackTk = GruppeImGebiet.mappeNachGruppeGebietSchluessel(getGruppenImGebiet());
    }
    return gruppeImGebietNackTk;
  }

  public abstract boolean hasChangedGruppeImGebietsByGruppe();

  public boolean hatZugelassenenDirektkandidatInGebietshierarchie(Gebiet gebiet, Map<Gruppe, Set<Wahlvorschlag>> alleWahlvorschlaegeNachGruppe) {
    // SystemGruppen haben keine Direktkandidaten
    return false;
  }

  public boolean hatListeInGebietshierarchie(Gebiet gebiet, Map<Gruppe, Set<Wahlvorschlag>> alleWahlvorschlaegeNachGruppe) {
    // SystemGruppen haben keine Listen
    return false;
  }

  public boolean isLogoVorhanden() {
    return getLogo() != null;
  }

  public static void relativePositionSetzenWennNoetig(Collection<Gruppe> neueEntities, Collection<Gruppe> entitiesInDb) {
    Map<Long, Gruppe> gruppeAnPosition = new HashMap<>();
    Set<Long> vergebenePositionen = new HashSet<>();
    for (Gruppe neueEntity : neueEntities) {
      if (neueEntity.getRelativePosition() != null) {
        vergebenePositionen.add(neueEntity.getRelativePosition());
        gruppeAnPosition.put(neueEntity.getRelativePosition(), neueEntity);
      }
    }
    for (Gruppe gruppe : entitiesInDb) {
      if (gruppe.getRelativePosition() != null) {
        vergebenePositionen.add(gruppe.getRelativePosition());
        gruppeAnPosition.put(gruppe.getRelativePosition(), gruppe);
      }
    }

    long letzteRelativePosition = 1L;
    for (Gruppe neueEntity : neueEntities) {
      if (neueEntity.getRelativePosition() == null || gruppeAnPosition.get(neueEntity.getRelativePosition()) != neueEntity) {
        while (vergebenePositionen.contains(letzteRelativePosition)) {
          letzteRelativePosition++;
        }
        vergebenePositionen.add(letzteRelativePosition);
        neueEntity.setRelativePosition(letzteRelativePosition); // Ursache für ELECT-16923
      }
    }
  }

  @Transient
  private transient Long berechnetePosition = null;

  public Long getBerechnetePosition(Collection<? extends Gruppe> alleGruppen) {

    if (berechnetePosition == null) {
      berechnetePosition = bestimmeBerechnetePosition(alleGruppen);
    }
    return berechnetePosition;
  }

  protected Long bestimmeBerechnetePosition(Collection<? extends Gruppe> alleGruppen) {
    // wenn Position fixiert, dann diese zurückgeben
    if (getVorgegebenePosition() != null) {
      return getVorgegebenePosition();
    }

    Set<Long> benutztePositionen = alleGruppen.stream().filter(gruppe -> gruppe.getVorgegebenePosition() != null).map(Gruppe::getVorgegebenePosition)
        .collect(Collectors.toSet());

    List<Gruppe> alleGruppenSortiertNachRelativerPosition = new ArrayList<>(alleGruppen);

    alleGruppenSortiertNachRelativerPosition.sort(new GruppeByRelativerPositionComparator());

    long aktuellePosition = 1;
    for (Gruppe gruppe : alleGruppenSortiertNachRelativerPosition) {
      if (gruppe.equals(this)) {
        while (benutztePositionen.contains(aktuellePosition)) {
          aktuellePosition++;
        }
        return aktuellePosition;
      }

      if (gruppe.getVorgegebenePosition() == null) {
        while (benutztePositionen.contains(aktuellePosition)) {
          aktuellePosition++;
        }
        benutztePositionen.add(aktuellePosition);
        aktuellePosition++;
      }
    }
    return aktuellePosition;
  }

  public static Gruppe findeSystemGruppe(Collection<Gruppe> gruppen, SystemGruppe systemGruppe, SystemGruppeEigenschaften systemGruppeEigenschaften) {
    for (Gruppe gruppe : gruppen) {
      if (gruppe.isGruppeEqualsSystemGruppe(systemGruppe, systemGruppeEigenschaften)) {
        return gruppe;
      }
    }
    throw new IllegalStateException("Gruppe " + systemGruppe + " nicht gefunden, habe: " + gruppen);
  }

  @Override
  public EntityMitMandant getParent() {
    return getWahl();
  }

}
