package de.ivu.elect.background.scheduler;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;

import javax.enterprise.event.Observes;
import javax.enterprise.event.Reception;
import javax.enterprise.event.TransactionPhase;
import javax.inject.Inject;

import org.slf4j.Logger;

import de.ivu.elect.business.konfiguration.entity.KonfigurationLoader;
import de.ivu.elect.business.konfiguration.entity.KonfigurationWertGeaendertEvent;
import de.ivu.elect.business.system.control.AbstractElectBackgroundService;
import de.ivu.elect.business.system.entity.SystemLogQuelle;


import static java.util.stream.Collectors.toList;

// Annotieren
//@javax.ejb.Singleton;
//@DependsOn("DbInit")
//// https://issues.jboss.org/browse/EJBTHREE-2070 (keine Transaktion)
//@TransactionManagement(TransactionManagementType.BEAN)
public abstract class AbstractSchedulerBackgroundService<C extends Serializable> extends AbstractElectBackgroundService {

  private final List<SchedulerSlot<C>> slots = new CopyOnWriteArrayList<>();

  @Inject
  protected transient KonfigurationLoader konfigurationLoader;

  @Override
  protected void postInit() {
    super.postInit();
    initSlots();
  }

  protected void initSlots() {
    var anzahlSlots = getAnzahlSlots();
    getLog().info("Initialisiere Scheduler - Anzahl Slots: " + anzahlSlots);
    slots.clear();
    for (var i = 0; i < anzahlSlots; i++) {
      slots.add(craeteEmptySlot());
    }
    backgroundServiceCounter.anzahlSlots.set(anzahlSlots);
  }

  @Override
  protected boolean isSomethingToDoInternal() {
    return !bestimmeNaechstePakete().isEmpty();
  }

  protected List<C> bestimmeNaechstePakete() {
    var freieSlots = bestimmeFreieSlots();
    if (freieSlots.isEmpty()) {
      return Collections.emptyList();
    }
    return new ArrayList<>(getServiceTransaction().bestimmeNaechstePakete(bestimmeAktuellBearbeitetePakete(), freieSlots.size())); // eher zufällige Reihenfolge
  }

  @Override
  protected void doSomethingInternal() {
    var naechstePakete = bestimmeNaechstePakete();

    if (naechstePakete.isEmpty()) {
      getLog().info("Keine weiteren Pakete vorhanden");
    } else {
      getLog().info("Anzahl nächster Pakete: " + naechstePakete.size());
      var freieSlots = bestimmeFreieSlots();
      if (freieSlots.isEmpty()) {
        return;
      }
      if (naechstePakete.size() > freieSlots.size()) {
        naechstePakete = naechstePakete.subList(0, freieSlots.size());
      }
      List<SchedulerSlot<C>> zuStarten = new ArrayList<>();
      for (var i = 0; i < naechstePakete.size(); i++) {
        var paket = naechstePakete.get(i);
        var slot = freieSlots.get(i);
        slot.setPaket(paket);
        zuStarten.add(slot);
      }

      if (!zuStarten.isEmpty()) {
        var wartezeit = getWartezeitInSekunden();
        for (var slot : zuStarten) {
          getServiceAsync().bearbeitePaket(slot, backgroundServiceCounter, wartezeit);
        }
      }
    }
  }

  @Override
  protected boolean isCountInvocatiom() {
    return false; // wird in den Slots gezählt
  }

  protected SchedulerSlot<C> craeteEmptySlot() {
    return new SchedulerSlot<>();
  }

  private long getAnzahlSlots() {
    var l = konfigurationLoader.resolveLongValue(getSchedulerTyp().getAnzahlSlotsParameter());
    if (l == 0) {
      return 1; // immer einen Slot anbieten
    }
    return l;
  }

  private long getWartezeitInSekunden() {
    try {
      var l = konfigurationLoader.resolveLongValue(getSchedulerTyp().getWartezeitInSekundenParameter());
      if (l > 0) {
        return l;
      }
    } catch (Exception e) {
      getLog().warn("Fehler beim Bestimmen der Wartezeit: " + e.getMessage(), e);
    }
    return 0L; // im Zweifel nicht warten
  }

  private Set<C> bestimmeAktuellBearbeitetePakete() {
    return slots.stream().filter(s -> !s.isFrei()).map(SchedulerSlot::getPaket).filter(Objects::nonNull).collect(Collectors.toSet());
  }

  protected abstract AbstractSchedulerServiceAsync<C> getServiceAsync();

  protected abstract AbstractSchedulerServiceTransaction<C> getServiceTransaction();

  protected List<SchedulerSlot<C>> bestimmeFreieSlots() {
    return slots.stream().filter(SchedulerSlot::isFrei).collect(toList());
  }

  @Override
  public SystemLogQuelle getSystemLogQuelle() {
    return getSchedulerTyp().getSystemLogQuelle();
  }

  protected SchedulerTyp getSchedulerTyp() {
    return getServiceAsync().getSchedulerTyp();
  }

  @Override
  protected Logger getLog() {
    return getLogInternal();
  }

  protected abstract Logger getLogInternal();

  public void listen(@Observes(during = TransactionPhase.AFTER_COMPLETION, notifyObserver = Reception.ALWAYS) KonfigurationWertGeaendertEvent konfigurationWertGeaendertEvent) {
    try {
      if (getSchedulerTyp().getAnzahlSlotsParameter().equals(konfigurationWertGeaendertEvent.getKey())) {
        initSlots();
      }
    } catch (Exception e) {
      getLog().warn("Fehler beim Anpassen der Slots: " + e.getMessage(), e);
    }
  }

}