/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIESOR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.aries.util.io;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.apache.aries.util.filesystem.IFile;
import org.apache.aries.util.internal.MessageUtil;

public class IOUtils
{
  /**
   * Copy an InputStream to an OutputStream and close the InputStream afterwards.
   */
  public static void copy(InputStream in, OutputStream out) throws IOException
  {
    try {
      copyAndDoNotCloseInputStream(in, out);
    }
    finally {
      close(in);
    }
  }
  
  /**
   * Copy an InputStream to an OutputStream and do not close the InputStream afterwards.
   */
  public static void copyAndDoNotCloseInputStream(InputStream in, OutputStream out) throws IOException
  {
    int len;
    byte[] b = new byte[1024];
    while ((len = in.read(b)) != -1)
      out.write(b,0,len);
  }
  
  /**
   * Close some xStream for good :)
   */
  public static void close(Closeable c)
  {
    try {
      if (c != null)
        c.close();
    }
    catch (IOException e) {
      c = null;
    }
  }
  
  /**
   * A special version of close() for ZipFiles, which don't implement Closeable.
   * @param file the file to close. ZipFiles seem prone to file locking problems
   * on Windows, so to aid diagnostics we throw, not swallow, any exceptions. 
   */
  public static void close(ZipFile file) throws IOException
  {
    if (file != null) file.close();
  }
  
  public static OutputStream getOutputStream(File outputDir, String relativePath) throws IOException
  {
    int lastSeparatorIndex = relativePath.replace(File.separatorChar,'/').lastIndexOf("/");
    String dirName = null;
    String fileName = null;
    
    File outputDirectory;
    if (lastSeparatorIndex != -1)
    {
      dirName = relativePath.substring(0, lastSeparatorIndex);
      fileName = relativePath.substring(lastSeparatorIndex + 1);

      outputDirectory = new File(outputDir, dirName);
      
      if (!outputDirectory.exists() && !outputDirectory.mkdirs())
        throw new IOException(MessageUtil.getMessage("UTIL0015E", relativePath));
    }
    else
    {
      outputDirectory = outputDir;
      fileName = relativePath;
    }
    
    File outputFile = new File(outputDirectory, fileName);
    return new FileOutputStream(outputFile);
  }
  
  /**
   * Write the given InputStream to a file given by a root directory (outputDir) and a relative directory.
   * Necessary subdirectories will be created. This method will close the supplied InputStream.
   */
  public static void writeOut(File outputDir, String relativePath, InputStream content) throws IOException
  {
    OutputStream out = null;
    try {
      out = getOutputStream(outputDir, relativePath);
      IOUtils.copy(content, out);
    }
    finally {
      close(out);
    }
  }
  
  /**
   * Write the given InputStream to a file given by a root directory (outputDir) and a relative directory.
   * Necessary subdirectories will be created. This method will not close the supplied InputStream.
   */
  public static void writeOutAndDontCloseInputStream(File outputDir, String relativePath, InputStream content) throws IOException
  {
    OutputStream out = null;
    try {
      out = getOutputStream(outputDir, relativePath);
      IOUtils.copyAndDoNotCloseInputStream(content, out);
    }
    finally {
      close(out);
    }
  }
  
   /** 
   * Zip up all contents of rootDir (recursively) into targetStream
   */
  @SuppressWarnings("unchecked")
  public static void zipUp (File rootDir, OutputStream targetStream) throws IOException
  {
    ZipOutputStream out = null;
    try { 
      out = new ZipOutputStream (targetStream);
      zipUpRecursive(out, "", rootDir, (Set<String>) Collections.EMPTY_SET);
    } finally { 
      close(out);
    }
  }
  
  /**
   * Zip up all contents of rootDir (recursively) into targetFile
   */
  @SuppressWarnings("unchecked")
  public static void zipUp(File rootDir, File targetFile) throws IOException
  {
    ZipOutputStream out = null; 
    try {
      out = new ZipOutputStream(new FileOutputStream(targetFile));
      zipUpRecursive(out, "", rootDir, (Set<String>) Collections.EMPTY_SET);
    }
    finally {
      close(out);
    }
  }
  
  /**
   * Jar up all the contents of rootDir (recursively) into targetFile and add the manifest
   */
  public static void jarUp(File rootDir, File targetFile, Manifest manifest) throws IOException
  {
    JarOutputStream out = null;
    try {
      out = new JarOutputStream(new FileOutputStream(targetFile), manifest);
      zipUpRecursive(out, "", rootDir, new HashSet<String>(Arrays.asList("META-INF/MANIFEST.MF")));
    }
    finally {
      close(out);
    }
  }
  
  /**
   * Helper method used by zipUp
   */
  private static void zipUpRecursive(ZipOutputStream out, String prefix, 
      File directory, Set<String> filesToExclude) throws IOException
  {
    File[] files = directory.listFiles();
    if (files != null) 
    {
      for (File f : files)
      {        
        String fileName; 
        if (f.isDirectory())
          fileName = prefix + f.getName() + "/";
        else
          fileName = prefix + f.getName();
        
        if (filesToExclude.contains(fileName))
          continue;
        
        ZipEntry ze = new ZipEntry(fileName);
        ze.setSize(f.length());
        ze.setTime(f.lastModified());
        out.putNextEntry(ze);

        if (f.isDirectory()) 
          zipUpRecursive(out, fileName, f, filesToExclude);
        else 
        {
          IOUtils.copy(new FileInputStream(f), out);
        }
      }
    }
  }
  
  /**
   * Do rm -rf
   */
  public static boolean deleteRecursive(File root)
  {
    if (!root.exists())
      return false;
    else if (root.isFile())
      return root.delete();
    else {
      boolean result = true;
      for (File f : root.listFiles())
      {
        result = deleteRecursive(f) && result;
      }
      return root.delete() && result;
    }
  }
  
  /**
   * Unpack the zip file into the outputDir
   * @param zip
   * @param outputDir
   * @return true if the zip was expanded, false if the zip was found not to be a zip
   * @throws IOException when there are unexpected issues handling the zip files.
   */
  public static boolean unpackZip(IFile zip, File outputDir) throws IOException{
    boolean success=true;
    //unpack from fileOnDisk into bundleDir.
    ZipInputStream zis = null;
    try{
      boolean isZip = false;
      ZipEntry zipEntry = null;
      try {
        zis = new ZipInputStream (zip.open());
        zipEntry = zis.getNextEntry();
        isZip = zipEntry != null; 
      } catch (ZipException e) { // It's not a zip - that's ok, we'll return that below. 
        isZip = false;
      } catch (UnsupportedOperationException e) {  // This isn't declared, but is thrown in practice
        isZip = false;                             // It's not a zip - that's ok, we'll return that below. 
      }
      if(isZip){
        do {
          File outFile = new File(outputDir, zipEntry.getName());
          String canonicalizedDir = outputDir.getCanonicalPath();
          if (!canonicalizedDir.endsWith(File.separator)) {
               canonicalizedDir += File.separator;
          }
          if (!outFile.getCanonicalPath().startsWith(canonicalizedDir)) {
            throw new IOException("The output file is not contained in the destination directory");
          }

          if (!zipEntry.isDirectory()) { 
            writeOutAndDontCloseInputStream(outputDir, zipEntry.getName(), zis);
          }
          zis.closeEntry();
          zipEntry = zis.getNextEntry();
        } while (zipEntry != null);
      }else{
        success=false;
      }
    }finally{
      IOUtils.close(zis);
    }
    return success;
  }
  

}
