
package de.ivu.elect.dokumente.ergebnis;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;

import java.io.File;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.inject.Inject;

import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisdokumentTyp;
import de.ivu.elect.business.ergebnis.boundary.ErgebnisdokumentTypPerWahlartResolver;
import de.ivu.elect.business.ergebnis.control.ErgebnisDaten;
import de.ivu.elect.business.ergebnis.control.ErgebnisdokumentTypExportContext;
import de.ivu.elect.business.ergebnis.control.ErgebnisdokumentTypExportObject;
import de.ivu.elect.business.ergebnis.entity.Ergebnisart;
import de.ivu.elect.business.exchange.control.FileFormat;
import de.ivu.elect.business.exchange.control.FileKonfiguration;
import de.ivu.elect.business.exchange.control.MappingException;
import de.ivu.elect.business.gebietsbaum.boundary.GebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahldurchfuehrungsgebietDAO;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietDescriptor;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformation;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolver;
import de.ivu.elect.business.gebietsbaum.boundary.WahlverwaltungGebietInformationResolverContext;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gebietsbaum.entity.Wahldurchfuehrungsgebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.stimmen.boundary.DirektstimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.GruppeStimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.ListenkandidaturstimmeLabelGenerator;
import de.ivu.elect.business.stimmen.boundary.ListenstimmeLabelGenerator;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahldurchfuehrung;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.core.exchange.gebietsbaum.WahlverwaltungGebietInformationCollector;
import de.ivu.elect.dokumente.ElectDocument;
import de.ivu.elect.dokumente.konverter.AbstractDocumentMapper;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.util.TriFunction;
import de.ivu.elect.was.exchange.ergebnis.ErgebnisdokumentTypDocumentExportMapper;

public abstract class AbstractErgebnisDocumentExportMapper<C extends ErgebnisdokumentTypExportContext, E, R, D extends ElectDocument> extends AbstractDocumentMapper<C>
    implements
      ErgebnisdokumentTypDocumentExportMapper<ErgebnisdokumentTypExportObject, C> {

  protected static final Comparator<Gebiet> GEBIET_DEFAULT_COMPARATOR = AbstractErgebnisTabellarischDocumentZeile.GEBIET_DEFAULT_COMPARATOR;

  protected static final Comparator<WahlverwaltungGebietInformation> GEBIET_INFORMATION_DEFAULT_COMPARATOR = AbstractErgebnisTabellarischDocumentZeile.GEBIET_INFORMATION_DEFAULT_COMPARATOR;

  @Inject
  private transient GebietDAO gebietDAO;

  @Inject
  private transient WahldurchfuehrungsgebietDAO wahldurchfuehrungsgebietDAO;

  @Inject
  private transient GruppeStimmeLabelGenerator gruppeStimmeLabelGenerator;

  @Inject
  private transient WahlverwaltungGebietInformationResolver wahlverwaltungGebietInformationResolver;

  @Inject
  private transient WahlverwaltungGebietInformationCollector wahlverwaltungGebietInformationCollector;

  @Inject
  private transient ErgebnisdokumentTypPerWahlartResolver ergebnisdokumentTypPerWahlartResolver;

  private final String erfassungsgebietGebietsreferenzFormat;

  protected AbstractErgebnisDocumentExportMapper() {
    this(ERFASSUNGSGEBIET_GEBIETSREFERENZ_DEFAULT_FORMAT);
  }

  protected AbstractErgebnisDocumentExportMapper(String erfassungsgebietGebietsreferenzFormat) {
    super();

    this.erfassungsgebietGebietsreferenzFormat = erfassungsgebietGebietsreferenzFormat;
  }

  @Override
  public boolean isAcceptContext(C context) {
    Wahldurchfuehrung wahldurchfuehrung = context.getWahl();

    return wahldurchfuehrung != null && isAcceptContext(wahldurchfuehrung)
        && ergebnisdokumentTypPerWahlartResolver.isErgebnisartSupported(wahldurchfuehrung, getErgebnisdokumentTyp(), context.getErgebnisart());
  }

  protected abstract boolean isAcceptContext(Wahldurchfuehrung wahldurchfuehrung);

  @Override
  public FileFormat getFileFormat() {
    return new FileFormat(getFileFormatName(), getFileType(), getFileTypeArchived());
  }

  protected abstract String getFileFormatName();

  protected FileType getFileType() {
    return FileType.ZIP;
  }

  private FileType getFileTypeArchived() {
    return FileType.ODT;
  }

  @Override
  protected <K> String getDateiName(Wahl wahl, Wahltermin wahltermin, K entity) {
    return ErgebnisdokumentTyp.createFileName(wahl, wahltermin, getMessages(), getErgebnisdokumentTyp(), entity, getFileTypeArchived());
  }

  @Override
  public void serialize(ErgebnisdokumentTypExportObject exportObject, OutputStream os, C context, FileKonfiguration fileKonfiguration) throws MappingException {
    ErgebnisDaten ergebnisDaten = exportObject.getErgebnisDaten();

    Wahldurchfuehrung wahldurchfuehrung = ergebnisDaten.getWahl();
    List<E> auswertungseinheiten = collectAuswertungseinheiten(context, ergebnisDaten);

    writeOdtDocumentsToZipArchive(os, wahldurchfuehrung, wahldurchfuehrung.getWahltermin(), createDocuments(context, ergebnisDaten, auswertungseinheiten),
        getTemplateFileByDocumentFinder(context, ergebnisDaten));
  }

  protected String getErfassungsgebietGebietsreferenzFormat() {
    return erfassungsgebietGebietsreferenzFormat;
  }

  protected String addErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(C context, String value) {
    return trimToEmpty(trimToEmpty(value) + " " + getErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(context));
  }

  private String getErfassungsgebietGebietsreferenzSuffixSonderstimmbezirk(C context) {
    return context.getMessage("ergebnis.gesamtergebnis.tabellarisch.exportieren.dokument.zeile.suffix.SONDERSTIMMBEZIRK");
  }

  /**
   * @return Die Auswertungseinheiten, für deren (untergeordnete) Erfassungsgebiete Ergebnisinformationen ins Dokument geschrieben werden sollen. Enthält standardmäßig das Wurzelgebiet (also alle
   *         Erfassungsgebiete), bei MUC aber auch Untergebiete (Wahlkreis/Stimmkreis/Stadtbezirk) oder eine GruppeImGebiet (, deren Verwendung besser auch auf Gebiete umgestellt werden sollte).
   */
  protected abstract List<E> collectAuswertungseinheiten(C context, ErgebnisDaten ergebnisDaten);

  /*
   * LinkedHashMap, um eine immer gleiche Reihenfolge der Dokumente zu garantieren. Das soll sicherstellen, dass bspw. bei gleichnamigen ZIP-Entries immer derselbe ZIP-Entry im ZIP landet und nicht
   * zufälligerweise der eine oder andere...
   */
  protected abstract LinkedHashMap<R, List<D>> createDocuments(C context, ErgebnisDaten ergebnisDaten, List<E> auswertungseinheiten);

  protected abstract Function<D, File> getTemplateFileByDocumentFinder(C context, ErgebnisDaten ergebnisDaten);

  protected List<Wahldurchfuehrungsgebiet> collectAuswertungsgebieteOderWurzelgebiet(C context, ErgebnisDaten ergebnisDaten) {
    List<Wahldurchfuehrungsgebiet> result = new ArrayList<>();

    result.addAll(collectAuswertungsgebiete(context));

    if (result.isEmpty()) {
      result.add(getWurzelgebiet(context));
    }

    result.sort(GEBIET_DEFAULT_COMPARATOR);

    return result;
  }

  protected List<Wahldurchfuehrungsgebiet> collectUntergeordneteGebiete(Gebiet gebiet) {
    List<Wahldurchfuehrungsgebiet> result = new ArrayList<>();

    for (Gebiet untergeordnesGebiet : gebiet.getUntergeordneteGebiete()) {
      result.add(attach(untergeordnesGebiet));
    }

    result.sort(GEBIET_DEFAULT_COMPARATOR);

    return result;
  }

  protected <T> SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet(
      ErgebnisDaten ergebnisDaten, List<Wahldurchfuehrungsgebiet> auswertungsgebiete, boolean erfassungsgebietErfasstInAnderemGebietIgnored,
      TriFunction<WahlverwaltungGebietInformation, WahlverwaltungGebietInformation, WahlverwaltungGebietDescriptor, T> erfassungsgebietMapper) {
    Ergebnisart ergebnisart = ergebnisDaten.getErgebnis().getErgebnisart();

    boolean gemeindeVerwaltungseinheitseinteilungIgnored = false;

    for (Wahldurchfuehrungsgebiet auswertungsgebiet : auswertungsgebiete) {
      info("Erzeuge Dokumente für Auswertungsgebiet [ergebnisart = " + ergebnisart + ", art = " + auswertungsgebiet.getGebietsart().getName() + ", nummer = " + auswertungsgebiet.getNummer()
          + ", name = " + auswertungsgebiet.getName() + ", gebiet in gebietsbaum = " + true + ", wurzelgebiet = " + auswertungsgebiet.isWurzelgebiet() + ", orts-/stadtbezirkseinteilung ignored = "
          + gemeindeVerwaltungseinheitseinteilungIgnored + ", erfassungsgebiete erfasst in anderem gebiet ignored = " + erfassungsgebietErfasstInAnderemGebietIgnored + "]");
    }

    Predicate<Gebiet> erfassungsgebietFilter = erfassungsgebiet -> auswertungsgebiete.stream().anyMatch(erfassungsgebiet::isUntergebietVon);

    if (auswertungsgebiete.size() == 1 && auswertungsgebiete.get(0).isWurzelgebiet()) {
      erfassungsgebietFilter = erfassungsgebiet -> true;
    }

    SortedMap<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> result = wahlverwaltungGebietInformationCollector.collectErfassungsgebieteGroupedByDefault(
        ergebnisDaten, gemeindeVerwaltungseinheitseinteilungIgnored, erfassungsgebietErfasstInAnderemGebietIgnored, erfassungsgebietFilter, erfassungsgebietMapper);

    for (Entry<WahlverwaltungGebietInformation, SortedMap<WahlverwaltungGebietInformation, List<T>>> entryPerStimmzettelgebietInformation : result.entrySet()) {
      WahlverwaltungGebietInformation stimmzettelgebietInformation = entryPerStimmzettelgebietInformation.getKey();

      if (stimmzettelgebietInformation.isUnbekannt()) {
        long erfassungsgebieteCount = entryPerStimmzettelgebietInformation.getValue().entrySet().stream().flatMap(entryPerGemeinde -> entryPerGemeinde.getValue().stream()).count();

        throw new ElectCommonException("ergebnis.export.message.stimmzettelgebiet.not.found", stimmzettelgebietInformation.getArt(), erfassungsgebieteCount);
      }
    }

    return result;
  }

  /**
   * @deprecated
   * @see AbstractErgebnisDocumentExportMapper#collectErfassungsgebieteInAuswertungsgebietenMappedPerGemeindePerStimmzettelgebiet
   */
  @Deprecated
  protected List<WahlverwaltungGebietInformation> collectStimmzettelgebietInformationen(C context, ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet auswertungsgebiet,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> stimmzettelgebietInformationFct) {
    Map<WahlverwaltungGebietInformation, Collection<Wahldurchfuehrungsgebiet>> erfassungsgebietePerStimmzettelgebietInformationen = new HashMap<>();

    for (Wahldurchfuehrungsgebiet erfassungsgebiet : ergebnisDaten.getErfassungsgebieteUnterhalbVon(auswertungsgebiet)) {
      WahlverwaltungGebietInformation stimmzettelgebietInformation = stimmzettelgebietInformationFct.apply(createWahlverwaltungGebietDescriptor(ergebnisDaten, erfassungsgebiet));

      erfassungsgebietePerStimmzettelgebietInformationen.computeIfAbsent(stimmzettelgebietInformation, ignored -> new ArrayList<>()).add(erfassungsgebiet);
    }

    for (Entry<WahlverwaltungGebietInformation, Collection<Wahldurchfuehrungsgebiet>> entryPerStimmzettelgebietInformation : erfassungsgebietePerStimmzettelgebietInformationen.entrySet()) {
      WahlverwaltungGebietInformation stimmzettelgebietInformation = entryPerStimmzettelgebietInformation.getKey();

      if (stimmzettelgebietInformation.isUnbekannt()) { // ELECT-16180
        long erfassungsgebieteCount = entryPerStimmzettelgebietInformation.getValue().size();

        throw new ElectCommonException("ergebnis.export.message.stimmzettelgebiet.not.found", stimmzettelgebietInformation.getArt(), erfassungsgebieteCount);
      }
    }

    return erfassungsgebietePerStimmzettelgebietInformationen.keySet().stream() //
        .sorted(GEBIET_INFORMATION_DEFAULT_COMPARATOR) //
        .collect(toList());
  }

  protected WahlverwaltungGebietDescriptor createWahlverwaltungGebietDescriptor(ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet erfassungsgebiet) {
    return wahlverwaltungGebietInformationResolver.createDescriptor(new WahlverwaltungGebietInformationResolverContext(ergebnisDaten), erfassungsgebiet);
  }

  protected List<PolitischeGruppe> collectPolitischeGruppenInStimmzettelgebietRelevantSorted(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation stimmzettelgebietInformation) {
    return collectPolitischeGruppenInStimmzettelgebietRelevantSorted(ergebnisDaten, stimmzettelgebietInformation, WahlverwaltungGebietDescriptor::getStimmzettelgebiet);
  }

  protected List<PolitischeGruppe> collectPolitischeGruppenInStimmzettelgebietRelevantSorted(ErgebnisDaten ergebnisDaten, WahlverwaltungGebietInformation stimmzettelgebietInformation,
      Function<WahlverwaltungGebietDescriptor, WahlverwaltungGebietInformation> stimmzettelgebietInformationFct) {
    for (Wahldurchfuehrungsgebiet erfassungsgebiet : ergebnisDaten.getErfassungsgebiete()) {
      WahlverwaltungGebietInformation wahlverwaltungGebietInformation = stimmzettelgebietInformationFct.apply(createWahlverwaltungGebietDescriptor(ergebnisDaten, erfassungsgebiet));

      if (stimmzettelgebietInformation.equals(wahlverwaltungGebietInformation)) {
        return collectPolitischeGruppenRelevantSorted(ergebnisDaten, erfassungsgebiet);
      }
    }

    return emptyList();
  }

  // Cf. AbstractErgebnisTabellarischDocumentZeileCreator#collectPolitischeGruppenRelevantSorted!
  protected List<PolitischeGruppe> collectPolitischeGruppenRelevantSorted(ErgebnisDaten ergebnisDaten, Wahldurchfuehrungsgebiet wahldurchfuehrungsgebiet) {
    return ergebnisDaten.getPolitischeGruppenZugelassenSortiertNachPositionAufGebiet(wahldurchfuehrungsgebiet);
  }

  protected Collection<Wahldurchfuehrungsgebiet> collectAuswertungsgebiete(C context) {
    return context.getAuswertungsgebiete(gebietDAO);
  }

  protected Wahldurchfuehrungsgebiet getWurzelgebiet(C context) {
    return context.getWurzelgebiet(gebietDAO);
  }

  protected Wahldurchfuehrungsgebiet attach(Gebiet gebiet) {
    return wahldurchfuehrungsgebietDAO.findByPrimaryKey(gebiet.getPk());
  }

  protected GruppeStimmeLabelGenerator getGruppeStimmeLabelGenerator() {
    return gruppeStimmeLabelGenerator;
  }

  protected DirektstimmeLabelGenerator getDirektstimmeLabelGenerator() {
    return getGruppeStimmeLabelGenerator().getDirektstimmeLabelGenerator();
  }

  protected ListenstimmeLabelGenerator getListenstimmeLabelGenerator() {
    return getGruppeStimmeLabelGenerator().getListenstimmeLabelGenerator();
  }

  protected ListenkandidaturstimmeLabelGenerator getListenkandidaturstimmeLabelGenerator() {
    return getListenstimmeLabelGenerator().getListenkandidaturstimmeLabelGenerator();
  }

  protected <K, V> SortedMap<K, V> asSingletonSortedMap(Entry<K, V> entry) {
    SortedMap<K, V> result = new TreeMap<>((one, another) -> 0); // Dummy-Comparator angeben, da der Key nicht zwangsläufig Comparable implementiert...
    result.put(entry.getKey(), entry.getValue());

    return result;
  }
}
