//------------------------------------------------------------------------------
// Copyright (c) 2005, 2006 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 implementation
//------------------------------------------------------------------------------
package org.eclipse.epf.common.utils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.nio.CharBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.eclipse.epf.common.CommonPlugin;

/**
 * Utility class for managing directories and files.
 * 
 * @author Kelvin Low
 * @author Jinhua Xi
 * @since 1.0
 */
public class FileUtil {

	/**
	 * Platform-specific line separator.
	 */
	public static final String LINE_SEP = System.getProperty("line.separator"); //$NON-NLS-1$

	/**
	 * Platform-specific file separator.
	 */
	public static final String FILE_SEP = System.getProperty("file.separator"); //$NON-NLS-1$

	/**
	 * Platform-specific line separator length.
	 */
	public static final int LINE_SEP_LENGTH = LINE_SEP.length();

	/**
	 * UNC path prefix.
	 */
	public static final String UNC_PATH_PREFIX = "\\\\"; //$NON-NLS-1$

	/**
	 * UNC path prefix length.
	 */
	public static final int UNC_PATH_PREFIX_LENGTH = UNC_PATH_PREFIX.length();

	/**
	 * ISO-8859-1 encoding.
	 */
	public static final String ENCODING_ISO_8859_1 = "ISO-8859-1"; //$NON-NLS-1$

	/**
	 * UTF-8 encoding.
	 */
	public static final String ENCODING_UTF_8 = "UTF-8";//$NON-NLS-1$

	/**
	 * Private constructor to prevent this class from being instantiated. All
	 * methods in this class should be static.
	 */
	private FileUtil() {
	}

	/**
	 * Returns the absolute path for the given file or directory.
	 * 
	 * @param file
	 *            The given file or directory.
	 * @return The absolute path of the given file or directory.
	 */
	public static String getAbsolutePath(File file) {
		return file.getAbsolutePath().replace('\\', '/');
	}

	/**
	 * Returns the absolute path for the given file or directory.
	 * 
	 * @param file
	 *            The given file or directory name.
	 * @return The absolute path of the given file or directory.
	 */
	public static String getAbsolutePath(String file) {
		return getAbsolutePath(new File(file));
	}

	/**
	 * Returns the absolute path for the given URL.
	 * 
	 * @param url
	 *            The given URL.
	 * @return The absolute path of the given URL.
	 */
	public static String getAbsolutePath(URL url) {
		String pathName = url.getFile().substring(1);
		String result = NetUtil.decodeUrl(pathName, null);
		return result;
	}

	/**
	 * Returns the parent directory of the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @return The name of the parent directory.
	 */
	public static String getParentDirectory(String path) {
		return (new File(path)).getParent();
	}

	/**
	 * Returns the file name and extension from the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @return The file name and the file extension.
	 */
	public static String getFileName(String path) {
		return getFileName(path, true);
	}

	/**
	 * Returns the file name from the given path, with or without the file
	 * extension.
	 * 
	 * @param path
	 *            The path name.
	 * @param withExtension
	 *            If true, include the file extension in the result.
	 * @return The file name with or without the file extension.
	 */
	public static String getFileName(String path, boolean withExtension) {
		String normalizedPath = path.replace('\\', '/');

		int prefixLength = 0;
		if (normalizedPath.startsWith(NetUtil.FILE_URI_PREFIX)) {
			prefixLength = NetUtil.FILE_URI_PREFIX_LENGTH;
		} else if (normalizedPath.startsWith(NetUtil.HTTP_URI_PREFIX)) {
			prefixLength = NetUtil.HTTP_URI_PREFIX_LENGTH;
		}

		String fileName;
		int index = normalizedPath.lastIndexOf("/"); //$NON-NLS-1$
		if (index < prefixLength) {
			fileName = normalizedPath.substring(prefixLength);
		} else {
			fileName = path.substring(index + 1);
		}

		if (withExtension) {
			return fileName;
		}

		index = fileName.indexOf("."); //$NON-NLS-1$
		return (index > 0) ? fileName.substring(0, index) : fileName;
	}

	/**
	 * Returns the relative path for the target path from the base path.
	 * 
	 * @param path
	 *            The target path.
	 * @param basePath
	 *            The base path.
	 * @return The relative path.
	 */
	public static String getRelativePathToBase(File path, File basePath) {
		try {
			String dir = path.toURL().toExternalForm();
			String baseDir = basePath.toURL().toExternalForm();
			StringBuffer result = new StringBuffer();
			if (dir.indexOf(baseDir) == 0) {
				String delta = dir.substring(baseDir.length());
				for (int i = 0; i < delta.length(); i++) {
					if (delta.charAt(i) == '/') {
						result.append("../"); //$NON-NLS-1$
					}
				}
			}
			return result.toString();
		} catch (Exception e) {
			return ""; //$NON-NLS-1$
		}
	}

	public static String getRelativePath(File path, File basePath) {
		try {
			String dir = path.toURL().toExternalForm();
			String baseDir = appendSeparator(basePath.toURL().toExternalForm(),
					"/"); //$NON-NLS-1$
			StringBuffer result = new StringBuffer();
			while (dir.indexOf(baseDir) == -1) {
				basePath = basePath.getParentFile();
				baseDir = appendSeparator(basePath.toURL().toExternalForm(),
						"/"); //$NON-NLS-1$
				result.append("../"); //$NON-NLS-1$
			}
			if (dir.indexOf(baseDir) == 0) {
				String delta = dir.substring(baseDir.length());
				result.append(delta);
			}
			return result.toString();
		} catch (Exception e) {
			return ""; //$NON-NLS-1$
		}
	}

	/**
	 * Appends the platform specific path separator to the end of the given
	 * path.
	 * 
	 * @param path
	 *            The path name.
	 * @return The path name appended with the platform specific path separator.
	 */
	public static String appendSeparator(String path) {
		return appendSeparator(path, File.separator);
	}

	/**
	 * Appends the given path separator to the end of the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @param separator
	 *            The path separator.
	 * @return The path name appended with the given separator.
	 */
	public static String appendSeparator(String path, String separator) {
		return path.endsWith(separator) ? path : path + separator;
	}

	/**
	 * Removes the ending path separator from the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @return The path name minus the platform specific path separator.
	 */
	public static String removeSeparator(String path) {
		return path.endsWith(File.separator) ? path.substring(0,
				path.length() - 1) : path;
	}

	/**
	 * Removes the ending path separator from the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @return The path name minus the path separator "\\" or "/".
	 */
	public static String removeAllSeparator(String path) {
		return path.endsWith("/") || path.endsWith("\\") ? path.substring(0, path.length() - 1) : path; //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Removes the ending path separator from the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @param separator
	 *            The path separator.
	 * @return The path name minus the separator.
	 */
	public static String removeSeparator(String path, String separator) {
		return path.endsWith(separator) ? path.substring(0, path.length() - 1)
				: path;
	}

	/**
	 * Replaces the file name with another in the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @param oldFileName
	 *            The old file name.
	 * @param newFileName
	 *            The new file name.
	 * @return The new path name with the new file name.
	 */
	public static String replaceFileName(String path, String oldFileName,
			String newFileName) {
		int index = path.lastIndexOf(oldFileName);
		return path.substring(0, index) + newFileName;
	}

	/**
	 * Replaces the file extension with another in the given path.
	 * 
	 * @param path
	 *            The path name.
	 * @param oldFileExt
	 *            The old file extension.
	 * @param newFileExt
	 *            The new file extension.
	 * @return The new path with the new file extension.
	 */
	public static String replaceExtension(String path, String oldExt,
			String newExt) {
		int index = path.lastIndexOf(oldExt);
		return path.substring(0, index) + newExt;
	}

	/**
	 * Returns the locale-specific path of a base path.
	 * 
	 * @param path
	 *            The base path name.
	 * @param localeStr
	 *            The locale string.
	 * @return The locale-specific path.
	 */
	public static String getLocalePath(String path, String localeStr) {
		if (StrUtil.isBlank(localeStr)) {
			return path;
		}
		String fileName = getFileName(path);
		return replaceFileName(path, fileName, localeStr + "/" + fileName); //$NON-NLS-1$
	}

	/**
	 * Returns the locale-specific path of a base path.
	 * 
	 * @param path
	 *            The base path name.
	 * @param locale
	 *            The locale object.
	 * @return The locale-specific path.
	 */
	public static String getLocalePath(String path, Locale locale) {
		return locale == null ? path : getLocalePath(path, locale.toString());
	}

	/**
	 * Writes the given text to a text file.
	 * 
	 * @param fileName
	 *            The target file name.
	 * @param text
	 *            The text to write.
	 * @return true if the given text is written successfully to file.
	 */
	public static boolean writeFile(String filename, String text) {
		FileWriter writer = null;
		try {
			writer = new FileWriter(filename);
			writer.write(text);
			writer.flush();
		} catch (IOException e) {
		} finally {
			if (writer != null) {
				try {
					writer.close();
					return true;
				} catch (Exception e) {
				}
			}
		}
		return false;
	}

	/**
	 * Write the given text to a file with UTF-8 encoding.
	 * 
	 * @param fileName
	 *            The target file name.
	 * @param text
	 *            The text to write.
	 * @param append ture to append to the end of the file, false to override the file
	 * @return true if the given text is written successfully to file.
	 */
	public static boolean writeUTF8File(String filename, String text) {
		return writeUTF8File(filename, text, false);
	}

	/**
	 * Write the given text to a file with UTF-8 encoding.
	 * 
	 * @param fileName
	 *            The target file name.
	 * @param text
	 *            The text to write.
	 * @param append ture to append to the end of the file, false to override the file
	 * @return true if the given text is written successfully to file.
	 */
	public static boolean writeUTF8File(String filename, String text, boolean append) {
		OutputStreamWriter writer = null;
		FileOutputStream fileOut = null;
		try {
			fileOut = new FileOutputStream(filename, append);
			writer = new OutputStreamWriter(fileOut, ENCODING_UTF_8);
			writer.write(text);
			writer.flush();
			fileOut.flush();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			if (writer != null) {
				try {
					writer.close();
					return true;
				} catch (Exception e) {
				}
			}
			if (fileOut != null) {
				try {
					fileOut.close();
					return true;
				} catch (Exception e) {
				}
			}
		}
		return false;
	}

	/**
	 * Write the content of the given URI to the given output stream.
	 * 
	 * @param uri
	 *            The source URI.
	 * @param output
	 *            The output stream.
	 */
	public static void writeFile(String uri, OutputStream output)
			throws IOException {
		if (uri == null) {
			return;
		}

		InputStream input = null;
		try {
			input = NetUtil.getInputStream(uri);
			int bytesRead;
			byte[] buf = new byte[4096];
			while ((bytesRead = input.read(buf, 0, 4096)) > 0) {
				output.write(buf, 0, bytesRead);
			}
			output.flush();
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (Exception e) {
				}
			}
		}
	}

	/**
	 * Write the content of the given URI to the given PrintWriter.
	 * 
	 * @param uri
	 *            The source URI.
	 * @param writer
	 *            The PrintWriter obejct.
	 */
	public static void writeFile(String uri, PrintWriter pw) throws IOException {
		if (uri == null) {
			return;
		}

		InputStreamReader input = null;
		try {
			input = new InputStreamReader(NetUtil.getInputStream(uri));
			int charsRead;
			char[] buf = new char[4096];
			while ((charsRead = input.read(buf, 0, 4096)) > 0) {
				pw.write(buf, 0, charsRead);
			}
			pw.flush();
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (Exception e) {
				}
			}
		}
	}

	/**
	 * Recursively delete all sub-directories and files in the given directory.
	 * Does not delete the given directory.
	 * 
	 * @param dir
	 *            The directory containing the sub-directories and files.
	 * @return boolean true if delete successful
	 */
	public static boolean deleteAllFiles(String dir) {
		boolean ret = true;
		File targetDir = new File(dir);
		File[] files = targetDir.listFiles();
		if (files != null) {
			for (int i = 0; i < files.length; i++) {
				if (files[i].isDirectory()) {
					ret = ret && deleteAllFiles(files[i].getAbsolutePath());
				}
				ret = ret && files[i].delete();
			}
		}

		return ret;
	}

	/**
	 * Copies the content of the source file to the target file.
	 * 
	 * @param srcFile
	 *            The source file or path.
	 * @param tgtFile
	 *            The target file or path.
	 */
	public static void copyFile(File srcFile, File tgtFile) throws IOException {
		if (srcFile.equals(tgtFile))
			return;

		if (!srcFile.exists() || !srcFile.canRead()) {
			return;
		}

		if (tgtFile.exists() && !tgtFile.canWrite()) {
			return;
		}

		if (!srcFile.isFile()) {
			File[] files = srcFile.listFiles();
			if (files != null) {
				for (int i = 0; i < files.length; i++) {
					copyFile(files[i], tgtFile);
				}
			}
			return;
		}

		FileInputStream src = null;
		FileOutputStream tgt = null;
		try {
			src = new FileInputStream(srcFile);
			if (tgtFile.isFile()) {
				tgt = new FileOutputStream(tgtFile);
			} else {
				String srcPath = srcFile.toURL().toString();
				int index = srcPath.lastIndexOf("/"); //$NON-NLS-1$
				String srcFileName = srcPath.substring(index + 1);
				if (srcFile.equals(new File(tgtFile, srcFileName))) {
					return;
				}
				tgt = new FileOutputStream(new File(tgtFile, srcFileName));
			}
			byte[] buffer = new byte[4096];
			int bytes_read;
			while ((bytes_read = src.read(buffer)) != -1) {
				tgt.write(buffer, 0, bytes_read);
			}
		} finally {
			if (src != null) {
				try {
					src.close();
				} catch (Exception e) {
				}
			}
			if (tgt != null) {
				try {
					tgt.close();
				} catch (Exception e) {
				}
			}
			tgtFile.setLastModified(srcFile.lastModified());
		}
	}

	/**
	 * Copies the content of the source file to the target file.
	 * 
	 * @param srcFileName
	 *            The source file name.
	 * @param tgtFileName
	 *            The target file name.
	 */
	public static void copyFile(String srcFileName, String tgtFileName)
			throws IOException {
		copyFile(new File(srcFileName), new File(tgtFileName));
	}

	/**
	 * Copies the content of a directory to another directory.
	 * 
	 * @param srcDirName
	 *            The source directory name.
	 * @param tgtDirName
	 *            The target directory name.
	 */
	public static void copyDir(String srcDirName, String tgtDirName)
			throws IOException {
		copyFile(new File(srcDirName), new File(tgtDirName));
	}

	/**
	 * Copies one file to another - operates ONLY on files, not on directories.
	 * 
	 * @param source
	 * @param dest
	 * @throws IOException
	 */
	public static void copyfile(File source, File dest) throws IOException {
		if (source.equals(dest))
			return;

		FileInputStream input = null;
		FileOutputStream output = null;

		try {
			input = new FileInputStream(source);
			FileChannel in = input.getChannel();
			if (!dest.exists()) {
				dest.getParentFile().mkdirs();
			}
			output = new FileOutputStream(dest);
			FileChannel out = output.getChannel();
			out.transferFrom(in, 0, source.length());
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (IOException e) {
				}
			}
			if (output != null) {
				try {
					output.close();
				} catch (IOException e) {
				}
			}
		}
	}

	public static void copydirectory(File sourceDir, File destDir)
			throws IOException {
		if (!sourceDir.exists() || !destDir.exists()) {
			return;
		}

		if (!sourceDir.isDirectory() || !destDir.isDirectory()) {
			return;
		}

		File[] files = sourceDir.listFiles();
		if (files != null) {
			for (int i = 0; i < files.length; i++) {
				// calc destination name
				String destName = destDir
						+ File.separator
						+ files[i].getAbsolutePath().substring(
								sourceDir.getAbsolutePath().length() + 1);
				if (files[i].isFile()) {
					// copy the file
					copyfile(files[i], new File(destName));
				} else if (files[i].isDirectory()) {
					// copy directory recursively
					File destFile = new File(destName);
					destFile.mkdirs();
					copydirectory(files[i], destFile);
				}
			}
		}

	}

	// for some reason, this guy locks the file, if you try to update the file,
	// got the following exception
	// java.io.FileNotFoundException:
	// (The requested operation cannot be performed on a file with a user-mapped
	// section open)
	// need to handle later
	public static CharBuffer readFile(File file) throws IOException {
		FileInputStream input = null;
		CharBuffer charBuffer = null;
		try {
			input = new FileInputStream(file);
			FileChannel inChannel = input.getChannel();
			int length = (int) inChannel.size();
			MappedByteBuffer byteBuffer = inChannel.map(
					FileChannel.MapMode.READ_ONLY, 0, length);
			Charset charset = Charset.forName(ENCODING_ISO_8859_1);
			CharsetDecoder decoder = charset.newDecoder();
			charBuffer = decoder.decode(byteBuffer);
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (IOException e) {
				}
			}
		}
		return charBuffer;
	}

	public static StringBuffer readFile(File file, String encoding)
			throws IOException {

		StringBuffer result = new StringBuffer();
		FileInputStream fis = null;
		InputStreamReader reader = null;
		try {
			char[] buffer = new char[1024];
			fis = new FileInputStream(file);
			reader = new InputStreamReader(fis, encoding);
			int size;
			while ((size = reader.read(buffer, 0, 1024)) > 0) {
				result.append(buffer, 0, size);
			}
		} finally {
			if (fis != null) {
				fis.close();
			}

			if (reader != null) {
				reader.close();
			}
		}

		return result;
	}

	/**
	 * Uses Java 1.4's FileLock class to test for a file lock
	 * 
	 * @param file
	 * @return
	 */
	public static boolean isFileLocked(File file) {
		boolean isLocked = false;
		FileOutputStream input = null;
		FileLock lock = null;

		try {
			input = new FileOutputStream(file);
			FileChannel fileChannel = input.getChannel();

			lock = fileChannel.tryLock();

			if (lock == null)
				isLocked = true;
			else
				lock.release();
		} catch (Exception e) {
			if (e instanceof SecurityException)
				// Can't write to file.
				isLocked = true;
			else if (e instanceof FileNotFoundException)
				isLocked = false;
			else if (e instanceof IOException)
				isLocked = true;
			// OverlappingFileLockException means that this JVM has it locked
			// therefore it is not locked to us
			else if (e instanceof OverlappingFileLockException)
				isLocked = false;
			// Could not get a lock for some other reason.
			else
				isLocked = true;
		} finally {
			if (input != null) {
				try {
					input.close();
				} catch (Exception ex) {
				}
			}
		}
		return isLocked;
	}

	/**
	 * Locks a file for the current JVM. Will create the file if it does not
	 * exist
	 * 
	 * @param file
	 * @return a FileLock object, or null if file could not be locked
	 */
	public static FileLock lockFile(File file) {
		FileOutputStream input = null;
		FileLock lock = null;
		try {
			input = new FileOutputStream(file);
			FileChannel fileChannel = input.getChannel();
			lock = fileChannel.tryLock();

			if (lock.isValid())
				return lock;
		} catch (Exception e) {
			// Could not get a lock for some reason.
			return null;
		} finally {
			try {
				if (input != null && (lock == null || !lock.isValid())) {
					input.close();
				}
			} catch (Exception ex) {
			}
		}
		return null;
	}

	/**
	 * get all files in the specified path
	 * @param path, absolute path of a folder
	 * @param fileList List the list to collect the Files
	 * @param recursive boolean, if true find the files in sub folders as well
	 */
	public static void getAllFiles(File path, List fileList, boolean recursive)
	{
		// get all files in the specified folder
		if (path.isDirectory()) 
		{		
			File[] files = path.listFiles();
			if (files != null) 
			{
				for (int i = 0; i < files.length; i++) 
				{	
					if ( files[i].isFile() )
					{
						fileList.add(files[i]);
					}
					else if (recursive)
					{
						getAllFiles(files[i], fileList, recursive);
					}
				}
			}
		}
	}

	/**
	 * given a directory and extension, returns all files (recursively) whose
	 * extension startsWith the given extension
	 * 
	 * @param f
	 * @param extension
	 * @return
	 */
	public static List fileList(File f, String extension) {
		extension = extension.toUpperCase();
		List returnList = new ArrayList();
		try {
			if (f.isDirectory()) { // if dir then recurse
				String[] flist = f.list();
				for (int i = 0; i < flist.length; ++i) {
					File fc = new File(f.getPath(), flist[i]);
					returnList.addAll(fileList(fc, extension));
				}
			} else { // ordinary file
				if (extension != null) {
					String name = f.getName().toUpperCase();
					if (name.lastIndexOf(".") != -1) //$NON-NLS-1$
						if (name
								.substring(name.lastIndexOf(".") + 1).startsWith(extension)) { //$NON-NLS-1$
							returnList.add(f);
						}
				} else
					returnList.add(f);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return returnList;
	}

	/**
	 * given a directory and extension, returns all files (recursively)whose
	 * extension does not startsWith the given extension
	 * 
	 * @param f
	 * @param extension
	 * @return
	 */
	public static List fileListExcludeExt(File f, String extension) {
		List returnList = new ArrayList();
		try {
			if (f.isDirectory()) { // if dir then recurse
				String[] flist = f.list();
				for (int i = 0; i < flist.length; ++i) {
					File fc = new File(f.getPath(), flist[i]);
					returnList.addAll(fileListExcludeExt(fc, extension));
				}
			} else { // ordinary file
				if (extension != null) {
					String name = f.getName();
					if (name.lastIndexOf(".") != -1) //$NON-NLS-1$
						if (!(name.substring(name.lastIndexOf(".") + 1).startsWith(extension))) { //$NON-NLS-1$
							returnList.add(f);
						}
				} else
					returnList.add(f);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return returnList;
	}
	
	/**
	 * get all file paths in the specified path
	 * @param path, absolute path of a folder
	 * @param recursive boolean, if true find the files in sub folders as well
	 */
	public static ArrayList getAllFileAbsolutePaths(File path, boolean recursive) {	
		ArrayList files = new ArrayList();
		getAllFiles(path, files, recursive);
		ArrayList paths = new ArrayList();
		for (int i=0; i<files.size(); i++) {
			String absPath = ((File) files.get(i)).getAbsolutePath();
			paths.add(absPath);
		}
		return paths;
	}
	
	/**
	 * Moves a file
	 * 
	 * Attempts to rename the file first.  If that fails,
	 * will copy the sourceFile to destFile and delete the sourceFile.
	 * @param sourceFile
	 * @param destFile
	 * @return
	 */
	public static boolean moveFile(File sourceFile, File destFile) {
		try {
			// first try the renameTo method
			if (sourceFile.renameTo(destFile)) {
				return true;
			}
			else {
				// try to copy file, delete original
				copyfile(sourceFile, destFile);
				sourceFile.delete();
				return true;
			}
		} catch (Exception t) {
			CommonPlugin.getDefault().getLogger().logError(t);
			return false;
		}
	}
	
}