
package de.ivu.elect.business.stimmeingabe.boundary;

import java.util.Collection;

import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import de.ivu.elect.business.dokumente.control.ProgressMonitor;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenCreator;
import de.ivu.elect.business.ergebnis.control.ErgebnisDatenHolder;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.hochrechnung.boundary.Hochrechnungsalgorithm;
import de.ivu.elect.business.hochrechnung.boundary.HochrechnungsalgorithmusKonfiguration;
import de.ivu.elect.business.hochrechnung.control.StimmenHochrechnerEingabedatenVonErgebnisdaten;
import de.ivu.elect.business.hochrechnung.entity.HochrechneralgorithmusAusfuehrbarkeitStatus;
import de.ivu.elect.business.hochrechnung.entity.HochrechnungsalgorithmusArt;
import de.ivu.elect.business.hochrechnung.entity.StimmenHochrechnerEingabedaten;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerAusgabedaten;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingang;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangErfasser;
import de.ivu.elect.business.stimmeingabe.entity.ErgebniseingangGenerierenProgress;
import de.ivu.elect.business.stimmeingabe.entity.Ergebniseingangsart;
import de.ivu.elect.business.wahl.boundary.WahldurchfuehrungDAO;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.jee.faces.Messages;

@Stateless
public class StimmeingabeFiktivewerteBatchCreate {

  /*
   * elect: dieser Wert sagt, dass wir bei Massenerzeugung fiktiver Ergebniseingänge aller 25 Gebiete ein flush()&clear() durchführen. Das macht bei mir einerseits etwa 1 GiB Heapspeicher, der zwischen
   * zwei flush()&clear()s belegt und dann wieder freigegeben wird; andererseits verbringen wir dann etwa 33 % der Zeit mit flushen.
   */
  private static final int MASSENOPERATION_ANZAHL_GEBIETE_BIS_ENTITY_MANAGER_CLEAR = 25;

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

  @PersistenceContext
  private EntityManager entityManager;

  @Inject
  private transient StimmeingabeFiktivewerteCreate stimmeingabeFiktivewerteCreate;

  @Inject
  private transient WahldurchfuehrungDAO wahldurchfuehrungDAO;
  @Inject
  private transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDao;

  @Inject
  private transient ErgebniseingangDAO ergebniseingangDao;

  @Inject
  private transient ErgebnisDatenCreator ergebnisDatenCreator;

  @Inject
  private transient Messages messages;

  public StimmeingabeFiktivewerteBatchCreate() {
    super();
  }

  @Asynchronous
  public void create(Wahldurchfuehrung wahldurchfuehrungNotAttached, Collection< ? extends Gebiet> gebiete, Ergebniseingangsart ergebniseingangsart, ErgebniseingangErfasser erfasser, ProgressMonitor monitor, HochrechnungsalgorithmusArt hochrechnungsalgorithmusArt, HochrechnungsalgorithmusKonfiguration hochrechnungsalgorithmusKonfiguration) {
    LOG.info("Erzeuge fiktive Ergebniseingaenge");

    HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus;

    Wahldurchfuehrung wahldurchfuehrung = wahldurchfuehrungDAO.attach(wahldurchfuehrungNotAttached);
    if (wahldurchfuehrung == null) {
      ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEIL_WAHL_NICHT_VORHANDEN_IST;
      brecheAb(monitor, ausfuehrbarkeitStatus);
      return;
    }


    int gebietCount = 0;

    if (hochrechnungsalgorithmusArt == null) {
      ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUS;
      brecheAb(monitor, ausfuehrbarkeitStatus);
      return;
    }

    if (hochrechnungsalgorithmusKonfiguration == null) {
      ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUSKONFIGURATION;
      brecheAb(monitor, ausfuehrbarkeitStatus);
      return;
    }

    log("Erzeuge StimmenHochrechnerEingabedaten");
    ErgebnisDatenHolder ergebnisDatenHolder = new ErgebnisDatenHolder(ergebnisDatenCreator);
    ErgebnisDaten ergebnisDatenAktuell = ergebnisDatenHolder.getErgebnisDaten(wahldurchfuehrung, ergebniseingangsart.korrespondierendeErgebnisart());
    StimmenHochrechnerEingabedaten<Long> eingabeDaten = getStimmenHochrechnerEingabedaten(ergebnisDatenAktuell);
    log("Erzeuge StimmenHochrechnerEingabedaten - done");

    ausfuehrbarkeitStatus = hochrechnungsalgorithmusArt.getAlgorithmusAusfuehrbarkeitStatus(eingabeDaten, gebiete);
    if (!ausfuehrbarkeitStatus.isAlgorithmusAusfuehrbar()) {
      brecheAb(monitor, ausfuehrbarkeitStatus);
      return;
    }

    Hochrechnungsalgorithm hochrechnungsalgorithmus = hochrechnungsalgorithmusArt.getHochrechnungsalgorithm(hochrechnungsalgorithmusKonfiguration);
    if (hochrechnungsalgorithmus == null) {
      ausfuehrbarkeitStatus = HochrechneralgorithmusAusfuehrbarkeitStatus.NICHT_AUSFUEHRBAR_WEGEN_AUSWAHL_HOCHRECHNUNGSALGORITHMUS;
      brecheAb(monitor, ausfuehrbarkeitStatus);
      return;
    }

    log("Ermittle hochgerechnete Stimmen");
    StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten = hochrechnungsalgorithmus.ermittleHochgerechneteStimmen(eingabeDaten);
    log("Ermittle hochgerechnete Stimmen - done");

    ausfuehrbarkeitStatus = stimmenhochrechnerAusgabedaten.getAusfuehrbarkeitStatus();
    LOG.info(messages.lokalisiere(ausfuehrbarkeitStatus));
    monitor.progress(ausfuehrbarkeitStatus, 1, 1);

    if (!ausfuehrbarkeitStatus.isAlgorithmusAusfuehrbar()) {
      monitor.progress(ErgebniseingangGenerierenProgress.ABGEBROCHEN_WEGEN_FEHLERN);
      return;
    }

    for (Gebiet gebiet : gebiete) {
      gebietCount++;
      log(String.format("Erzeuge fiktive Ergebniseingang fuer Gebiet %s", gebiet.getName()));

      try {
        createFallsErfassungsgebietUndUeberschreibenErlaubt(gebiet, ergebniseingangsart,
            ergebnisDatenAktuell, erfasser, stimmenhochrechnerAusgabedaten);
      } catch (Exception loggedAndRethrownException) {
        logAndDisplayException(loggedAndRethrownException, monitor);
        throw loggedAndRethrownException;
      }

      if (isFlushUndClearDurchfuehren(gebietCount)) {
        // Aufräumen, um Vollaufen des PersistenceContexts mit 900*n Stimmeingaben zu vermeiden...
        entityManager.flush();
        entityManager.clear();
        ergebnisDatenHolder.reset();
      }

      monitor.progress(ErgebniseingangGenerierenProgress.ERZEUGE_ERGEBNISEINGAENGE, gebietCount, gebiete.size());

      if (monitor.isCanceled()) {
        // Transaktion geordnet beenden, um bereits erzeugte Ergebniseingänge zu behalten
        LOG.info("Erzeuge fiktive Ergebniseingaenge unterbrochen");
        break;
      }
    }

    monitor.progress(ErgebniseingangGenerierenProgress.ABGESCHLOSSEN, 1, 1);
    LOG.info("Erzeuge fiktive Ergebniseingaenge - done");
  }

  private StimmenHochrechnerEingabedaten<Long> getStimmenHochrechnerEingabedaten(ErgebnisDaten ergebnisDatenAktuell) {
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten von ErgebnnisDaten");
    if (ergebnisDatenAktuell == null) {
      LOG.info("Erzeuge leere StimmenHochrechnerEingabedaten, weil ErgebnisDaten == null ist.");
      return StimmeingabeFiktivewerteCreate.getEmptyEingabeDaten();
    }
    StimmenHochrechnerEingabedaten<Long> eingabeDaten =  new StimmenHochrechnerEingabedatenVonErgebnisdaten(ergebnisDatenAktuell);
    LOG.info("Erzeuge StimmenHochrechnerEingabedaten von ErgebnnisDaten - done");
    return eingabeDaten;
  }

  private void brecheAb(ProgressMonitor monitor, HochrechneralgorithmusAusfuehrbarkeitStatus ausfuehrbarkeitStatus) {
    log(messages.lokalisiere(ausfuehrbarkeitStatus));
    monitor.progress(ErgebniseingangGenerierenProgress.ABGEBROCHEN_WEGEN_FEHLERN);
    monitor.progress(ausfuehrbarkeitStatus, 1, 1);
  }

  private void log(String msg) {
    LOG.info(msg);
  }

  private void logAndDisplayException(Exception exception, ProgressMonitor monitor) {
    LOG.warn("Fehler bei Erzeugung fiktiver Ergebniseingänge!", exception);

    monitor.progress(ErgebniseingangGenerierenProgress.ABGEBROCHEN_WEGEN_FEHLERN);
    monitor.addError("ergebniseingang.fiktive.ergebniseingaenge.erzeugen.fehlernachricht", exception);
  }

  private boolean isFlushUndClearDurchfuehren(int gebietCount) {
    return gebietCount % MASSENOPERATION_ANZAHL_GEBIETE_BIS_ENTITY_MANAGER_CLEAR == 0;
  }

  private void createFallsErfassungsgebietUndUeberschreibenErlaubt(Gebiet gebiet, Ergebniseingangsart ergebniseingangsart,
                                                                   ErgebnisDaten ergebnisDaten, ErgebniseingangErfasser erfasser, StimmenhochrechnerAusgabedaten stimmenhochrechnerAusgabedaten) {
    Wahldurchfuehrungsgebiet attachedGebiet = wahldurchfuehrungsgebietDao.findByPrimaryKey(gebiet.getPk());

    if (attachedGebiet.isErfassungsgebiet()) {
      Ergebniseingang letzterErgebniseingang = attachedGebiet.getLetzterErgebniseingangAktuell(ergebniseingangDao, ergebniseingangsart);

      if (isErgebniseingangUeberschreibenErlaubt(letzterErgebniseingang)) {
        stimmeingabeFiktivewerteCreate.createAndBehebePlausiFehler(attachedGebiet, ergebniseingangsart,
            ergebnisDaten, erfasser, letzterErgebniseingang, false,
            stimmenhochrechnerAusgabedaten);
      }
    }
  }

  private boolean isErgebniseingangUeberschreibenErlaubt(Ergebniseingang ergebniseingang) {
    return ergebniseingang == null || ergebniseingang.isUngueltigMussFehler();
  }

  private static void log(String message, Object... params) {
    LOG.debug(message, params);
  }
}
