/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS
 */
package org.opends.quicksetup.util;

import static com.forgerock.opendj.cli.Utils.*;
import static com.forgerock.opendj.util.OperatingSystem.*;

import static org.opends.messages.QuickSetupMessages.*;
import static org.opends.server.util.CollectionUtils.*;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.quicksetup.Application;
import org.opends.quicksetup.ApplicationException;
import org.opends.quicksetup.ReturnCode;

/**
 * Class for extracting the contents of a zip file and managing
 * the reporting of progress during extraction.
 */
public class ZipExtractor {

  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /** Path separator for zip file entry names on Windows and *nix. */
  private static final char ZIP_ENTRY_NAME_SEP = '/';

  private InputStream is;
  private int minRatio;
  private int maxRatio;
  private int numberZipEntries;
  private String zipFileName;
  private Application application;

  /**
   * Creates an instance of an ZipExtractor.
   * @param zipFile File the zip file to extract
   * @throws FileNotFoundException if the specified file does not exist
   * @throws IllegalArgumentException if the zip file is not a zip file
   */
  public ZipExtractor(File zipFile)
    throws FileNotFoundException, IllegalArgumentException
  {
    this(zipFile, 0, 0, 1, null);
  }

  /**
   * Creates an instance of an ZipExtractor.
   * @param in InputStream for zip content
   * @param zipFileName name of the input zip file
   * @throws FileNotFoundException if the specified file does not exist
   * @throws IllegalArgumentException if the zip file is not a zip file
   */
  public ZipExtractor(InputStream in, String zipFileName)
    throws FileNotFoundException, IllegalArgumentException
  {
    this(in, 0, 0, 1, zipFileName, null);
  }

  /**
   * Creates an instance of an ZipExtractor.
   * @param zipFile File the zip file to extract
   * @param minRatio int indicating the max ration
   * @param maxRatio int indicating the min ration
   * @param numberZipEntries number of entries in the input stream
   * @param app application to be notified about progress
   * @throws FileNotFoundException if the specified file does not exist
   * @throws IllegalArgumentException if the zip file is not a zip file
   */
  public ZipExtractor(File zipFile, int minRatio, int maxRatio,
                                      int numberZipEntries,
                                      Application app)
    throws FileNotFoundException, IllegalArgumentException
  {
    this(new FileInputStream(zipFile),
      minRatio,
      maxRatio,
      numberZipEntries,
      zipFile.getName(),
      app);
    if (!zipFile.getName().endsWith(".zip")) {
      throw new IllegalArgumentException("File must have extension .zip");
    }
  }

  /**
   * Creates an instance of an ZipExtractor.
   * @param is InputStream of zip file content
   * @param minRatio int indicating the max ration
   * @param maxRatio int indicating the min ration
   * @param numberZipEntries number of entries in the input stream
   * @param zipFileName name of the input zip file
   * @param app application to be notified about progress
   */
  public ZipExtractor(InputStream is, int minRatio, int maxRatio,
                                      int numberZipEntries,
                                      String zipFileName,
                                      Application app) {
    this.is = is;
    this.minRatio = minRatio;
    this.maxRatio = maxRatio;
    this.numberZipEntries = numberZipEntries;
    this.zipFileName = zipFileName;
    this.application = app;
  }

  /**
   * Performs the zip extraction.
   * @param destination File where the zip file will be extracted
   * @throws ApplicationException if something goes wrong
   */
  public void extract(File destination) throws ApplicationException {
    extract(Utils.getPath(destination));
  }

  /**
   * Performs the zip extraction.
   * @param destination File where the zip file will be extracted
   * @throws ApplicationException if something goes wrong
   */
  public void extract(String destination) throws ApplicationException {
    extract(destination, true);
  }

  /**
   * Performs the zip extraction.
   * @param destDir String representing the directory where the zip file will
   * be extracted
   * @param removeFirstPath when true removes each zip entry's initial path
   * when copied to the destination folder.  So for instance if the zip entry's
   * name was /OpenDJ-2.4.x/some_file the file would appear in the destination
   * directory as 'some_file'.
   * @throws ApplicationException if something goes wrong
   */
  public void extract(String destDir, boolean removeFirstPath)
          throws ApplicationException
  {
    ZipInputStream zipIn = new ZipInputStream(is);
    int nEntries = 1;

    /* This map is updated in the copyZipEntry method with the permissions
     * of the files that have been copied.  Once all the files have
     * been copied to the file system we will update the file permissions of
     * these files.  This is done this way to group the number of calls to
     * Runtime.exec (which is required to update the file system permissions).
     */
    Map<String, ArrayList<String>> permissions = new HashMap<>();
    permissions.put(getProtectedDirectoryPermissionUnix(), newArrayList(destDir));
    try {
      if(application != null) {
        application.checkAbort();
      }
      ZipEntry entry = zipIn.getNextEntry();
      while (entry != null) {
        if(application != null) {
          application.checkAbort();
        }
        int ratioBeforeCompleted = minRatio
                + ((nEntries - 1) * (maxRatio - minRatio) / numberZipEntries);
        int ratioWhenCompleted =
                minRatio + (nEntries * (maxRatio - minRatio) / numberZipEntries);

        String name = entry.getName();
        if (name != null && removeFirstPath) {
          int sepPos = name.indexOf(ZIP_ENTRY_NAME_SEP);
          if (sepPos != -1) {
            name = name.substring(sepPos + 1);
          } else {
            logger.warn(LocalizableMessage.raw(
                    "zip entry name does not contain a path separator"));
          }
        }
        if (name != null && name.length() > 0) {
          try {
            File destination = new File(destDir, name);
            copyZipEntry(entry, destination, zipIn,
                    ratioBeforeCompleted, ratioWhenCompleted, permissions);
          } catch (IOException ioe) {
            throw new ApplicationException(
                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
                getThrowableMsg(INFO_ERROR_COPYING.get(entry.getName()), ioe),
                ioe);
          }
        }

        zipIn.closeEntry();
        entry = zipIn.getNextEntry();
        nEntries++;
      }

      if (isUnix()) {
        // Change the permissions for UNIX systems
        for (String perm : permissions.keySet()) {
          ArrayList<String> paths = permissions.get(perm);
          try {
            int result = Utils.setPermissionsUnix(paths, perm);
            if (result != 0) {
              throw new IOException("Could not set permissions on files "
                      + paths + ".  The chmod error code was: " + result);
            }
          } catch (InterruptedException ie) {
            throw new IOException("Could not set permissions on files " + paths
                + ".  The chmod call returned an InterruptedException.", ie);
          }
        }
      }
    } catch (IOException ioe) {
      throw new ApplicationException(
          ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
          getThrowableMsg(INFO_ERROR_ZIP_STREAM.get(zipFileName), ioe),
          ioe);
    }
  }

  /**
    * Copies a zip entry in the file system.
    * @param entry the ZipEntry object.
    * @param destination File where the entry will be copied.
    * @param is the ZipInputStream that contains the contents to be copied.
    * @param ratioBeforeCompleted the progress ratio before the zip file is copied.
    * @param ratioWhenCompleted the progress ratio after the zip file is copied.
    * @param permissions an ArrayList with permissions whose contents will be updated.
    * @throws IOException if an error occurs.
    */
  private void copyZipEntry(ZipEntry entry, File destination,
      ZipInputStream is, int ratioBeforeCompleted,
      int ratioWhenCompleted, Map<String, ArrayList<String>> permissions)
      throws IOException
  {
    if (application != null) {
      LocalizableMessage progressSummary =
              INFO_PROGRESS_EXTRACTING.get(Utils.getPath(destination));
      if (application.isVerbose())
      {
        application.notifyListenersWithPoints(ratioBeforeCompleted,
            progressSummary);
      }
      else
      {
        application.notifyListenersRatioChange(ratioBeforeCompleted);
      }
    }
    logger.info(LocalizableMessage.raw("extracting " + Utils.getPath(destination)));
    if (Utils.insureParentsExist(destination))
    {
      if (entry.isDirectory())
      {
        String perm = getDirectoryFileSystemPermissions(destination);
        ArrayList<String> list = permissions.get(perm);
        if (list == null)
        {
          list = new ArrayList<>();
        }
        list.add(Utils.getPath(destination));
        permissions.put(perm, list);

        if (!Utils.createDirectory(destination))
        {
          throw new IOException("Could not create path: " + destination);
        }
      } else
      {
        String perm = Utils.getFileSystemPermissions(destination);
        ArrayList<String> list = permissions.get(perm);
        if (list == null)
        {
          list = new ArrayList<>();
        }
        list.add(Utils.getPath(destination));
        permissions.put(perm, list);
        Utils.createFile(destination, is);
      }
    } else
    {
      throw new IOException("Could not create parent path: " + destination);
    }
    if (application != null && application.isVerbose())
    {
      application.notifyListenersDone(ratioWhenCompleted);
    }
  }

  /**
   * Returns the UNIX permissions to be applied to a protected directory.
   * @return the UNIX permissions to be applied to a protected directory.
   */
  private String getProtectedDirectoryPermissionUnix()
  {
    return "700";
  }

  /**
   * Returns the file system permissions for a directory.
   * @param path the directory for which we want the file permissions.
   * @return the file system permissions for the directory.
   */
  private String getDirectoryFileSystemPermissions(File path)
  {
    // TODO We should get this dynamically during build?
    return "755";
  }
}
