/*
 * Created on Jul 27, 2006
 * 
 * (C) Copyright TANDBERG Television Ltd.
 */

package com.tandbergtv.workflow.webservice.filesubsystem;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Enumeration;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.apache.commons.io.FileSystemUtils;
import org.apache.log4j.Logger;
import org.apache.tools.tar.n2.TarEntry;
import org.apache.tools.tar.n2.TarInputStream;

import com.tandbergtv.workflow.webservice.filesubsystem.util.ArchiveUtility;

/**
 * Implementation of the FileManagementService interface.
 * 
 * @author Vijay Silva
 */
public class FileManager implements FileManagementService
{
	private static final String ZIP_EXTENSION = ".zip";

	private static final String TAR_EXTENSION = ".tar";

	private static final String CHECKSUM_ALGORITHM = "MD5";

	private static final String FILE_SEPERATOR = System.getProperty("file.separator");

	private static final String LINE_SEPERATOR = System.getProperty("line.separator");

	private static final Logger logger = Logger.getLogger(FileManager.class);

	private static FileManager instance = new FileManager();

	/**
	 * Method to get the File Manager instance
	 * 
	 * @return The FileManager
	 */
	public static FileManager getInstance()
	{
		return instance;
	}

	// Singleton, cannot instantiate
	private FileManager()
	{
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#fileExists(java.lang.String)
	 */
	public boolean fileExists(String filePath)
	{
		File f = new File(filePath);

		return f.exists() && f.isFile();
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#moveFile(java.lang.String,
	 *      java.lang.String)
	 */
	public void moveFile(String filePath, String destinationPath) throws FileManagementException
	{
		logger.debug("Moving file: " + filePath + " to destination path: " + destinationPath);

		File srcFile = new File(filePath);
		File destFile = new File(destinationPath);

		if (!srcFile.exists())
			throw new FileManagementException("File: " + srcFile + " not found.");

		if (srcFile.isDirectory())
			throw new FileManagementException("File: " + srcFile
					+ " is a directory which cannot be moved. Operation not allowed.");

		/*
		 * If dest file is a directory, get file name from src path and perform move
		 */
		if (destFile.isDirectory())
			destFile = new File(destinationPath + FILE_SEPERATOR + srcFile.getName());

		if (!srcFile.renameTo(destFile))
			throw new FileManagementException("Cannot move File: " + srcFile + " to destination: "
					+ destFile);

		logger.info("Moved File: " + srcFile.getAbsolutePath() + " to: "
				+ destFile.getAbsolutePath());
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#copyFile(java.lang.String,
	 *      java.lang.String)
	 */
	public void copyFile(String filePath, String destinationPath) throws FileManagementException
	{
		logger.debug("Copying file: " + filePath + " to destination path: " + destinationPath);

		File srcFile = new File(filePath);
		File destFile = new File(destinationPath);

		if (!srcFile.exists())
			throw new FileManagementException("File: " + srcFile + " not found.");

		if (srcFile.isDirectory())
		{
			String msg = "File: " + srcFile
					+ " is a directory which cannot be copied. Operation not allowed.";
			throw new FileManagementException(msg);
		}

		/*
		 * If dest file is a directory, get file name from src path and perform copy
		 */
		if (destFile.isDirectory())
			destFile = new File(destinationPath + FILE_SEPERATOR + srcFile.getName());

		if (srcFile.equals(destFile))
		{
			String msg = "Cannot copy file when the source and destination paths are the same.";
			throw new FileManagementException(msg);
		}

		FileInputStream in = null;
		FileOutputStream out = null;

		try
		{
			try
			{
				in = new FileInputStream(srcFile);
			}
			catch (FileNotFoundException ex)
			{
				String msg = "Failed to open file: " + srcFile + " for copying.";
				throw new FileManagementException(msg, ex);
			}

			try
			{
				out = new FileOutputStream(destFile);
			}
			catch (FileNotFoundException ex)
			{
				String msg = "Cannot open destination file: " + destFile + " for copying.";
				throw new FileManagementException(msg, ex);
			}

			FileChannel inputChannel = in.getChannel();
			FileChannel outputChannel = out.getChannel();

			long maxCount = getChunkSize() * 1024;
			long inputChannelSize = inputChannel.size();
			long position = 0;
			while (position < inputChannelSize)
			{
				long from = position;
				position += inputChannel.transferTo(position, maxCount, outputChannel);
				logger.debug(from + " -> " + position);
			}
		}
		catch (IOException ioe)
		{
			String msg = "Failed while copying the contents of file: " + srcFile
					+ " to destination: " + destFile;
			throw new FileManagementException(msg, ioe);
		}
		finally
		{
			if (in != null)
			{
				try
				{
					in.close();
				}
				catch (IOException ioe)
				{
					logger.warn("Failed to close input stream for file: " + srcFile
							+ " after reading the file contents.", ioe);
				}
			}

			if (out != null)
			{
				try
				{
					out.close();
				}
				catch (IOException ioe)
				{
					logger.warn("Failed to close output stream for file: " + destFile
							+ " after writing the copied file contents.", ioe);
				}
			}
		}

		logger.info("Copied File: " + srcFile.getAbsolutePath() + " to: "
				+ destFile.getAbsolutePath());
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#renameFile(java.lang.String,
	 *      java.lang.String)
	 */
	public void renameFile(String filePath, String newFileName) throws FileManagementException
	{
		logger.debug("Renaming file: " + filePath + " with new name: " + newFileName);

		File srcFile = new File(filePath);
		if (!srcFile.exists())
			throw new FileManagementException("File: " + srcFile + " not found.");

		if (srcFile.isDirectory())
		{
			String msg = "File: " + srcFile
					+ " is a directory which cannot be renamed. Operation not allowed.";
			throw new FileManagementException(msg);
		}

		if (newFileName == null)
		{
			String msg = "Target File Name: " + newFileName + " is invalid (null).";
			throw new FileManagementException(msg);
		}

		String folderPath = srcFile.getParent();
		String renamedFilePath = folderPath + FILE_SEPERATOR + newFileName;
		File targetFilePath = new File(renamedFilePath);

		if (!targetFilePath.getName().equals(newFileName))
		{
			String msg = "Target File Name: " + newFileName
					+ " contains a folder path and is not a valid file name.";
			throw new FileManagementException(msg);
		}

		if (!srcFile.renameTo(new File(renamedFilePath)))
		{
			String msg = "Cannot rename file: " + srcFile + ". Operation failed.";
			throw new FileManagementException(msg);
		}

		logger.info("Renamed File: " + srcFile.getAbsolutePath() + " to: " + newFileName);
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#removeFile(java.lang.String)
	 */
	public void removeFile(String filePath) throws FileManagementException
	{
		logger.debug("Removing file: " + filePath);

		File file = new File(filePath);

		if (!file.exists())
			throw new FileManagementException("File: " + file + " not found.");

		if (file.isDirectory())
			throw new FileManagementException("File: " + file
					+ " is a directory and cannot be removed. Operation not allowed.");

		if (!file.delete())
			throw new FileManagementException("Cannot delete File: " + file + ". Operation failed.");

		logger.info("Removed File: " + file.getAbsolutePath());
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#getFileSize(java.lang.String)
	 */
	public long getFileSize(String filePath) throws FileManagementException
	{
		logger.debug("Getting File Size for file: " + filePath);

		File file = new File(filePath);

		if (!file.exists())
			throw new FileManagementException("File: " + file + " not found.");

		if (file.isDirectory())
			throw new FileManagementException("No file name specified in the path: " + file);

		logger.info("Got File Size for File: " + file.getAbsolutePath());

		return file.length();
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#getFolderSize(java.lang.String)
	 */
	public long getFolderSize(String folderPath) throws FileManagementException
	{
		logger.debug("Calculating size of Folder: " + folderPath);

		File file = new File(folderPath);

		if (!file.exists())
			throw new FileManagementException("Folder: " + file + " not found.");

		if (!file.isDirectory())
			throw new FileManagementException("Folder: " + file + " is not a folder.");

		UsageStatistics stats = new UsageStatistics();
		long folderSize = this.getFolderSize(file, stats);
		stats.endDate = new Date();

		logger.info("Got Folder Size for Folder: " + file.getAbsolutePath()
				+ " with Usage Statistics: " + stats.toString());

		return folderSize;
	}
	
	private int getChunkSize()
	{
		ResourceBundle bundle = ResourceBundle.getBundle(getClass().getPackage().getName() + ".file");
		int chunk = 1024;
		
		try
		{
			chunk = Integer.parseInt(bundle.getString("copy.chunk.kb"));
		}
		catch (NumberFormatException e)
		{
			logger.info("Using default chunk size " + chunk + "KB");
		}
		
		return chunk;
	}

	/*
	 * Method to recursively calculate the size of a File / Directory and all its children.
	 */
	private long getFolderSize(File file, UsageStatistics stats)
	{
		if (file == null)
			return 0L;

		long fileSize = 0L;

		if (file.isDirectory())
		{
			stats.directoryCount++;
			File[] files = file.listFiles();
			for (File childFile : files)
			{
				fileSize += getFolderSize(childFile, stats);
			}
		}
		else
		{
			stats.fileCount++;
			fileSize = file.length();
		}

		return fileSize;
	}

	/**
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#getFileList(String,
	 *      boolean)
	 */
	public File[] getFileList(String folderPath, final boolean filesOnly)
			throws FileManagementException
	{
		File file = new File(folderPath);
		
		if (!file.exists() || !file.isDirectory())
		{
			String msg = "The file listing is requested for folder: " + folderPath
					+ " which does not exist.";
			logger.warn(msg);
		}
		
		FileFilter filter = new FileFilter()
		{
			public boolean accept(File pathname)
			{
				return (filesOnly != pathname.isDirectory());
			}
		};

		return file.listFiles(filter);
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#getDriveFreeSpace(java.lang.String)
	 */
	public long getDriveFreeSpace(String drive) throws FileManagementException
	{
		long free = 0L;
		
		try
		{
			free = FileSystemUtils.freeSpaceKb(drive) * 1024L;
		}
		catch (IOException e)
		{
			throw new FileManagementException("Failed to get free space on drive " + drive, e);
		}
		
		return free;
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#getDriveTotalSpace(java.lang.String)
	 */
	public long getDriveTotalSpace(String drive) throws FileManagementException
	{
		/* This will eventually be new File(drive).getTotalSpace() in JDK 1.6 */
		throw new UnsupportedOperationException();
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#ftpDownload
	 */
	public void ftpDownload(String sourcePath, String destinationPath, String host, String username, String password) throws FileManagementException
	{
		if (sourcePath.trim().endsWith("/"))
			throw new FileManagementException("Source path should include a file name and not end with a slash (/)");
		
		File destFile = new File(destinationPath);

		if (destFile.isDirectory()) {
			//getting source file name
			int lastSlashIndex = sourcePath.lastIndexOf("/");
			String sourceFileName = sourcePath.substring(lastSlashIndex+1);
			
			//preparing destination path (with file name)
			destinationPath = new File(destinationPath, sourceFileName).getAbsolutePath();
		}
		
		logger.debug("sourcePath= "+sourcePath+", destinationPath= "+destinationPath+", host="+host+
				", username= "+username+", password= "+password);
		
		//ftp download
		FTPClientWrapper ftp = new FTPClientWrapper();
		try {
			if (ftp.connLogin(host, username, password)) {
				try {
					boolean successfulDownload = ftp.downloadFile(sourcePath, destinationPath);
					if (successfulDownload)
						logger.debug("downloading "+sourcePath+ " completed");
					else {
						throw new FileManagementException("Unable to complete the download operation");
					}
				} finally {
					ftp.logout();
					ftp.disconnect();
				}
			} else {
				throw new FileManagementException("Unable to connect to " + host);
			}
		} catch (IOException e) {
			throw new FileManagementException("Exception while ftp downloading: ", e);		
		}
	}

	/* (non-Javadoc)
	 * @see com.tandbergtv.workflow.webservice.filesubsystem.FileManagementService#ftpUpload
	 */
	public void ftpUpload(String sourcePath, String destinationPath, String host, String username, String password) throws FileManagementException
	{		
		File srcFile = new File(sourcePath);
		
		if (!srcFile.exists())
			throw new FileManagementException("File: " + srcFile + " not found.");

		if (srcFile.isDirectory())
			throw new FileManagementException("File: " + srcFile
					+ " is a directory which cannot be uploaded. Operation not allowed.");
		
		if (destinationPath.endsWith("/"))
			destinationPath = destinationPath + srcFile.getName();		

		logger.debug("sourcePath= "+sourcePath+", destinationPath= "+destinationPath+", host="+host+
				", username= "+username+", password= "+password);
		
		FTPClientWrapper ftp = new FTPClientWrapper();
		try {
			if (ftp.connLogin(host, username, password)) {
				try {
					boolean successfulUpload = ftp.uploadFile(sourcePath, destinationPath); 
					if (successfulUpload)
						logger.debug("uploading "+sourcePath+ " completed");
					else {						
						throw new FileManagementException("Unable to complete the upload operation");
					}
				} finally {
					ftp.logout();
					ftp.disconnect();
				}
			} else {
				throw new FileManagementException("Unable to connect to the host:" + host + ".");
			}
		} catch (IOException e) {
			throw new FileManagementException("Exception while ftp uploading: ", e);		
		} 
	}
	

	public void archiveFiles(String sourceFiles, String destinationPath)
			throws FileManagementException {

		if (destinationPath == null || destinationPath.trim().length() == 0)
			throw new FileManagementException("Target File Name: "
					+ destinationPath + " is invalid.");

		File destination = new File(destinationPath);
		if (destination.exists()) {
			removeFile(destinationPath);			
		}

		if ((new File(destinationPath)).getParent().equalsIgnoreCase(
				sourceFiles))
			throw new FileManagementException("Target File Name: "
					+ destinationPath + " cannot be in the same folder "
					+ "where the files that need to be archived are located.");

		de.schlichtherle.io.File archiveFile = null;
		try {

			archiveFile = ArchiveUtility.openArchive(destinationPath);
			archiveFile.mkdir();

			// check if the sourceFiles is a comma-separated list
			if (sourceFiles.indexOf(",") != -1) {
				StringTokenizer st = new StringTokenizer(sourceFiles, ",");
				while (st.hasMoreTokens()) {
					String filePath = st.nextToken().trim();
					de.schlichtherle.io.File f = new de.schlichtherle.io.File(
							filePath);
					if (!f.exists()) {
						throw new FileManagementException("File: " + filePath
								+ " not found.");
					}

					if (f.isDirectory()) {
						throw new FileManagementException(filePath
								+ " is not a file.");
					}

					logger.debug("Adding file " + f.getName() + " to archive "
							+ destinationPath);
					de.schlichtherle.io.File targetFile = new de.schlichtherle.io.File(
							archiveFile, f.getName());
					boolean fileAddedToArchive = f.copyTo(targetFile);
					if (!fileAddedToArchive) {
						throw new FileManagementException(
								"Unable to add file: " + filePath
										+ " to the archive.");
					}

					logger.debug(" added to archive, result= "
							+ fileAddedToArchive);
				}
				logger.info("Files from the list: " + sourceFiles
						+ " successfully archived to: " + destinationPath);

			} else {// sourceFiles is a folder path or a single file

				File source = new File(sourceFiles);

				if (!source.exists())
					throw new FileManagementException("File or Folder: "
							+ source + " not found.");

				if (!source.isDirectory()) {
					de.schlichtherle.io.File f = new de.schlichtherle.io.File(
							sourceFiles);
					de.schlichtherle.io.File targetFile = new de.schlichtherle.io.File(
							archiveFile, f.getName());
					boolean fileAddedToArchive = f.copyTo(targetFile);
					if (!fileAddedToArchive) {
						throw new FileManagementException(
								"Unable to archive file: "
										+ source.getAbsolutePath());
					}

					logger.info("File " + source.getAbsolutePath()
							+ " successfully archived");
				} else {//folder

					boolean success = new de.schlichtherle.io.File(sourceFiles)
							.copyAllTo(new de.schlichtherle.io.File(archiveFile));
					if (!success) {
						throw new FileManagementException(
								"Unable to archive file(s) from: "
										+ source.getAbsolutePath());
					}
					logger.info("File(s) from folder: " + source.getAbsolutePath()
							+ " successfully archived to: " + destinationPath);
				}

			}

		} catch (Exception e) {
			throw new FileManagementException(
					"Exception occurred while archiving file(s) from: "
							+ sourceFiles + "; " + e.getMessage(), e);
		} finally {
			ArchiveUtility.closeArchive(archiveFile);
		}

	}
	
	@Override
	public void extractFile(String filePath, String destinationDirectory)
			throws FileManagementException {

		File srcFile = new File(filePath);

		if (!srcFile.exists())
			throw new FileManagementException("File: " + srcFile
					+ " not found.");

		if (srcFile.isDirectory())
			throw new FileManagementException("File: " + srcFile
				+ " is a directory which cannot be extracted. Operation not allowed.");

		File destDirectory = new File(destinationDirectory);

		if (!destDirectory.exists())
			destDirectory.mkdirs();

		if (!destDirectory.isDirectory())
			throw new FileManagementException("Destination: " + destinationDirectory
					+ " is not a directory. Operation not allowed.");

		if (srcFile.getName().toLowerCase().endsWith(TAR_EXTENSION)) {
			untarFile(srcFile, destinationDirectory);
		} else if (srcFile.getName().toLowerCase().endsWith(ZIP_EXTENSION)) {
			unzipFile(srcFile, destinationDirectory);
		} else
			throw new FileManagementException("Cannot extract file: "
					+ srcFile.getAbsolutePath()
					+ ". Expecting file with extension " + TAR_EXTENSION
					+ " or " + ZIP_EXTENSION);
	}

	/**
	 * @param srcFile
	 * @param destinationDirectory
	 * @throws FileManagementException
	 */
	private void unzipFile(File srcFile, String destinationDirectory)
			throws FileManagementException {
		Enumeration entries;
		ZipFile zipFile = null;

		try {
			zipFile = new ZipFile(srcFile.getAbsolutePath());
			entries = zipFile.entries();

			while (entries.hasMoreElements()) {
				ZipEntry entry = (ZipEntry) entries.nextElement();
				File out = new File(destinationDirectory, entry.getName());
				if (entry.isDirectory()) {
					out.mkdirs();
					continue;
				}
				copyInputStream(zipFile.getInputStream(entry),
						new BufferedOutputStream(new FileOutputStream(out
								.getAbsolutePath())));
			}

		} catch (Exception ex) {
			throw new FileManagementException(
					"Exception occurred while extracting file: "
							+ srcFile.getAbsolutePath() + "; "
							+ ex.getMessage(), ex);
		} finally {
			if (zipFile != null) {
				try {
					zipFile.close();
				} catch (Exception ex) {
					logger.warn("Failed to close input stream for file: "
							+ srcFile.getAbsolutePath(), ex);
				}
			}
		}
	}

	private static final void copyInputStream(InputStream in, OutputStream out)
			throws IOException {

		if (in == null || out == null)
			return;
		byte[] buffer = new byte[1024];
		int len;

		while ((len = in.read(buffer)) >= 0)
			out.write(buffer, 0, len);
		try {
			in.close();
		} catch (Exception ex) {
			logger.warn("Failed to close input stream.", ex);
		}
		try {
			out.close();
		} catch (Exception ex) {
			logger.warn("Failed to close output stream.", ex);
		}
	}

	/**
	 * @param srcFile
	 * @param destinationDirectory
	 * @throws FileManagementException
	 */
	private void untarFile(File srcFile, String destinationDirectory)
			throws FileManagementException {
		TarInputStream in = null;
		TarEntry te = null;
		try {
			in = new TarInputStream(new FileInputStream(srcFile));
			in.setFastScan(true);
			in.setDebug(false);
			while ((te = in.getNextEntry()) != null) {
				File out = new File(destinationDirectory, te.getName());
				if (te.isDirectory()) {
					out.mkdirs();
					continue;
				}
				FileOutputStream fos = null;
				try {
					fos = new FileOutputStream(out);
					in.copyEntryContents(fos);
					fos.flush();
				} finally {
					if (fos != null) {
						try {
							fos.close();
						} catch (Exception ex) {
							logger.warn(
									"Failed to close output stream for file: "
											+ out.getAbsolutePath(), ex);
						}
					}
				}
			}
		} catch (Exception ex) {
			throw new FileManagementException(
					"Exception occurred while extracting file: "
							+ srcFile.getAbsolutePath() + "; "
							+ ex.getMessage(), ex);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (Exception ex) {
					logger.warn("Failed to close input stream for file: "
							+ srcFile.getAbsolutePath(), ex);
				}
			}
		}
	}


	private byte[] createChecksum(String filePath) throws FileManagementException{
		InputStream fis = null;
		try {
			fis = new FileInputStream(filePath);
	
			byte[] buffer = new byte[1024];
			MessageDigest complete = MessageDigest.getInstance(CHECKSUM_ALGORITHM);
			int numRead;
			do {
				numRead = fis.read(buffer);
				if (numRead > 0) {
					complete.update(buffer, 0, numRead);
				}
			} while (numRead != -1);
			
			return complete.digest();
		} catch (FileNotFoundException e) {
			String msg = "Failed to open file: " + filePath + " for calculating the checksum.";
			throw new FileManagementException(msg, e);			
		} catch (NoSuchAlgorithmException e) {
			String msg = "Cannot use the algorithm: " + CHECKSUM_ALGORITHM;
			throw new FileManagementException(msg, e);
		} catch (IOException e) {
			String msg = "Failed to read file: " + filePath ;
			throw new FileManagementException(msg, e);
		} finally {
			if (fis != null) {
				try	{
					fis.close();
				}
				catch (IOException ioe)	{
					logger.warn("Failed to close input stream for file: " + filePath
							+ " after reading the file contents.", ioe);
				}
			}
		}
	}
	
	public String computeMD5Checksum(String filePath) throws FileManagementException {
		byte[] b = createChecksum(filePath);
		String result = "";
		
		// Convert a byte array to a Hex string
		for (int i = 0; i < b.length; i++) {
			result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
		}
		return result;		
	}




	
	// ========================================================================
	// ====================== INTERNAL HELPER CLASS FOR STATISICS
	// ========================================================================

	/*
	 * Internal class used to collect usage statistics when calculating the Drive Usage.
	 */
	private class UsageStatistics
	{
		int fileCount = 0;

		int directoryCount = 0;

		Date startDate = new Date();

		Date endDate = null;

		/**
		 * @see java.lang.Object#toString()
		 */
		@Override
		public String toString()
		{
			String display = LINE_SEPERATOR + "\tQuery Start: " + startDate.toString();
			display += LINE_SEPERATOR + "\tQuery End: " + endDate.toString();
			display += LINE_SEPERATOR + "\tDirectory Count: " + directoryCount;
			display += LINE_SEPERATOR + "\tFile Count: " + fileCount;

			return display;
		}
	}


}
