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

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

import javax.annotation.Nullable;
import javax.inject.Inject;

import de.ivu.elect.business.ergebnis.boundary.StimmenWaehler;
import de.ivu.elect.business.ergebnis.entity.Stimmen;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.Gruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppe;
import de.ivu.elect.business.gruppe.entity.SystemGruppeEigenschaften;
import de.ivu.elect.business.kandidatur.entity.Direktkandidatur;
import de.ivu.elect.business.kandidatur.entity.Listenkandidatur;
import de.ivu.elect.business.stimmen.control.ErgebnisStimmenFilter;
import de.ivu.elect.business.wahlvorschlag.entity.Direktkandidat;
import de.ivu.elect.business.wahlvorschlag.entity.Liste;
import de.ivu.elect.business.wahlvorschlag.entity.ReferendumOption;

/**
 * Hilfsklasse für alles, was mit dem Summieren von Stimmen zu tun hat
 */
public class StimmenKumulierer implements Serializable {

  private static final long serialVersionUID = 1L;

  @Inject
  private transient SystemGruppeEigenschaften systemGruppeEigenschaften;

  public StimmenKumulierer() {
    super();
  }

  public Long getSummeGueltigeStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet, Stimmenart stimmenart) {
    return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, SystemGruppe.GUELTIGE, stimmenart);
  }

  public Long getSummeUngueltigeStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet, Stimmenart stimmenart) {
    return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, SystemGruppe.UNGUELTIGE, stimmenart);
  }

  public long getSummeWaehlerOhneWahlscheinStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.OHNE_WAHLSCHEIN);
  }

  public long getSummeWaehlerMitWahlscheinStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.MIT_WAHLSCHEIN);
  }

  public long getSummeWaehlerMitWahlscheinInUrnenwahlAnteilAusUrnenwahlStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.ALS_URNENWAHL_IN_URNE);
  }

  public long getSummeWaehlerMitWahlscheinInUrnenwahlAnteilAusBriefwahlStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.ALS_BRIEFWAHL_IN_URNE);
  }

  public long getSummeWaehlerMitVollmachtStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.MIT_VOLLMACHT);
  }

  public long getSummeWaehlerGesamtStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWaehlerStimmenAufGebiet(stimmenPool, gebiet, Stimmenart.GESAMT);
  }

  long getSummeWaehlerStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet, Stimmenart stimmenart) {
    SystemGruppe systemGruppe = SystemGruppe.WAEHLER;

    StimmenWaehler stimmenWaehler = new StimmenWaehler(gebiet, //
        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.GESAMT), //

        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.OHNE_WAHLSCHEIN), //

        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.MIT_WAHLSCHEIN), //
        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.ALS_URNENWAHL_IN_URNE), //
        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.ALS_BRIEFWAHL_IN_URNE), //

        () -> bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, Stimmenart.MIT_VOLLMACHT) //
    );

    switch (stimmenart) {
      case OHNE_WAHLSCHEIN :
        return stimmenWaehler.getAnzahlOhneWahlschein();
      case MIT_WAHLSCHEIN :
        return stimmenWaehler.getAnzahlMitWahlschein();
      case ALS_BRIEFWAHL_IN_URNE :
        return stimmenWaehler.getAnzahlMitWahlscheinInUrnenwahlAnteilAusBriefwahl();
      case ALS_URNENWAHL_IN_URNE :
        return stimmenWaehler.getAnzahlMitWahlscheinInUrnenwahlAnteilAusUrnenwahl();
      case MIT_VOLLMACHT :
        return stimmenWaehler.getAnzahlMitVollmacht();
      case GESAMT :
        return stimmenWaehler.getAnzahlGesamt();
    }

    return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, systemGruppe, stimmenart);
  }

  /**
   * @see StimmergebnisDaten#getErgebnisDatenWahlberechtigte
   * @see StimmergebnisDaten#getErgebnisDatenWahlberechtigteVorperiode
   */
  Long getSummeWahlberechtigteStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet, Stimmenart stimmenart) {
    switch (stimmenart) {
      case OHNE_WAHLSCHEIN :
        return getSummeWahlberechtigteOhneWahlscheinAusStimmen(stimmenPool, gebiet);
      case MIT_WAHLSCHEIN :
        return getSummeWahlberechtigteMitWahlscheinAusStimmen(stimmenPool, gebiet);
      case NICHT_IM_WAEHLERVERZEICHNIS :
        return getSummeWahlberechtigteNichtImWaehlerverzeichnisAusStimmen(stimmenPool, gebiet);
      case GESAMT :
        return getSummeWahlberechtigteGesamtAusStimmen(stimmenPool, gebiet);
    }

    throw new IllegalArgumentException("Stimmenart nicht für Wahlberechtigte erlaubt: " + stimmenart);
  }

  /**
   * @see StimmergebnisDaten#getWahlberechtigteGesamtVorperiode
   * @see StimmergebnisDaten#getWahlberechtigteGesamtVorperiodeFuerErfassungsgebietUnterhalbVon
   */
  public Long getSummeWahlberechtigteGesamtAusStimmen(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWahlberechtigteStimmenAufGebietAusStimmen(stimmenPool, gebiet, Stimmenart.GESAMT);
  }

  /**
   * @see StimmergebnisDaten#getWahlberechtigteOhneWahlscheinVorperiodeFuerErfassungsgebietUnterhalbVon
   */
  Long getSummeWahlberechtigteOhneWahlscheinAusStimmen(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWahlberechtigteStimmenAufGebietAusStimmen(stimmenPool, gebiet, Stimmenart.OHNE_WAHLSCHEIN);
  }

  /**
   * @see StimmergebnisDaten#getWahlberechtigteMitWahlscheinVorperiodeFuerErfassungsgebietUnterhalbVon
   */
  Long getSummeWahlberechtigteMitWahlscheinAusStimmen(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWahlberechtigteStimmenAufGebietAusStimmen(stimmenPool, gebiet, Stimmenart.MIT_WAHLSCHEIN);
  }

  /**
   * @see StimmergebnisDaten#getWahlberechtigteNichtImWaehlerverzeichnisVorperiodeFuerErfassungsgebietUnterhalbVon
   */
  Long getSummeWahlberechtigteNichtImWaehlerverzeichnisAusStimmen(StimmenPool stimmenPool, Gebiet gebiet) {
    return getSummeWahlberechtigteStimmenAufGebietAusStimmen(stimmenPool, gebiet, Stimmenart.NICHT_IM_WAEHLERVERZEICHNIS);
  }

  public Long getSummeWahlberechtigteStimmenAufGebietAusStimmen(StimmenPool stimmenPool, Gebiet gebiet, Stimmenart stimmenart) {
    return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, SystemGruppe.WAHLBERECHTIGTE, stimmenart);
  }

  /**
   * Stimmen kumulieren, wenn Systemgruppe übereinstimmt und Gebiet der Stimmen unterhalb des übergebenen Gebietes
   */
  public long bestimmeSummeSystemGruppenStimmenAufGebiet(StimmenPool stimmenPool, Gebiet gebiet, SystemGruppe systemGruppe, Stimmenart stimmenart) {
    long result = 0L;

    for (Stimmen stimmen : stimmenPool.getSystemgruppenStimmenUnterhalbVonGebiet().getOrDefault(gebiet, Collections.emptyList())) {
      if (stimmenart.equals(stimmen.getStimmenart()) && systemGruppe.equalsGruppeOhneSystemgruppenPruefung(stimmen.getGruppe(), getSystemGruppeEigenschaften())) { // ELECT-4284
        if (isStimmenInGebietOderDarunter(stimmen, gebiet)) {
          result += stimmen.getAnzahl();
        }
      }
    }

    return result;
  }

  protected boolean isStimmenInGebietOderDarunter(Stimmen stimmen, Gebiet gebiet) {
    // ELECT-3919 // nicht ergebnisrelevante Gebiete nicht kumulieren...
    if (!stimmen.getGebiet().isErgebnisRelevant()) {
      return stimmen.getGebiet().equals(gebiet);
    }

    return stimmen.getGebiet().isInGebietshierarchieUntergeordnetesGebietVonSchnelleVersionFuerMassenoperationen(gebiet);
  }

  public Long getSummeStimmenFuerListenkandidaturAufGebietUndDarunter(Listenkandidatur kandidatur, Gebiet gebiet, StimmenPool stimmenPool) {
    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenkandidaturstimmenAufErfassungsgebiet(kandidatur, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenkandidaturstimmen(kandidatur));
  }

  public Long getSummeStimmenFuerListenkandidaturNichtErgebnisrelevantAufGebietUndDarunter(Listenkandidatur kandidatur, Gebiet gebiet, StimmenPool stimmenPool) {
    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenkandidaturstimmenNichtErgebnisrelevantAufErfassungsgebiet(kandidatur, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenkandidaturstimmenNichtErgebnisrelevant(kandidatur));
  }

  public Long getSummeStimmenFuerDirektkandidaturAufGebietUndDarunter(Direktkandidatur kandidatur, Gebiet gebiet, StimmenPool stimmenPool,
                                                                      @Nullable Function<Gruppe, List<Direktkandidat>> direktKandidatenDerGruppe) {
    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getDirektkandidatStimmenAufErfassungsgebiet(kandidatur.getDirektkandidat(), gebiet, direktKandidatenDerGruppe);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getDirektkandidatStimmen(kandidatur.getDirektkandidat(), direktKandidatenDerGruppe));
  }

  public Long getSummeListenstimmenFuerPolitischeGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool) {
    if (gruppe.isSystemGruppe()) {
      return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, gruppe.getSystemGruppe(getSystemGruppeEigenschaften()), Stimmenart.LISTENSTIMME);
    }

    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenstimmenDerGruppeAufErfassungsgebiet(gruppe, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenstimmenDerGruppe(gruppe));
  }

  public Long getSummeListenstimmenAbzuziehenFuerPolitischeGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool) {
    if (gruppe.isSystemGruppe()) {
      return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, gruppe.getSystemGruppe(getSystemGruppeEigenschaften()), Stimmenart.LISTENSTIMME_ABZUZIEHEN);
    }

    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenstimmenAbzuziehenDerGruppeAufErfassungsgebiet(gruppe, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenstimmenAbzuziehenDerGruppe(gruppe));
  }

  public Long getSummeListenstimmenEinzelnerWahlvorschlagUnveraendertFuerPolitischeGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool) {
    if (gruppe.isSystemGruppe()) {
      return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, gruppe.getSystemGruppe(getSystemGruppeEigenschaften()), Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_UNVERAENDERT);
    }

    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppeAufErfassungsgebiet(gruppe, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenstimmenEinzelnerWahlvorschlagUnveraendertDerGruppe(gruppe));
  }

  public Long getSummeListenstimmenEinzelnerWahlvorschlagVeraendertFuerPolitischeGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool) {
    if (gruppe.isSystemGruppe()) {
      return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, gruppe.getSystemGruppe(getSystemGruppeEigenschaften()), Stimmenart.LISTENSTIMME_EINZELNER_WAHLVORSCHAG_VERAENDERT);
    }

    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppeAufErfassungsgebiet(gruppe, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getListenstimmenEinzelnerWahlvorschlagVeraendertDerGruppe(gruppe));
  }

  private Long summiereAufGebietUndDarunter(Gebiet gebiet, Collection<Stimmen> stimmenZumSummieren) {
    Long result = 0L;

    for (Stimmen stimmen : stimmenZumSummieren) {
      if (isStimmenInGebietOderDarunter(stimmen, gebiet)) {
        result += stimmen.getAnzahl();
      }
    }

    return result;
  }

  public Long getSummeDirektstimmenFuerPolitischeGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool) {
    if (gruppe.isSystemGruppe()) {
      return bestimmeSummeSystemGruppenStimmenAufGebiet(stimmenPool, gebiet, gruppe.getSystemGruppe(getSystemGruppeEigenschaften()), Stimmenart.DIREKTSTIMME);
    }

    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getDirektstimmenAufErfassungsgebiet(gruppe, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getDirektstimmen(gruppe));
  }

  public Long getSummeFuerSystemGruppeAufGebietUndDarunter(Gruppe gruppe, Gebiet gebiet, StimmenPool stimmenPool, Stimmenart stimmenart) {
    if (!gruppe.isSystemGruppe()) {
      throw new IllegalStateException("NUR FÜR SYSTEM GRUPPEN ERLAUBT!");
    }

    Long result = 0L;

    for (Stimmen stimmen : stimmenPool.getSystemgruppenStimmen()) {
      if (stimmenart.equals(stimmen.getStimmenart()) && stimmen.getListenkandidatur() == null && stimmen.getGruppe().equals(gruppe) && isStimmenInGebietOderDarunter(stimmen, gebiet)) {
        result += stimmen.getAnzahl();
      }
    }

    return result;
  }

  public Long getSummeDirektstimmenFuerListeAufGebietUndDarunter(Liste liste, Gebiet gebiet, StimmenPool stimmenPool) {
    return getSummeDirektstimmenFuerPolitischeGruppeAufGebietUndDarunter(liste.getGruppe(), gebiet, stimmenPool);
  }

  public Long getSummeListenstimmenFuerListeAufGebietUndDarunter(Liste liste, Gebiet gebiet, StimmenPool stimmenPool) {
    return getSummeListenstimmenFuerPolitischeGruppeAufGebietUndDarunter(liste.getGruppe(), gebiet, stimmenPool);
  }

  public SystemGruppeEigenschaften getSystemGruppeEigenschaften() {
    return systemGruppeEigenschaften;
  }

  public Long getSummeStimmenFuerReferendumOptionAufGebietUndDarunter(ReferendumOption referendumOption, Gebiet gebiet, StimmenPool stimmenPool) {
    // Abkürzung für Erfassungsgebiete
    if (gebiet.isErfassungsgebiet()) {
      Stimmen stimmenErfassungsgebiet = stimmenPool.getReferendumOptionStimmenAufErfassungsgebiet(referendumOption, gebiet);

      if (stimmenErfassungsgebiet == null) {
        return 0L;
      }

      return stimmenErfassungsgebiet.getAnzahl();
    }

    return summiereAufGebietUndDarunter(gebiet, stimmenPool.getReferendumOptionStimmen(referendumOption));
  }

  public static StimmenFilter createStimmenFilter(Predicate<Gebiet> gebietFilter, ErgebnisStimmenFilter ergebnisStimmenFilter) {
    return stimmen -> ergebnisStimmenFilter.filterStimmen(stimmen, gebietFilter);
  }
}
