package com.quran.labs.androidquran.util;

import androidx.annotation.VisibleForTesting;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import timber.log.Timber;

public class ZipUtils {

  private static final int BUFFER_SIZE = 512;
  private static final int MAX_FILES = 12000; // Max number of files

  @VisibleForTesting
  static int MAX_UNZIPPED_SIZE = 0x1f400000; // Max size of unzipped data, 500MB

  /**
   * Unzip a file given the file, an item, and the listener
   * Does similar checks to those shown in rule 0's IDS04-J rule from:
   * https://www.securecoding.cert.org/confluence/display/java
   * @param zipFile the path to the zip file
   * @param destDirectory the directory to extract the file in
   * @param item any data object passed back to the listener
   * @param listener a progress listener
   * @param <T> the type of the item passed in
   * @return a boolean representing whether we succeeded to unzip the file or not
   */
  public static <T> boolean unzipFile(File zipFile,
      File destDirectory, T item, ZipListener<T> listener){
    try {
      Timber.d("unzipping %s, size: %d", zipFile, zipFile.length());

      try (ZipFile zip = new ZipFile(zipFile, ZipFile.OPEN_READ)) {
        int numberOfFiles = zip.size();
        Enumeration<? extends ZipEntry> entries = zip.entries();

        String canonicalPath = destDirectory.getCanonicalPath();

        long total = 0;
        int processedFiles = 0;
        while (entries.hasMoreElements()) {
          processedFiles++;
          ZipEntry entry = entries.nextElement();

          File currentEntryFile = new File(destDirectory, entry.getName());
          if (currentEntryFile.getCanonicalPath().startsWith(canonicalPath)) {
            if (entry.isDirectory()) {
              if (!currentEntryFile.exists()) {
                currentEntryFile.mkdirs();
              }
              continue;
            } else if (currentEntryFile.exists()) {
              // delete files that already exist
              currentEntryFile.delete();
            }

            try ( InputStream is = zip.getInputStream(entry);  FileOutputStream ostream = new FileOutputStream(currentEntryFile)) {
              int size;
              byte[] buf = new byte[BUFFER_SIZE];
              while (total + BUFFER_SIZE <= MAX_UNZIPPED_SIZE && (size = is.read(buf)) > 0) {
                ostream.write(buf, 0, size);
                total += size;
              }
            }

            if (processedFiles >= MAX_FILES || total >= MAX_UNZIPPED_SIZE) {
              throw new IllegalStateException("Invalid zip file.");
            }

            if (listener != null) {
              listener.onProcessingProgress(item, processedFiles, numberOfFiles);
            }
          } else {
            throw new IllegalStateException("Invalid zip file.");
          }
        }
      }

      return true;
    }
    catch (IOException ioe) {
      Timber.e(ioe, "Error unzipping file");
      return false;
    }
  }

  public interface ZipListener<T> {
    void onProcessingProgress(T obj, int processed, int total);
  }
}
