
package de.ivu.elect.dokumente.stimmeingabe;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.StringUtils.defaultIfBlank;
import static org.apache.commons.lang3.StringUtils.leftPad;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.inject.Inject;

import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.dokumente.control.ElectDokumentContext;
import de.ivu.elect.business.dokumente.entity.DokumentVorlage;
import de.ivu.elect.business.dokumente.entity.NoDokumentExtraFileOutput;
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.control.GebietByNummerComparator;
import de.ivu.elect.business.gebietsbaum.entity.EntityMitGebiet;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeDocumentByErgebnisDatenExportContext;
import de.ivu.elect.business.stimmeingabe.control.StimmeingabeDocumentExportContext;
import de.ivu.elect.dokumente.CoreElectContext;
import de.ivu.elect.dokumente.CoreElectContextBuilder;
import de.ivu.elect.dokumente.konverter.AbstractDocumentMapper;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.util.FileUtil;
import de.ivu.elect.util.ListUtil;
import de.ivu.elect.util.TriFunction;

public abstract class AbstractCoreStimmeingabeDocumentExportMapper<C extends ElectDokumentContext & StimmeingabeDocumentExportContext, T> extends AbstractDocumentMapper<C> {

  /**
   * ELECTMUC-2784, bspw. bei 10 Seiten per Erfassungsgebiet-Niederschrift -> 30 Java-Dokumente * 10 Seiten = 300 Seiten in der exportierten ODT-Datei.
   */
  protected static final int MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_STIMMABGABEART = 30;

  /**
   * ELECT-6150, bspw. bei 10 Seiten per Erfassungsgebiet-Niederschrift -> 75 Java-Dokumente * 10 Seiten = 750 Seiten in der exportierten ODT-Datei.
   */
  protected static final int MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_ERFASSUNGSGEBIET_IN_SUBDIRECTORY_PER_GEBIET = 75;

  protected static final Comparator<Gebiet> GEBIET_COMPARATOR = GebietByNummerComparator.ASCENDING; // ELECT-9621

  @Inject
  protected transient GebietDAO gebietDAO;

  @Inject
  protected transient CoreElectContextBuilder coreElectContextBuilder;

  protected AbstractCoreStimmeingabeDocumentExportMapper() {
    super();
  }

  public boolean isDokumentVorlageBenoetigt() {
    return true;
  }

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

  protected abstract String getFileFormatName();

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

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

  public void serialize(T exportObject, OutputStream os, C exportContext, FileKonfiguration fileKonfiguration) throws MappingException {
    StimmeingabeDocumentContext context = new StimmeingabeDocumentContext(corify(exportContext), exportContext);

    Gebiet wahlgebiet = findWahlgebiet(exportContext, exportObject);
    boolean documentFuerWahlgebietIncluded = isDocumentFuerWahlgebietIncluded(context, wahlgebiet);

    Map<Gebiet, List<StimmeingabeDocument>> documentsFuerErfassungsgebietPerUebergeordnetemGebiet = createDocumentsFuerErfassungsgebietPerUebergeordnetemGebiet(context, exportContext, exportObject);
    List<StimmeingabeDocument> documentsFuerErfassungsgebiet = documentsFuerErfassungsgebietPerUebergeordnetemGebiet.values().stream().flatMap(Collection::stream).collect(toList());

    Collection<Gebiet> auswertungsgebiete = exportContext.getAuswertungsgebiete(gebietDAO);

    if (!auswertungsgebiete.isEmpty()) {
      info("Erzeuge Dokumente zu Erfassungsgebieten fuer Auswertungsgebiete");

      for (Gebiet auswertungsgebiet : auswertungsgebiete) {
        info("\tAuswertungsgebiet [nummer = " + auswertungsgebiet.getNummer() + ", name = " + auswertungsgebiet.getName() + "]");
      }
    }

    if (serializeOdtDocumentPerWurzelgebiet()) {
      Gebiet wurzelgebiet = wahlgebiet.getWurzelgebiet();
      List<StimmeingabeDocument> documents = new ArrayList<>();

      if (documentFuerWahlgebietIncluded) {
        documents.add(createStimmeingabeDocumentFuerWahlgebiet(context, wahlgebiet, documentsFuerErfassungsgebiet));
      }

      documents.addAll(documentsFuerErfassungsgebiet);

      info("Erzeuge Dokumente zu Erfassungsgebieten in EINZELNER ODT-Datei per Wurzelgebiet gepackt als ZIP-Datei [dokument wahlgebiet included = " + documentFuerWahlgebietIncluded
          + ", section stimmzettel included = " + !exportContext.isSectionErfassteStimmzettelExcluded() + ", section zaehlliste included = " + !exportContext.isSectionZaehllisteElektronischExcluded()
          + ", eingaben included = " + !exportContext.isEingabenExcluded() + "]");
      info("\tWurzelgebiet [nummer = " + wurzelgebiet.getNummer() + ", name = " + wurzelgebiet.getName() + ", anzahl dokumente (mit je x seiten) = " + documents.size() + "]");

      writeOdtDocument(os, exportContext, wurzelgebiet, documents);
    } else {
      Map<Gebiet, List<StimmeingabeDocument>> documentsPerUebergeordnetemGebiet = new HashMap<>();

      for (Gebiet gebiet : documentsFuerErfassungsgebietPerUebergeordnetemGebiet.keySet()) {
        documentsPerUebergeordnetemGebiet.putIfAbsent(gebiet, new ArrayList<>());
        documentsPerUebergeordnetemGebiet.get(gebiet).addAll(documentsFuerErfassungsgebietPerUebergeordnetemGebiet.get(gebiet));
      }

      if (documentFuerWahlgebietIncluded) {
        documentsPerUebergeordnetemGebiet.putIfAbsent(wahlgebiet, new ArrayList<>());
        documentsPerUebergeordnetemGebiet.get(wahlgebiet).add(0, createStimmeingabeDocumentFuerWahlgebiet(context, wahlgebiet, documentsFuerErfassungsgebiet));
      }

      info("Erzeuge Dokumente zu Erfassungsgebieten in MEHREREN ODT-Dateien per uebergeordnetem Gebiet gepackt als ZIP-Datei [dokument wahlgebiet included = " + documentFuerWahlgebietIncluded
          + ", section stimmzettel included = " + !exportContext.isSectionErfassteStimmzettelExcluded() + ", section zaehlliste included = " + !exportContext.isSectionZaehllisteElektronischExcluded()
          + ", eingaben included = " + !exportContext.isEingabenExcluded() + ", anzahl uebergeordnete gebiete = " + documentsPerUebergeordnetemGebiet.size() + "]");

      for (Entry<Gebiet, List<StimmeingabeDocument>> entry : documentsPerUebergeordnetemGebiet.entrySet()) {
        info("\tUebergeordnetes Gebiet [nummer = " + entry.getKey().getNummer() + ", name = " + entry.getKey().getName() + ", anzahl dokumente (mit je x seiten) = " + entry.getValue().size() + "]");
      }

      writeOdtDocumentPerGebiet(os, exportContext, documentsPerUebergeordnetemGebiet);
    }
  }

  protected boolean serializeOdtDocumentPerWurzelgebiet() {
    return false;
  }

  protected abstract Gebiet findWahlgebiet(C exportContext, T exportObject);

  protected Gebiet findWahlgebiet(Collection< ? extends EntityMitGebiet> entitiesMitGebiet) {
    return entitiesMitGebiet.stream() //
        .map(entity -> entity.getGebiet().getUebergeordnetesWahlgebiet()) //
        .filter(Objects::nonNull) //
        .findFirst().orElse(null);
  }

  protected abstract Map<Gebiet, List<StimmeingabeDocument>> createDocumentsFuerErfassungsgebietPerUebergeordnetemGebiet(StimmeingabeDocumentContext context, C exportContext, T exportObject);

  private boolean isDocumentFuerWahlgebietIncluded(StimmeingabeDocumentContext context, Gebiet wahlgebiet) {
    return wahlgebiet != null && context.isDocumentFuerWahlgebietRequired();
  }

  private StimmeingabeDocument createStimmeingabeDocumentFuerWahlgebiet(StimmeingabeDocumentContext context, Gebiet wahlgebiet, Collection<StimmeingabeDocument> documentsFuerErfassungsgebiet) {
    return getStimmeingabeDocumentFactory().createDocumentFuerWahlgebiet(context, wahlgebiet, documentsFuerErfassungsgebiet);
  }

  private void writeOdtDocument(OutputStream os, C exportContext, Gebiet gebiet, List<StimmeingabeDocument> documents) {
    Map<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet = new HashMap<>();
    documentsPerGebiet.put(gebiet, documents);

    writeOdtDocumentPerGebietToZipArchive(os, documentsPerGebiet, getDokumentVorlage(exportContext));
  }

  protected void writeOdtDocumentPerGebiet(OutputStream os, C exportContext, Map<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet) {
    DokumentVorlage dokumentVorlage = getDokumentVorlage(exportContext);

    boolean maxDocumentsPerSingleOdtDocumentCausingOdtDocumentPerErfassungsgebietInSubdirectoryPerGebietExceeded = documentsPerGebiet.values().stream()
        .anyMatch(documents -> documents.size() > MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_ERFASSUNGSGEBIET_IN_SUBDIRECTORY_PER_GEBIET);

    boolean maxDocumentsPerSingleOdtDocumentCausingOdtDocumentPerStimmabgabeartExceeded = documentsPerGebiet.values().stream()
        .anyMatch(documents -> documents.size() > MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_STIMMABGABEART);

    String maxDocumentsExceededMessagePrefix = "MAXIMALE ANZAHL DOKUMENTE (MIT JE X SEITEN) PER UEBERGEORDNETEM GEBIET UEBERSCHRITTEN";

    // ELECTMUC-2784, (erstmal?) keine Unterverzeichnisse mehr, sondern gruppieren nach Stimmenart und dann mehrere durchnummerierte ODT-Dokumente mit festgelegter Anzahl Erfassungsgebiete...
    //    if (maxDocumentsPerSingleOdtDocumentCausingOdtDocumentPerErfassungsgebietInSubdirectoryPerGebietExceeded) {
    //      warn(maxDocumentsExceededMessagePrefix
    //          + " - erzeuge Dokumente zu Erfassungsgebieten in MEHREREN ODT-Dateien per Erfassungsgebiet unterhalb eines Unterverzeichnisses per uebergeordnetem Gebiet gepackt als ZIP-Datei [limit dokumente (mit je x seiten) = "
    //          + MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_ERFASSUNGSGEBIET_IN_SUBDIRECTORY_PER_GEBIET + "]");
    //
    //      writeOdtDocumentToZipArchiveByMapping(os, documentsPerGebiet, dokumentVorlage, this::writeOdtDocumentToZipArchiveByMappingOdtDocumentPerErfassungsgebietInSubdirectoryPerGebiet);
    //    } else

    if (maxDocumentsPerSingleOdtDocumentCausingOdtDocumentPerStimmabgabeartExceeded) {
      warn(maxDocumentsExceededMessagePrefix
          + " - erzeuge Dokumente zu Erfassungsgebieten in MEHREREN durchnummerierten ODT-Dateien per uebergeordnetem Gebiet UND Stimmabgabeart gepackt als ZIP-Datei [limit dokumente (mit je x seiten) = "
          + MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_STIMMABGABEART + "]");

      writeOdtDocumentToZipArchiveByMapping(os, documentsPerGebiet, dokumentVorlage, this::writeOdtDocumentToZipArchiveByMappingOdtDocumentPerStimmabgabeart);
    } else {
      writeOdtDocumentPerGebietToZipArchive(os, documentsPerGebiet, dokumentVorlage);
    }
  }

  private void writeOdtDocumentToZipArchiveByMapping(OutputStream os, Map<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet, DokumentVorlage dokumentVorlage,
      TriFunction<ZipOutputStream, Entry<Gebiet, List<StimmeingabeDocument>>, Function<StimmeingabeDocument, File>, ZipOutputStream> mappingFct) {
    Function<StimmeingabeDocument, File> templateFileByDocumentFinder = document -> getTemplateFile(dokumentVorlage);

    try (ZipOutputStream zos = createZipOutputStream(os)) {

      for (Entry<Gebiet, List<StimmeingabeDocument>> entry : documentsPerGebiet.entrySet()) {
        mappingFct.apply(zos, entry, templateFileByDocumentFinder);
      }

      zos.finish();
    } catch (Exception ex) {
      throw new ElectCommonException("Fehler beim Erzeugen des Dokumentes!", ex);
    }
  }

  private ZipOutputStream writeOdtDocumentToZipArchiveByMappingOdtDocumentPerErfassungsgebietInSubdirectoryPerGebiet(ZipOutputStream zos, Entry<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet,
      Function<StimmeingabeDocument, File> templateFileByDocumentFinder) {
    Gebiet gebiet = documentsPerGebiet.getKey();
    String zipEntryNameDirectoryPrefix = FileUtil.normalizeZipEntryName(gebiet.getNummer() + "-" + gebiet.getName()) + "/";

    try {
      List<StimmeingabeDocument> documents = new ArrayList<>(documentsPerGebiet.getValue());

      for (int i = 0; i < documents.size(); i++) {
        StimmeingabeDocument document = documents.get(i);

        String zipEntrySimpleName = defaultIfBlank(FileUtil.normalizeZipEntryName(document.wahlbezirk.nummer), leftPad("" + i, 5, "0"));
        String zipEntryName = createOdtDateiName(zipEntryNameDirectoryPrefix + zipEntrySimpleName);

        zos.putNextEntry(new ZipEntry(zipEntryName));

        writeSeitenToOdtDocumentWithoutClosingStream(zos, zipEntryName, asList(document), templateFileByDocumentFinder);
      }
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }

    return zos;
  }

  private ZipOutputStream writeOdtDocumentToZipArchiveByMappingOdtDocumentPerStimmabgabeart(ZipOutputStream zos, Entry<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet,
      Function<StimmeingabeDocument, File> templateFileByDocumentFinder) {
    List<StimmeingabeDocument> documentsUrnenwahl = new ArrayList<>();
    List<StimmeingabeDocument> documentsBriefwahl = new ArrayList<>();

    for (StimmeingabeDocument document : documentsPerGebiet.getValue()) {
      if (document.wahlbezirk != null && document.wahlbezirk.briefwahlgebiet) {
        documentsBriefwahl.add(document);
      } else {
        documentsUrnenwahl.add(document);
      }
    }

    Gebiet gebiet = documentsPerGebiet.getKey();

    String zipEntryNamePrefix = gebiet.getNummer() + "-" + gebiet.getName() + "-";
    String zipEntryNamePrefixUrnenwahl = zipEntryNamePrefix + "urne";
    String zipEntryNamePrefixBriefwahl = zipEntryNamePrefix + "brief";

    int maxDocumentsPerSingleOdtDocument = MAX_DOCUMENTS_PER_SINGLE_ODT_DOCUMENT_CAUSING_ODT_DOCUMENT_PER_STIMMABGABEART;

    try {
      writeOdtDocumentToZipArchiveHavingMaxDocumentsPerSingleOdtDocument(zos, zipEntryNamePrefixUrnenwahl, documentsUrnenwahl, maxDocumentsPerSingleOdtDocument, templateFileByDocumentFinder);
      writeOdtDocumentToZipArchiveHavingMaxDocumentsPerSingleOdtDocument(zos, zipEntryNamePrefixBriefwahl, documentsBriefwahl, maxDocumentsPerSingleOdtDocument, templateFileByDocumentFinder);
    } catch (Exception ex) {
      throw new RuntimeException(ex);
    }

    return zos;
  }

  private void writeOdtDocumentToZipArchiveHavingMaxDocumentsPerSingleOdtDocument(ZipOutputStream zos, String zipEntryNamePrefix, List<StimmeingabeDocument> documents,
      int maxDocumentsPerSingleOdtDocument, Function<StimmeingabeDocument, File> templateFileByDocumentFinder) throws IOException {
    List<List<StimmeingabeDocument>> groupsOfDocuments = ListUtil.bildeBloeckeAlsListen(documents, maxDocumentsPerSingleOdtDocument);

    int groupsOfDocumentsCount = groupsOfDocuments.size();

    for (int i = 0; i < groupsOfDocumentsCount; i++) {
      String zipEntryNameIndex = groupsOfDocumentsCount > 1 ? "-" + leftPad("" + (i + 1), 2, "0") : "";

      String zipEntryName = createOdtDateiName(zipEntryNamePrefix + zipEntryNameIndex);
      zos.putNextEntry(new ZipEntry(zipEntryName));

      writeSeitenToOdtDocumentWithoutClosingStream(zos, zipEntryName, groupsOfDocuments.get(i), templateFileByDocumentFinder);
    }
  }

  private void writeOdtDocumentPerGebietToZipArchive(OutputStream os, Map<Gebiet, List<StimmeingabeDocument>> documentsPerGebiet, DokumentVorlage dokumentVorlage) {
    writeOdtDocumentsToZipArchive(os, documentsPerGebiet, dokumentVorlage);
  }

  protected abstract AbstractStimmeingabeDocumentFactory getStimmeingabeDocumentFactory();

  protected abstract DokumentVorlage getDokumentVorlage(C exportContext);

  protected CoreElectContext corify(C exportContext) {
    return coreElectContextBuilder.create(createBaseElectContext(exportContext, NoDokumentExtraFileOutput.INSTANCE));
  }

  protected DokumentVorlage getDokumentVorlage(StimmeingabeDocumentByErgebnisDatenExportContext exportContext) {
    return exportContext.getDokumentVorlage();
  }
}
