package de.duehl.basics.io.zip;

/*
 * Copyright 2024 Christian Dühl. All rights reserved.
 *
 * This program is free software. You can redistribute it and/or
 * modify it under the same terms as perl:
 *
 * general:  http://dev.perl.org/licenses/
 * GPL:      http://dev.perl.org/licenses/gpl1.html
 * artistic: http://dev.perl.org/licenses/artistic.html
 */

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import de.duehl.basics.datetime.DateAndTime;
import de.duehl.basics.datetime.DateAndTimeHelper;
import de.duehl.basics.io.FileHelper;
import de.duehl.basics.io.exceptions.ZippingException;

/**
 * Diese Klasse ist die Basisklasse für ZipFiles und ZipDirectories.
 *
 * @version 1.01     2024-04-26
 * @author Christian Dühl
 */

abstract class Zip {

    /**
     * Gibt an, ob der letzte Verzeichnisname als erstes Verzeichnis im Archiv auftauchen soll oder
     * nicht.
     */
    private final boolean withStartDirectory;

    /**
     * Gibt an, ob die Last-Modified-Time, die Last-Access-Time und die Creation-Time der gepackten
     * Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen.
     *
     * Anderenfalls wird die Last-Modified-Time (und nur diese) von der DAtei im Dateisystem
     * verwendet.
     */
    private boolean changeCreationLastAccessAndModificationTime;

    /**
     * Falls die Last-Modified-Time, die Last-Access-Time und die Creation-Time der gepackten
     * Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen, ist dies die zu setzende
     * Zeit.
     */
    private FileTime timeToSet;

    /**
     * Konstruktor.
     *
     * @param withStartDirectory
     *            Gibt an, ob der letzte Verzeichnisname als erstes Verzeichnis im Archiv
     *            auftauchen soll oder nicht.
     */
    public Zip(boolean withStartDirectory) {
        this.withStartDirectory = withStartDirectory;
        changeCreationLastAccessAndModificationTime = false;
    }

    /**
     * Legt fest, dass die Last-Modified-Time, die Last-Access-Time und die Creation-Time der
     * gepackten Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen.
     *
     * Anderenfalls wird die Last-Modified-Time (und nur diese) von der DAtei im Dateisystem
     * verwendet.
     *
     * @param time
     *            Die Zeit die gesetzt werden soll.
     */
    public void changeCreationLastAccessAndModificationTime(long time) {
        changeCreationLastAccessAndModificationTime(FileTime.from(time, TimeUnit.MILLISECONDS));
    }

    /**
     * Legt fest, dass die Last-Modified-Time, die Last-Access-Time und die Creation-Time der
     * gepackten Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen.
     *
     * Anderenfalls wird die Last-Modified-Time (und nur diese) von der DAtei im Dateisystem
     * verwendet.
     *
     * @param time
     *            Die Zeit die gesetzt werden soll.
     */
    public void changeCreationLastAccessAndModificationTime(FileTime time) {
        timeToSet = time;
        changeCreationLastAccessAndModificationTime = true;
    }

    /**
     * Legt fest, dass die Last-Modified-Time, die Last-Access-Time und die Creation-Time der
     * gepackten Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen.
     *
     * Anderenfalls wird die Last-Modified-Time (und nur diese) von der Datei im Dateisystem
     * verwendet.
     *
     * @param date
     *            Das Datum im Format "DD.MM.YYYY".
     * @param time
     *            Die Zeit im Format "hh:mm:ss".
     */
    public void changeCreationLastAccessAndModificationTime(String date, String time) {
        long millis = DateAndTimeHelper.toEpoch(date, time);
        changeCreationLastAccessAndModificationTime(millis);
    }

    /**
     * Legt fest, dass die Last-Modified-Time, die Last-Access-Time und die Creation-Time der
     * gepackten Dateien im Archiv auf einen bestimmten Wert gesetzt werden sollen.
     *
     * Anderenfalls wird die Last-Modified-Time (und nur diese) von der DAtei im Dateisystem
     * verwendet.
     *
     * @param dateAndTime
     *            Das Datum und die Uhrzeit.
     */
    public void changeCreationLastAccessAndModificationTime(DateAndTime dateAndTime) {
        long millis = dateAndTime.toEpoch();
        changeCreationLastAccessAndModificationTime(millis);
    }

    /**
     * Legt fest, dass die Last-Modified-Time, die Last-Access-Time und die Creation-Time der
     * gepackten Dateien im Archiv NICHT auf einen bestimmten Wert gesetzt werden sollen. Statt
     * dessen wird die Last-Modified-Time (und nur diese) von der DAtei im Dateisystem verwendet.
     */
    protected void switchCreationLastAccessAndModificationTimeChangingOff() {
        changeCreationLastAccessAndModificationTime = false;
    }

    /**
     * Gibt an, ob der letzte Verzeichnisname als erstes Verzeichnis im Archiv auftauchen soll oder
     * nicht.
     */
    protected boolean isWithStartDirectory() {
        return withStartDirectory;
    }

    /**
     * Erstellt das leere Archiv.
     *
     * @param zipFileName
     *            Name des Archivs (mit Pfad)
     * @return OutputStream für das Archiv.
     * @throws ZippingException
     *             Wenn das Archiv nicht angelegt werden kann.
     */
    protected ZipOutputStream createArchive(String zipFileName) {
        try {
            return new ZipOutputStream(new FileOutputStream(zipFileName));
        }
        catch (IOException ex) {
            throw new ZippingException("Archiv " + zipFileName + " kann nicht angelegt werden.");
        }
    }

    /**
     * Schließt das Archiv.
     *
     * @param zipOut
     *            Archiv-Datenstrom.
     * @throws ZippingException
     *             Wenn das Archiv nicht geschlossen werden kann.
     */
    protected void closeArchive(ZipOutputStream zipOut) {
        try {
            zipOut.close();
        }
        catch (IOException e) {
            throw new ZippingException("Kann Archiv nicht schließen!");
        }
    }

    /**
     * Bestimmt den Namen im Archiv für die gegebene, einzelne Datei und packt dies in das Archiv.
     *
     * @param directory
     *            Zu packendes Verzeichnis.
     * @param zipOut
     *            OutputStream für das Archiv.
     * @param file
     *            Zu packende Datei.
     * @throws ZippingException
     *             Wenn ein Fehler beim Packen auftritt.
     */
    protected void packFile(String directory, ZipOutputStream zipOut, File file) {
        packFile(directory, zipOut, file.getPath());
    }

    /**
     * Bestimmt den Namen im Archiv für die gegebene, einzelne Datei und packt dies in das Archiv.
     *
     * @param directory
     *            Zu packendes Verzeichnis.
     * @param zipOut
     *            OutputStream für das Archiv.
     * @param file
     *            Zu packende Datei im Dateisystem.
     * @throws ZippingException
     *             Wenn ein Fehler beim Packen auftritt.
     */
    protected void packFile(String directory, ZipOutputStream zipOut, String file) {
        String cleanedDirectory = FileHelper.removeTrailingSlash(directory);
        String fileNameInArchive = determineFilenameForFileToPack(cleanedDirectory, file);
        packFileInternal(zipOut, file, fileNameInArchive);
    }

    /**
     * Bestimmt den Dateinamen mit Pfad im Archiv für die zu packende Datei.
     *
     * @param directory
     *            Zu packendes Verzeichnis.
     * @param file
     *            Zu packende Datei.
     * @return Dateinamen mit Pfad im Archiv der zu packenden Datei.
     */
    private String determineFilenameForFileToPack(String directory, String file) {
        if (!file.startsWith(directory)) {
            throw new ZippingException("Datei fängt nicht mit dem Basisverzeichnis an!"
                    + "\n\t" + "Verzeichnis: " + directory
                    + "\n\t" + "Datei      : " + file);
        }

        if (isWithStartDirectory()) {
            int index = FileHelper.lastSlash(directory);
            return file.substring(index + 1);
        }
        else {
            return file.substring(directory.length());
        }
    }

    /**
     * Packt eine einzelne Datei in das Archiv.
     *
     * @param zipOut
     *            OutputStream für das Archiv.
     * @param fileNameInFileSystem
     *            Zu packende Datei.
     * @param fileNameInArchive
     *            Dateinamen mit Pfad im Archiv der zu packenden Datei.
     * @throws ZippingException
     *             Wenn ein Fehler beim Packen auftritt.
     */
    private void packFileInternal(ZipOutputStream zipOut, String fileNameInFileSystem,
            String fileNameInArchive) {
        try {
            tryToPackFileInternal(zipOut, fileNameInFileSystem, fileNameInArchive);
        }
        catch (IOException | IllegalArgumentException exception) {
            throw new ZippingException("Die Datei " + fileNameInFileSystem
                    + " kann nicht ins Archiv gelegt werden.", exception);
        }
    }

    /**
     * Packt eine einzelne Datei in das Archiv.
     *
     * @param zipOut
     *            OutputStream für das Archiv.
     * @param fileNameInFileSystem
     *            Zu packende Datei.
     * @param fileNameInArchive
     *            Dateinamen mit Pfad im Archiv der zu packenden Datei.
     * @throws IOException
     *             Wenn ein Fehler beim Packen auftrat.
     */
    private void tryToPackFileInternal(ZipOutputStream zipOut, String fileNameInFileSystem,
            String fileNameInArchive) throws IOException {
        ZipEntry zipEntry = new ZipEntry(fileNameInArchive);
        zipOut.putNextEntry(zipEntry);
        byte[] buffer = readFileToBuffer(fileNameInFileSystem);
        zipOut.write(buffer, 0, buffer.length);
        zipOut.closeEntry();

        if (changeCreationLastAccessAndModificationTime) {
            zipEntry.setLastModifiedTime(timeToSet);
            zipEntry.setLastAccessTime(timeToSet);
            zipEntry.setCreationTime(timeToSet);
        }
        else {
            zipEntry.setTime(new File(fileNameInFileSystem).lastModified());
        }
    }

    /**
     * Liest eine Datei in den Puffer ein.
     *
     * @param fileNameInFileSystem
     *            Name der Datei.
     * @return Byte-Array mit der Datei.
     * @throws FileNotFoundException
     *             Wenn die Datei nicht vorhanden ist (ist eine IOException).
     * @throws IOException
     *             Wenn ein Fehler beim Packen auftritt.
     */
    private byte[] readFileToBuffer(String fileNameInFileSystem) throws FileNotFoundException,
            IOException {
        FileInputStream fis = new FileInputStream(fileNameInFileSystem);
        BufferedInputStream bis = new BufferedInputStream(fis);
        int avail = bis.available();

        byte[] buffer = new byte[avail];
        if (avail > 0) {
            bis.read(buffer, 0, avail);
            bis.close();
        }
        return buffer;
    }

}
