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

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;

import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.entity.Stimmabgabeart;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.GebietDatenWahlbeteiligung;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmen;
import de.ivu.elect.business.hochrechnung.entity.ReferendumFrage;
import de.ivu.elect.business.hochrechnung.entity.WahlteilnehmerAnzahl;
import de.ivu.elect.business.internet.control.GebietUndStimmabgabeart;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;


import static de.ivu.elect.business.hochrechnung.control.StimmenHochrechnerHilfsfunktionen.notNull;

public abstract class GebietDatenVonErgebnisdaten implements GebietDaten<Long> {

  private final Gebiet gebiet;

  private final WahlteilnehmerAnzahl anzahlWahlberechtigte;
  private final WahlteilnehmerAnzahl anzahlWaehler;

  private final Map<Stimmenart, Long> gueltige;
  private final Map<Stimmenart, Long> ungueltige;

  private final Boolean ausgezaehlt;
  private final GebietDatenWahlbeteiligung wahlbeteiligung;

  private final Map<PolitischeGruppe, ParteiStimmen<Long>> parteiStimmen;
  private final Map<Gruppe, ReferendumFrage<Long>> referendumFragen;


  /**
   * VORSICHT
   * @param  ergebnisDatenAktuell ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuell.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuell.getZweitstimmenAktuellGueltige
   */
  GebietDatenVonErgebnisdaten(Wahldurchfuehrungsgebiet gebiet, ErgebnisDaten ergebnisDatenAktuell) {
    this.gebiet = gebiet;
    //GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);
    this.anzahlWahlberechtigte = getAWerteFuerGebiet(gebiet, ergebnisDatenAktuell);
    this.anzahlWaehler = getWahlerAnzahlVonWahlgebietFuerGebiet(gebiet, ergebnisDatenAktuell);
    this.wahlbeteiligung = new GebietDatenWahlbeteiligung(anzahlWahlberechtigte, anzahlWaehler);

    this.parteiStimmen = Collections.unmodifiableMap(extractParteiStimmenMapVonErgebnisDatenFuerGebiet(ergebnisDatenAktuell, gebiet));
    this.referendumFragen = Collections.unmodifiableMap(extractReferendumFragenVonErgebnisDatenFuerGebiet(ergebnisDatenAktuell, gebiet));

    this.ausgezaehlt = isAusgezaehlt(this.parteiStimmen, this.referendumFragen);

    this.gueltige = Collections.unmodifiableMap(makeSystemgruppenStimmenMap(getErststimmenGueltigeFuerGebiet(ergebnisDatenAktuell, gebiet),
        getZweitstimmenGueltigeFuerGebiet(ergebnisDatenAktuell, gebiet), this.ausgezaehlt, ergebnisDatenAktuell));

    this.ungueltige = Collections.unmodifiableMap(makeSystemgruppenStimmenMap(getErststimmenUngueltigeFuerGebiet(ergebnisDatenAktuell, gebiet),
        getZweitstimmenUngueltigeFuerGebiet(ergebnisDatenAktuell, gebiet), this.ausgezaehlt, ergebnisDatenAktuell));
  }

  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuelleWahl ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuelleWahl.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuelleWahl.getZweitstimmenAktuellGueltige
   */
  abstract Function<GebietUndStimmabgabeart, Long> getWahlberechtigteOhneWahlscheinSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);
  abstract Function<GebietUndStimmabgabeart, Long> getWahlberechtigteMitWahlscheinSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);
  abstract Function<GebietUndStimmabgabeart, Long> getWahlberechtigteNichtImWaehlerverzeichnisSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);
  abstract Function<GebietUndStimmabgabeart, Long> getWahlberechtigteGesamtSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);

  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuelleWahl ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuelleWahl.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuelleWahl.getZweitstimmenAktuellGueltige
   */
  abstract Function<Wahldurchfuehrungsgebiet, Long> getWaehlerOhneWahlscheinSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);
  abstract Function<Wahldurchfuehrungsgebiet, Long> getWaehlerMitWahlscheinSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);
  abstract Function<Wahldurchfuehrungsgebiet, Long> getWaehlerGesamtSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);


  abstract Function<GebietUndStimmabgabeart, Long> getErststimmenGueltigeSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);

  private Long getErststimmenGueltigeFuerGebiet(ErgebnisDaten ergebnisDatenAktuelleWahl, Wahldurchfuehrungsgebiet gebiet) {
    if (ergebnisDatenAktuelleWahl == null || gebiet == null) {
      return null;
    }
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);
    return getErststimmenGueltigeSupplier(ergebnisDatenAktuelleWahl).apply(gebietUndStimmabgabeart);
  }

  abstract Function<GebietUndStimmabgabeart, Long> getZweitstimmenGueltigeSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);

  private Long getZweitstimmenGueltigeFuerGebiet(ErgebnisDaten ergebnisDatenAktuelleWahl, Wahldurchfuehrungsgebiet gebiet) {
    if (ergebnisDatenAktuelleWahl == null || gebiet == null) {
      return null;
    }
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);
    return getZweitstimmenGueltigeSupplier(ergebnisDatenAktuelleWahl).apply(gebietUndStimmabgabeart);
  }

  abstract Function<GebietUndStimmabgabeart, Long> getErststimmenUngueltigeSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);


  private Long getErststimmenUngueltigeFuerGebiet(ErgebnisDaten ergebnisDatenAktuelleWahl, Wahldurchfuehrungsgebiet gebiet) {
    if (ergebnisDatenAktuelleWahl == null || gebiet == null) {
      return null;
    }
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);
    return getErststimmenUngueltigeSupplier(ergebnisDatenAktuelleWahl).apply(gebietUndStimmabgabeart);
  }

  abstract Function<GebietUndStimmabgabeart, Long> getZweitstimmenUngueltigeSupplier(ErgebnisDaten ergebnisDatenAktuelleWahl);


  private Long getZweitstimmenUngueltigeFuerGebiet(ErgebnisDaten ergebnisDatenAktuelleWahl, Wahldurchfuehrungsgebiet gebiet) {
    if (ergebnisDatenAktuelleWahl == null || gebiet == null) {
      return null;
    }
    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);
    return getZweitstimmenUngueltigeSupplier(ergebnisDatenAktuelleWahl).apply(gebietUndStimmabgabeart);
  }

  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuell ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuell.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuell.getZweitstimmenAktuellGueltige
   */
  abstract Map<PolitischeGruppe, Long> getErstStimmenFuerGebiet(ErgebnisDaten ergebnisDatenAktuell, GebietUndStimmabgabeart gebietUndStimmabgabeart);
  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuell ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuell.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuell.getZweitstimmenAktuellGueltige
   */
  abstract Map<PolitischeGruppe, Long> getZweitStimmenFuerGebiet(ErgebnisDaten ergebnisDatenAktuell, GebietUndStimmabgabeart gebietUndStimmabgabeart);
  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuell ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuell.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuell.getZweitstimmenAktuellGueltige
   */
  abstract Function<PolitischeGruppe, Map<Listenkandidatur, Long>> getListenKandidaturStimmenGetterFuerGebiet(ErgebnisDaten ergebnisDatenAktuell, Wahldurchfuehrungsgebiet gebiet);

  private Boolean isAusgezaehlt(Map<PolitischeGruppe, ParteiStimmen<Long>> parteiStimmen, Map<Gruppe, ReferendumFrage<Long>> referendumFragen) {
    return hasParteiStimmenAnyAnzahlStimmenNotNull(parteiStimmen) || hasReferendumFragenAnyAnzahlStimmenNotNull(referendumFragen);
  }

  private Boolean hasParteiStimmenAnyAnzahlStimmenNotNull(Map<PolitischeGruppe, ParteiStimmen<Long>> parteiStimmen) {
    return parteiStimmen.values().stream().anyMatch(this::hasParteiStimmenAnyAnzahlStimmenNotNull);
  }

  private Boolean hasReferendumFragenAnyAnzahlStimmenNotNull(Map<Gruppe, ReferendumFrage<Long>> referendumFragen) {
    return referendumFragen.values().stream().anyMatch(this::hasReferendumFrageAnyAnzahlStimmenNotNull);
  }

  private boolean hasReferendumFrageAnyAnzahlStimmenNotNull(ReferendumFrage<Long> referendumFrage) {
    return referendumFrage != null && referendumFrage.getStimmenPerReferendumOption().values().stream().anyMatch(x -> notNull(x) > 0);
  }

  private boolean hasParteiStimmenAnyAnzahlStimmenNotNull(ParteiStimmen<Long> parteiStimmen) {
    return parteiStimmen != null && hasStimmenMapAnyAnzahlStimmenNotNull(parteiStimmen.getStimmen());
  }

  private boolean hasStimmenMapAnyAnzahlStimmenNotNull(Map<Stimmenart, Long> stimmen) {
    return stimmen.values().stream().anyMatch(x -> notNull(x) > 0);
  }

  Double getRatio(Long dividend, Long divisor) {
    if (dividend == null || divisor  == null || divisor  == 0L) {
      return null;
    }
    return 1.0 * dividend / divisor;
  }

  private Map<PolitischeGruppe, ParteiStimmen<Long>> extractParteiStimmenMapVonErgebnisDatenFuerGebiet(ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet gebiet) {
    if (ergebnisDaten == null || gebiet == null) {
      return new HashMap<>();
    }

    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);

    boolean direktkandudatenVorhanden = ergebnisDaten.isDirektkandidatenVorhanden();
    boolean listenVorhanden = ergebnisDaten.isListenVorhanden();
    boolean listenkandidaturStimmenVorhanden = isListenkandidaturStimmenVorhanden(ergebnisDaten);

    Map<PolitischeGruppe, Long> erststimmen, zweitstimmen;
    erststimmen = zweitstimmen = null;
    Set<PolitischeGruppe> politischeGruppen = new HashSet<>();
    boolean erststimmenVorhanden, zweitstimmenVorhanden;
    erststimmenVorhanden = zweitstimmenVorhanden = false;

    if (direktkandudatenVorhanden) {
      erststimmen = getErstStimmenFuerGebiet(ergebnisDaten, gebietUndStimmabgabeart);
      assert erststimmen != null: "Erststimmen-Map darf nicht null sein.";
      erststimmenVorhanden = isStimmenVorhanden(erststimmen);
      politischeGruppen.addAll(erststimmen.keySet());
    }
    if (listenVorhanden) {
      zweitstimmen = getZweitStimmenFuerGebiet(ergebnisDaten, gebietUndStimmabgabeart);
      assert zweitstimmen != null: "Zweitstimmen-Map darf nicht null sein.";
      zweitstimmenVorhanden = isStimmenVorhanden(zweitstimmen);
      politischeGruppen.addAll(zweitstimmen.keySet());
    }

    return getParteiStimmenMap(ergebnisDaten, gebietUndStimmabgabeart,
        direktkandudatenVorhanden,
        listenVorhanden,
        listenkandidaturStimmenVorhanden,
        erststimmen,
        zweitstimmen,
        politischeGruppen, erststimmenVorhanden, zweitstimmenVorhanden);
  }

  private boolean isListenkandidaturStimmenVorhanden(ErgebnisDaten ergebnisDaten) {
    if (ergebnisDaten == null) {
      return false;
    }
    return ergebnisDaten.getListen().stream().anyMatch(liste -> !liste.getListenkandidaturen().isEmpty());
  }

  private Map<PolitischeGruppe, ParteiStimmen<Long>> getParteiStimmenMap(ErgebnisDaten ergebnisDaten, GebietUndStimmabgabeart gebietUndStimmabgabeart, boolean direktkandudatenVorhanden, boolean listenVorhanden, boolean listenkandidaturStimmenVorhanden, Map<PolitischeGruppe, Long> erststimmen, Map<PolitischeGruppe, Long> zweitstimmen, Set<PolitischeGruppe> politischeGruppen, boolean erststimmenVorhanden, boolean zweitstimmenVorhanden) {
    Map<PolitischeGruppe, ParteiStimmen<Long>> result = new HashMap<>();
    if (ergebnisDaten == null) {
      return result;
    }

    for (PolitischeGruppe politischeGruppe: politischeGruppen) {
      Map<Stimmenart, Long> stimmen = new HashMap<>();
      if (direktkandudatenVorhanden && erststimmenVorhanden && erststimmen.containsKey(politischeGruppe)) {
        stimmen.put(Stimmenart.DIREKTSTIMME, erststimmen.get(politischeGruppe));
      }
      if (listenVorhanden && zweitstimmenVorhanden && zweitstimmen.containsKey(politischeGruppe)) {
        stimmen.put(Stimmenart.LISTENSTIMME, zweitstimmen.get(politischeGruppe));
      }
      Map<Listenkandidatur, Long> listenKandidaturStimmen = listenkandidaturStimmenVorhanden?
          getListenKandidaturStimmenGetterFuerGebiet(ergebnisDaten, gebietUndStimmabgabeart.getWahldurchfuehrungsgebiet())
          .apply(politischeGruppe)
          : new HashMap<>();
      result.put(politischeGruppe, new ParteiStimmen<>(politischeGruppe, stimmen, listenKandidaturStimmen));
    }

    return result;
  }

  private boolean isStimmenVorhanden(Map<PolitischeGruppe, Long> stimmen) {
    return stimmen.values().stream().anyMatch(x -> x != null && x > 0);
  }

  private Map<Stimmenart, Long> makeSystemgruppenStimmenMap(Long erststimmen, Long zweitstimmen, boolean isAusgezaehlt, ErgebnisDaten ergebnisDaten) {
    if (ergebnisDaten == null) {
      return new HashMap<>();
    }

    boolean direktkandudatenVorhanden = ergebnisDaten.isDirektkandidatenVorhanden();
    boolean listenVorhanden = ergebnisDaten.isListenVorhanden();
    Map<Stimmenart, Long> result = new HashMap<>();
    if (direktkandudatenVorhanden) {
      result.put(Stimmenart.DIREKTSTIMME, isStimmenErwartet(isAusgezaehlt, erststimmen) ? erststimmen: null);
    }
    if (listenVorhanden) {
      result.put(Stimmenart.LISTENSTIMME, isStimmenErwartet(isAusgezaehlt, erststimmen) ? zweitstimmen: null);
    }
    return result;
  }

  private boolean isStimmenErwartet(boolean isAusgezaehlt, Long vorhandeneStimmen) {
    return isAusgezaehlt || notNull(vorhandeneStimmen) != 0L;
  }

  private WahlteilnehmerAnzahl getAWerteFuerGebiet(Wahldurchfuehrungsgebiet gebiet, ErgebnisDaten ergebnisDaten) {
    if (ergebnisDaten == null || gebiet == null) {
      return new WahlteilnehmerAnzahl();
    }

    GebietUndStimmabgabeart gebietUndStimmabgabeart = new GebietUndStimmabgabeart(gebiet, Stimmabgabeart.GESAMT);

    Long a1 = getWahlberechtigteOhneWahlscheinSupplier(ergebnisDaten).apply(gebietUndStimmabgabeart);
    Long a2 = getWahlberechtigteMitWahlscheinSupplier(ergebnisDaten).apply(gebietUndStimmabgabeart);
    Long a3 = getWahlberechtigteNichtImWaehlerverzeichnisSupplier(ergebnisDaten).apply(gebietUndStimmabgabeart);
    Long a = getWahlberechtigteGesamtSupplier(ergebnisDaten).apply(gebietUndStimmabgabeart);

    return getAnzahlWahlberechtigte(a1, a2, a3, a);
  }

  /**
   * VORSICHT!
   * @param  ergebnisDatenAktuell ist das aktuelle Ergebnis und man muss explizit die Vorperiode aufrufen
   *                                   z. B. ergebnisDatenAktuell.getZweitstimmenVorperiodeGueltige und nicht ergebnisDatenAktuell.getZweitstimmenAktuellGueltige
   */
  private WahlteilnehmerAnzahl getWahlerAnzahlVonWahlgebietFuerGebiet(Wahldurchfuehrungsgebiet gebiet, ErgebnisDaten ergebnisDatenAktuell) {
    WahlteilnehmerAnzahl result = new WahlteilnehmerAnzahl();

    result.setAnzahl(Stimmenart.OHNE_WAHLSCHEIN, getWaehlerOhneWahlscheinSupplier(ergebnisDatenAktuell).apply(gebiet));
    result.setAnzahl(Stimmenart.MIT_WAHLSCHEIN, getWaehlerMitWahlscheinSupplier(ergebnisDatenAktuell).apply(gebiet));
    result.setAnzahl(Stimmenart.GESAMT, getWaehlerGesamtSupplier(ergebnisDatenAktuell).apply(gebiet));
    result.korrigiereWennWaehlerNichtUnterOhneWSErfasstSind();
    return result;
  }

  WahlteilnehmerAnzahl getAnzahlWahlberechtigte(Long a1, Long a2, Long a3, Long a) {
    WahlteilnehmerAnzahl result = new WahlteilnehmerAnzahl();

    result.setAnzahl(Stimmenart.OHNE_WAHLSCHEIN, a1);
    result.setAnzahl(Stimmenart.MIT_WAHLSCHEIN, a2);
    result.setAnzahl(Stimmenart.NICHT_IM_WAEHLERVERZEICHNIS, a3);
    result.setAnzahl(Stimmenart.GESAMT, a);

    return result;
  }

  abstract Map<Gruppe, ReferendumFrage<Long>> extractReferendumFragenVonErgebnisDatenFuerGebiet(ErgebnisDaten ergebnisDatenAktuell, Wahldurchfuehrungsgebiet gebiet);

  @Override
  public Gebiet getGebiet() {
    return gebiet;
  }
  @Override
  public WahlteilnehmerAnzahl getAnzahlWahlberechtigteImGebiet() {
    return anzahlWahlberechtigte;
  }

  @Override
  public WahlteilnehmerAnzahl getAnzahlWaehlerImGebiet() {
    return anzahlWaehler;
  }

  @Override
  public Map<Stimmenart, Long> getGueltige() {
    return gueltige;
  }

  @Override
  public Map<Stimmenart, Long> getUngueltige() {
    return ungueltige;
  }

  @Override
  public boolean isErgebnisVorhanden() {
    return ausgezaehlt;
  }

  @Override
  public GebietDatenWahlbeteiligung getWahlbeteiligung() {
    return wahlbeteiligung;
  }

  @Override
  public Map<PolitischeGruppe, ParteiStimmen<Long>> getParteiStimmen() {
    return parteiStimmen;
  }

  @Override
  public Map<Gruppe, ReferendumFrage<Long>> getReferendumFragen() {
    return referendumFragen;
  }
}
