/*******************************************************************************
 * Copyright (c) 2005, 2007 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jst.jee.archive.internal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
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.eclipse.jst.jee.archive.ArchiveSaveFailureException;


public class ArchiveUtil {
	
	protected static String tempDirectoryName;

	protected static java.io.File tempDirectory;

	/**
	 * Returns the filename from the uri, or the segment after the last
	 * occurrence of a separator
	 */
	public static String getFileNameTail(String uri) {
		String tempURI = uri.replace('\\', '/');
		while (tempURI.endsWith("/")) //$NON-NLS-1$
			tempURI = tempURI.substring(0, tempURI.length() - 1);
		int lastIndex = tempURI.lastIndexOf('/');
		if (lastIndex == -1)
			return uri;
		return uri.substring(lastIndex + 1, tempURI.length());
	}

	public static java.io.File getTempDirectory() {
		return tempDirectory;
	}

	public static java.io.File createTempFile(String baseName) throws IOException {
		return createTempFile(baseName, getTempDirectory());
	}

	public static java.io.File createTempFile(String baseName, java.io.File directory) throws IOException {
		String fileName = getFileNameTail(baseName);
		if (fileName.length() < 3) {
			fileName = "WSTMP" + fileName; //$NON-NLS-1$
		}
		java.io.File tempFile = java.io.File.createTempFile(fileName, null, directory);
		DeleteOnExitUtility.markForDeletion(tempFile);
		return tempFile;
	}

	/**
	 * returns a list of all files, recursive, that can't be written
	 */
	public static List getWriteProtectedFiles(java.io.File aFile, List aList) {
		if (aList == null)
			aList = new ArrayList();
		if (aFile.exists() && !aFile.canWrite())
			aList.add(aFile);
		if (aFile.isDirectory()) {
			java.io.File[] files = aFile.listFiles();
			for (int i = 0; i < files.length; i++) {
				getWriteProtectedFiles(files[i], aList);
			}
		}
		return aList;
	}

	public static void checkWriteable(java.io.File aFile) throws ArchiveSaveFailureException {
		List locked = ArchiveUtil.getWriteProtectedFiles(aFile, null);
		if (locked.isEmpty())
			return;

		StringBuffer msg = new StringBuffer();
		msg.append("Cannot write to file: "); //$NON-NLS-1$
		msg.append(aFile.getAbsolutePath());
		msg.append('\n');
		msg.append("One or more files is write protected or locked:"); //$NON-NLS-1$
		msg.append('\n');
		for (int i = 0; i < locked.size(); i++) {
			java.io.File lockedFile = (java.io.File) locked.get(i);
			msg.append(lockedFile.getAbsolutePath());
			msg.append('\n');
		}
		throw new ArchiveSaveFailureException(msg.toString());
	}

	/**
	 * deletes a file from the file system; for directories, recurse the
	 * subdirectories and delete them as well
	 * 
	 * @return true if successful; false if any file or sub file could not be
	 *         deleted
	 */
	public static boolean delete(java.io.File aFile) {
		if (aFile == null)
			return true;
		if (aFile.isDirectory()) {
			java.io.File[] files = aFile.listFiles();
			if (files != null) {
				for (int i = 0; i < files.length; i++) {
					if (!delete(files[i]))
						return false;
				}
			}
		}
		return aFile.delete();
	}

	/**
	 * If we can rename it then we can delete it
	 */
	public static boolean isRenameable(java.io.File orig) {
		java.io.File origCopy1 = null;
		java.io.File origCopy2 = null;
		try {
			origCopy1 = orig.getCanonicalFile();
			origCopy2 = orig.getCanonicalFile();
		} catch (java.io.IOException ex) {
			return false;
		}
		String name = null;
		String baseName = "save.tmp"; //$NON-NLS-1$
		try {
			if (orig.getParent() != null)
				baseName = new java.io.File(orig.getParent(), baseName).getCanonicalPath();
		} catch (java.io.IOException ex) {
			return false;
		}

		java.io.File temp = null;
		int index = 0;
		do {
			name = baseName + index;
			temp = new java.io.File(name);
			index++;
		} while (temp.exists());
		return origCopy1.renameTo(temp) && temp.renameTo(origCopy2);
	}

	public static void cleanupAfterTempSave(String aUri, java.io.File original, java.io.File destinationFile) throws ArchiveSaveFailureException {

		checkWriteable(original);
		boolean deleteWorked = false;
		if (original.isDirectory() && !isRenameable(original)) {
			// TODO throw new
			// SaveFailureException(CommonArchiveResourceHandler.getString(CommonArchiveResourceHandler.unable_replace_EXC_,
			// (new Object[]{original.getAbsolutePath()}))); // = "Unable to
			// replace original archive "
		}

		for (int i = 0; i < 10; i++) {
			if (ArchiveUtil.delete(original)) {
				deleteWorked = true;
				break;
			}
			try {
				// TODO Major hack here; the problem is that a previous call
				// to close the source file may not yet have
				// been reflected in the os/vm; therefore a subsequent call
				// to delete fails. To get around this,
				// wait for a bit and retry; if it continues to fail, then
				// time out and throw an exception
				Thread.sleep(250);
			} catch (InterruptedException e) {
				// Ignore
			}
		}
		if (deleteWorked) {
			for (int i = 0; i < 10; i++) {
				if (destinationFile.renameTo(original)){
					DeleteOnExitUtility.fileHasBeenDeleted(destinationFile);
					return;
				}
				try {
					Thread.sleep(250);
				} catch (InterruptedException e) {
					// Ignore
				}
			}
		}
		// TODO throw new
		// SaveFailureException(CommonArchiveResourceHandler.getString(CommonArchiveResourceHandler.unable_replace_EXC_,
		// (new Object[]{original.getAbsolutePath()}))); // = "Unable to replace
		// original archive "
	}

	/**
	 * Copy all the data from the input stream to the output stream up until the
	 * first end of file character, and close the two streams
	 */
	public static void copy(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[1024];
		try {
			int n = in.read(buffer);
			while (n > 0) {
				out.write(buffer, 0, n);
				n = in.read(buffer);
			}
		} finally {
			if (!(in instanceof ZipInputStream))
				in.close();
			if (!(out instanceof ZipOutputStream))
				out.close();
		}
	}
	
	public static void warn(Throwable e){
		org.eclipse.jem.util.logger.proxy.Logger.getLogger().logWarning(e);
	}
	
	public static void warn(String message) {
		org.eclipse.jem.util.logger.proxy.Logger.getLogger().logWarning(message);
	}
	
	public static ZipFile newZipFile(String fileName)throws ZipException, IOException {
		return ArchiveUtil.newZipFile(new File(fileName), ZipFile.OPEN_READ);
	}
	public static ZipFile newZipFile(File aFile)throws ZipException, IOException {
		return ArchiveUtil.newZipFile(aFile, ZipFile.OPEN_READ);
	}
	
	/**
	 * Utility to create ZipFiles which avoid memory leaks 
	 * because closing them fails to close open inputstreams.
	 * There is a SUN bug open for this: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6735255 
     * but it looks like the "fix" will be to change the Javadoc :-(   
	 * @param aFile mode
	 * @return
	 * @throws ZipException
	 * @throws IOException
	 */
	public static ZipFile newZipFile(File aFile, int mode) throws ZipException, IOException {
		return new ZipFile(aFile, mode){
			Collection <InputStream> openStreams = null;
			
			public InputStream getInputStream(ZipEntry entry) throws IOException {
				InputStream in = super.getInputStream(entry);
				if(in != null){
					if(openStreams == null){
						openStreams = new ArrayList<InputStream>();
					}
					openStreams.add(in);
				}
				return in;
			}
			
			public void close() throws IOException {
				closeOpenStreams();
				super.close();
			}

			private void closeOpenStreams() {
				if(openStreams != null){
					for(InputStream in:openStreams){
						try {
							in.close();
						} catch (IOException e) {
							warn(e);
						}
					}
				}
				
			}
		};
	}
}
