
package de.ivu.elect.dokumente.konverter;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.inject.Inject;

import de.ivu.document.generator.template.BaseDocument;
import de.ivu.document.generator.template.OdtDocumentBuilder;
import de.ivu.elect.business.datei.entity.FileType;
import de.ivu.elect.business.dokumente.boundary.DokumentVorlageDAO;
import de.ivu.elect.business.dokumente.control.ElectDokumentContext;
import de.ivu.elect.business.dokumente.entity.DokumentVorlage;
import de.ivu.elect.business.dokumente.entity.DokumentVorlageArt;
import de.ivu.elect.business.exchange.control.ImportExportStreamCreator;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.WahlOderWahltermin;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.exception.ElectCommonException;
import de.ivu.elect.jee.faces.Messages;
import de.ivu.elect.jee.faces.UserLocale;
import de.ivu.elect.util.FileUtil;

public abstract class AbstractDocumentMapper<C extends ElectDokumentContext> extends AbstractDokumentKonverter<C> {

  @Inject
  protected transient Messages messages;

  @Inject
  protected transient UserLocale userLocale;

  @Inject
  protected transient DokumentVorlageDAO dokumentVorlageDAO;

  @Inject
  protected transient ImportExportStreamCreator importExportStreamCreator;

  public AbstractDocumentMapper() {
    super();
  }

  protected <K, D extends BaseDocument> void writeOdtDocumentsToZipArchive(OutputStream os, Map<K, ? extends Collection<D>> documentsPerZipEntryKey, DokumentVorlage dokumentVorlage) {
    // Identische Dokumentvorlage für alle Dokumente.
    writeOdtDocumentsToZipArchive(os, dokumentVorlage.getWahl(), dokumentVorlage.getWahltermin(), documentsPerZipEntryKey, document -> getTemplateFile(dokumentVorlage));
  }

  protected <K, D extends BaseDocument> void writeOdtDocumentsToZipArchive(OutputStream os, Wahl wahl, Wahltermin wahltermin, Map<K, ? extends Collection<D>> documentsPerZipEntryKey,
      Function<D, File> templateFileByDocumentFinder) {
    writeOdtDocumentsToZipArchive(os, wahl, wahltermin, documentsPerZipEntryKey.keySet(), documentsPerZipEntryKey::get, templateFileByDocumentFinder);
  }

  protected <K, D extends BaseDocument> void writeOdtDocumentsToZipArchive(OutputStream os, Wahl wahl, Wahltermin wahltermin, Collection<K> zipEntryKeys,
      Function<K, ? extends Collection<D>> documentsByZipEntryKeyFinder, Function<D, File> templateFileByDocumentFinder) {
    try (ZipOutputStream zos = createZipOutputStream(os)) {

      for (K zipEntryKey : zipEntryKeys) {
        String zipEntryName = getDateiName(wahl, wahltermin, zipEntryKey);
        zos.putNextEntry(new ZipEntry(zipEntryName));

        Collection<D> documents = documentsByZipEntryKeyFinder.apply(zipEntryKey);

        writeSeitenToOdtDocumentWithoutClosingStream(zos, zipEntryName, documents, templateFileByDocumentFinder);
      }

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

  protected ZipOutputStream createZipOutputStream(OutputStream os) {
    // HINT (Cp437): Damit Umlaute wie "München" korrekt dargestellt werden,
    // cf. http://stackoverflow.com/questions/11276343/zipinputstreaminputstream-charset-decodes-zipentry-file-name-falsely
    return new ZipOutputStream(os, Charset.forName("Cp437"));
  }

  protected <D extends BaseDocument> void writeSeitenToOdtDocumentWithoutClosingStream(OutputStream os, String fileName, Collection<D> documents, Function<D, File> templateFileByDocumentFinder) {
    OdtDocumentBuilder odtDocumentBuilder = OdtDocumentBuilder.create(os);

    int documentsCount = documents.size();
    LocalDateTime startDateTime = LocalDateTime.now();

    info("Schreibe Dokumente in Datei - START [fileName = " + fileName + ", documentsCount = " + documentsCount + "]");

    for (D document : documents) {
      odtDocumentBuilder.insertAndFillTemplate(templateFileByDocumentFinder.apply(document), document);
    }

    long durationInSeconds = Math.max(Duration.between(startDateTime, LocalDateTime.now()).getSeconds(), 1);
    double documentsWrittenPerSecond = documentsCount / (double) durationInSeconds;

    info("Schreibe Dokumente in Datei -  DONE [fileName = " + fileName + ", documentsCount = " + documentsCount + ", duration = " + durationInSeconds + "s, documents/s = " + documentsWrittenPerSecond
        + "]");

    odtDocumentBuilder.saveWithoutClosingStream();
  }

  protected <K> String getDateiName(Wahl wahl, Wahltermin wahltermin, K dateiSchluessel) {
    if (dateiSchluessel instanceof Gebiet) {
      Gebiet gebiet = (Gebiet) dateiSchluessel;

      return createOdtDateiName(gebiet.getNummer() + "-" + gebiet.getName());
    }

    throw new IllegalArgumentException("Für Schlüssel [dateiSchluessel=" + dateiSchluessel + "] kann kein Dateiname erzeugt werden.");
  }

  protected String createOdtDateiName(String dateiName) {
    return FileUtil.normalizeZipEntryName(dateiName) + "." + FileType.ODT.getFileExtension();
  }

  protected String formatDate(Date date, Locale locale) {
    return UserLocale.formatDateMedium(date, locale);
  }

  protected File getTemplateFile(DokumentVorlage dokumentVorlage) {
    return attachOrThrowException(dokumentVorlage).getDatei().createFile(importExportStreamCreator);
  }

  protected DokumentVorlage attachOrThrowException(DokumentVorlage dokumentVorlage) {
    if (dokumentVorlage == null) {
      throw new ElectCommonException("dokument.vorlage.nicht.ausgewählt");
    }

    return dokumentVorlageDAO.attach(dokumentVorlage);
  }

  protected List<DokumentVorlage> find(Wahl wahl, DokumentVorlageArt dokumentVorlageArt) {
    return dokumentVorlageDAO.findByWahlAndArtOrderByName(wahl, dokumentVorlageArt);
  }

  protected boolean isDokumentVorlageForSpreadsheet(DokumentVorlage dokumentVorlage, FileType fileType) {
    if (dokumentVorlage != null) {
      return dokumentVorlage.hasFileType(fileType);
    }

    return false;
  }

  protected Map<String, String> mapSpreadsheetParameters(WahlOderWahltermin context, Locale locale) {
    Map<String, String> result = new HashMap<>();

    result.put("datum", formatDate(new Date(), locale));
    result.put("wahlName", context.getName());
    result.put("wahlDatum", formatDate(context.getWahldatum(), locale));
    result.put("wahlJahr", String.valueOf(context.getWahljahr()));

    if (context.isInstanceOfWahl()) {
      result.put("wahlWahlart", context.castTo(Wahl.class).getEntity().getWahlart().getName());
    }

    return result;
  }
}
