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

import de.ivu.elect.business.ergebnis.entity.WahlbeteiligungWahlberechtigeQuelle;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.internet.control.GebietUndStimmabgabeart;
import de.ivu.elect.business.sitzberechnung.control.wahlbeteiligung.BerechnungWahlbeteiligungMittelwertGewichtungsart;
import de.ivu.elect.business.sitzberechnung.control.wahlbeteiligung.WahlberechnungContextDaten;
import de.ivu.elect.business.sitzberechnung.control.wahlbeteiligung.Wahlbeteiligungsberechner;
import de.ivu.elect.business.sitzberechnung.control.wahlbeteiligung.Wahlbeteiligungsgebiet;

import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static de.ivu.elect.util.NotNull.notNull;

public class ErgebnisDatenWahlbeteiligung {

  private final Map<Wahldurchfuehrungsgebiet, Wahlbeteiligungsgebiet> wahlbeteiligungsgebiete = new HashMap<>();

  private final Map<Wahlbeteiligungsgebiet, Wahldurchfuehrungsgebiet> wahlbeteiligungsgebieteInversed = new HashMap<>();

  private final ErgebnisDaten ergebnisDaten;

  private final double defaultRuecklaufquote;

  private final BerechnungWahlbeteiligungMittelwertGewichtungsart mittelwertGewichtungsart;

  private final int anzahlNachkommastellen;

  private final WahlbeteiligungWahlberechtigeQuelle wahlberechtigeQuelle;

  private final Map<GebietUndStimmabgabeart, BigDecimal> cache = new HashMap<>();

  public ErgebnisDatenWahlbeteiligung(ErgebnisDaten ergebnisDaten,
                                      BerechnungWahlbeteiligungMittelwertGewichtungsart mittelwertGewichtungsart,
                                      double defaultRuecklaufquote,
                                      int anzahlNachkommastellen,
                                      WahlbeteiligungWahlberechtigeQuelle wahlberechtigeQuelle) {
    this.ergebnisDaten = ergebnisDaten;
    this.defaultRuecklaufquote = defaultRuecklaufquote;
    this.mittelwertGewichtungsart = mittelwertGewichtungsart;
    this.anzahlNachkommastellen = anzahlNachkommastellen;
    this.wahlberechtigeQuelle = wahlberechtigeQuelle;
  }

  public BigDecimal getWahlbeteiligung(GebietUndStimmabgabeart gebietUndStimmabgabeart) {
    return cache.computeIfAbsent(gebietUndStimmabgabeart, x -> getWahlbeteiligung(gebietUndStimmabgabeart.getWahldurchfuehrungsgebiet()));
  }

  private BigDecimal getWahlbeteiligung(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    Wahlbeteiligungsgebiet wahlbeteiligungsgebiet = getWahlbeteiligungsgebiet(wahldurchfuehrungsgebiet);
    Wahlbeteiligungsberechner wahlbeteiligungsberechner = new Wahlbeteiligungsberechner(mittelwertGewichtungsart, getContextDaten());
    double wahlbeteiligungAlsDouble = wahlbeteiligungsberechner.berechneWahlbeteiligung(wahlbeteiligungsgebiet);
    return new BigDecimal(wahlbeteiligungAlsDouble).setScale(anzahlNachkommastellen, BigDecimal.ROUND_HALF_UP);
  }

  private Wahlbeteiligungsgebiet getWahlbeteiligungsgebiet(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    // Kein computeIfAbsent (rekursiver Aufruf)
    if (!wahlbeteiligungsgebiete.containsKey(wahldurchfuehrungsgebiet)) {
      wahlbeteiligungsgebiete.put(wahldurchfuehrungsgebiet, erzeugeWahlbeteiligungsgebietZu(wahldurchfuehrungsgebiet));
    }
    return wahlbeteiligungsgebiete.get(wahldurchfuehrungsgebiet);
  }

  private Wahldurchfuehrungsgebiet getWahldurchfuehrungsgebiet(Wahlbeteiligungsgebiet wahlbeteiligungsgebiet) {
    return wahlbeteiligungsgebieteInversed.get(wahlbeteiligungsgebiet);
  }

  private Wahlbeteiligungsgebiet erzeugeWahlbeteiligungsgebietZu(Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    Wahlbeteiligungsgebiet result;

    boolean isErfassungsgebiet = wahldurchfuehrungsgebiet.isErfassungsgebiet();
    boolean isUrnenwahlgebiet = wahldurchfuehrungsgebiet.isUrnenwahlgebiet();
    boolean isBriefwahlgebiet = wahldurchfuehrungsgebiet.isBriefwahlgebiet();
    boolean isWahlgebiet = wahldurchfuehrungsgebiet.isWahlgebiet();

    result = new Wahlbeteiligungsgebiet(wahldurchfuehrungsgebiet.getNummer(), wahldurchfuehrungsgebiet.getName(), isErfassungsgebiet, isUrnenwahlgebiet,
            isBriefwahlgebiet, isWahlgebiet);

    wahlbeteiligungsgebiete.put(wahldurchfuehrungsgebiet, result);
    wahlbeteiligungsgebieteInversed.put(result, wahldurchfuehrungsgebiet);

    return result;
  }

  private WahlberechnungContextDaten getContextDaten() {
    return new WahlberechnungContextDaten() {
      @Override
      public boolean isErfassungsgebiet(Wahlbeteiligungsgebiet gebiet) {
        return getWahldurchfuehrungsgebiet(gebiet).isErfassungsgebiet();
      }

      @Override
      public long getAnzahlWahlberechtigteInsgesamt(Wahlbeteiligungsgebiet erfassungsgebiet) {
        Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet = getWahldurchfuehrungsgebiet(erfassungsgebiet);
        return notNull(wahlberechtigeQuelle.bestimmeAnzahlWahlberechtigte(
                () -> ergebnisDaten.getWahlberechtigteGesamtFuerErfassungsgebietUnterhalbVonAnzahlGesamt(wahldurchfuehrungsgebiet),
                wahldurchfuehrungsgebiet::getAnzahlWahlberechtigterGesamtA1A2A3)
        );
      }

      @Override
      public long getAnzahlWahlberechtigteMitWahlschein(Wahlbeteiligungsgebiet erfassungsgebiet) {
        return notNull(
                ergebnisDaten.getWahlberechtigteMitWahlscheinFuerErfassungsgebietUnterhalbVonAnzahl(getWahldurchfuehrungsgebiet(erfassungsgebiet)));
      }

      private transient Map<Wahlbeteiligungsgebiet, Boolean> kopfzahlwerteVollstaendigErfasstCache = new HashMap<>();

      @Override
      public boolean isKopfzahlwerteVollstaendigErfasst(Wahlbeteiligungsgebiet gebiet) {
        // copmuteIfAbsent ist hier nicht moeglich, weil die Methode nochmals bei den untergeordneten Gebieten aufgerufen wird, bevor die Berechnung fertig ist.
        if (!kopfzahlwerteVollstaendigErfasstCache.containsKey(gebiet)) {
          boolean result = isEinObergebietMitKomplettErfasstenKopfzahlwerte(gebiet) || isEinErfassungsgebietMitKomplettErfasstenKopfzahlwerte(gebiet);
          kopfzahlwerteVollstaendigErfasstCache.put(gebiet, result);
        }
        return kopfzahlwerteVollstaendigErfasstCache.get(gebiet);
      }

      private boolean isEinObergebietMitKomplettErfasstenKopfzahlwerte(Wahlbeteiligungsgebiet gebiet) {
        if (isErfassungsgebiet(gebiet)) {
          return false;
        }
        return isAlleUntergebieteMitKomplettErfasstenKopfzahlwerte(gebiet);
      }

      private boolean isEinErfassungsgebietMitKomplettErfasstenKopfzahlwerte(Wahlbeteiligungsgebiet gebiet) {
        if (!isErfassungsgebiet(gebiet)) {
          return false;
        }
        if (gebiet.isBriefwahlgebiet()) {
          return hasAnyWaehler(gebiet);
        }
        return hasAnyWahlberechtigte(gebiet) && hasAnyWaehler(gebiet);
      }

      private boolean hasAnyWaehler(Wahlbeteiligungsgebiet gebiet) {
        return getAnzahlWaehlerInsgesamt(gebiet) > 0;
      }

      private boolean hasAnyWahlberechtigte(Wahlbeteiligungsgebiet gebiet) {
        return getAnzahlWahlberechtigteInsgesamt(gebiet) > 0;
      }

      private long getAnzahlWaehlerInsgesamt(Wahlbeteiligungsgebiet gebiet) {
        return getAnzahlWaehlerMitWahlschein(gebiet) + getAnzahlWaehlerOhneWahlschein(gebiet);
      }

      private boolean isAlleUntergebieteFallsVorhandenAusgezaehlt(Wahlbeteiligungsgebiet gebiet) {
        return !isUntergebieteVorhanden(gebiet) || isAlleUntergebieteMitKomplettErfasstenKopfzahlwerte(gebiet);
      }

      private boolean isAlleUntergebieteMitKomplettErfasstenKopfzahlwerte(Wahlbeteiligungsgebiet obergebiet) {
        return getUntergeordneteGebiete(obergebiet).stream().allMatch(this::isKopfzahlwerteVollstaendigErfasst);
      }

      @Override
      public long getAnzahlWaehlerOhneWahlschein(Wahlbeteiligungsgebiet erfassungsgebiet) {
        Long waehlerGesamt = ergebnisDaten.getWaehlerGesamt(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        Long waehlerOhneWahlschein = ergebnisDaten.getWaehlerOhneWahlschein(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        Long waehlerMitWahlschein = ergebnisDaten.getWaehlerMitWahlschein(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        return bestimmeWaehlerOhneWahlschein(waehlerGesamt, waehlerOhneWahlschein, waehlerMitWahlschein);
      }

      private long bestimmeWaehlerOhneWahlschein(Long waehlerGesamt, Long waehlerOhneWahlschein, Long waehlerMitWahlschein) {
        if (waehlerOhneWahlschein != null && waehlerOhneWahlschein > 0) {
          return waehlerOhneWahlschein;
        }

        if (waehlerGesamt != null && waehlerMitWahlschein != null && waehlerGesamt >= waehlerMitWahlschein) {
          return waehlerGesamt - waehlerMitWahlschein;
        }
        return 0;
      }

      @Override
      public long getAnzahlWaehlerMitWahlschein(Wahlbeteiligungsgebiet erfassungsgebiet) {
        Long waehlerGesamt = ergebnisDaten.getWaehlerGesamt(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        Long waehlerOhneWahlschein = ergebnisDaten.getWaehlerOhneWahlschein(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        Long waehlerMitWahlschein = ergebnisDaten.getWaehlerMitWahlschein(getWahldurchfuehrungsgebiet(erfassungsgebiet));
        return bestimmeWaehlerMitWahlschein(waehlerGesamt, waehlerOhneWahlschein, waehlerMitWahlschein);
      }

      private long bestimmeWaehlerMitWahlschein(Long waehlerGesamt, Long waehlerOhneWahlschein, Long waehlerMitWahlschein) {
        if (waehlerOhneWahlschein != null && waehlerOhneWahlschein.equals(waehlerMitWahlschein)) {
          return 0;
        }
        if (waehlerMitWahlschein != null && waehlerMitWahlschein > 0) {
          return waehlerMitWahlschein;
        }
        if (waehlerGesamt != null && waehlerOhneWahlschein != null && waehlerGesamt >= waehlerOhneWahlschein) {
          return waehlerGesamt - waehlerOhneWahlschein;
        }
        return 0;
      }


      @Override
      public List<Wahlbeteiligungsgebiet> getUntergeordneteGebiete(Wahlbeteiligungsgebiet obergebiet) {
        return ergebnisDaten.getUntergeordneteGebiete(getWahldurchfuehrungsgebiet(obergebiet)).stream()
                .map(wahldurchfuehrungsgebiet -> getWahlbeteiligungsgebiet(wahldurchfuehrungsgebiet))
                .collect(Collectors.toList());
      }

      @Nullable
      @Override
      public Wahlbeteiligungsgebiet getUebergeordnetesGebiet(Wahlbeteiligungsgebiet gebiet) {
        Gebiet uebergeordnetesGebiet = getWahldurchfuehrungsgebiet(gebiet).getUebergeordnetesGebiet();
        if (uebergeordnetesGebiet != null) {
          return getWahlbeteiligungsgebiet(uebergeordnetesGebiet.castTo(Wahldurchfuehrungsgebiet.class).getEntity());
        }
        return null;
      }

      @Override
      public double getRuecklaufquote(Wahlbeteiligungsgebiet gebiet) {
        return defaultRuecklaufquote;
      }
    };
  }
}
