/**
 * THINGS/THINGER 2009
 * Copyright Erich P Gatejen (c) 2001 through 2009  ALL RIGHTS RESERVED
 * Licensed 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 WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package things.common.tools;

import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;

import org.apache.commons.io.CopyUtils;

import things.common.ThingsConstants;
import things.common.ThingsException;
import things.common.ThingsNamespace;
import things.thinger.SystemException;
import things.thinger.SystemNamespace;

/**
 * File manipulation utilities.
 * <p>
 * @author Erich P. Gatejen
 * @version 1.0
 * <p>
 * <i>Version History</i>
 * <pre>EPG - Initial - 24 NOV 04
 * </pre> 
 */
@SuppressWarnings("deprecation")
public class FileTools {

	/**
	 * Max number of delete retries.
	 */
	private final static int FILE_DELETE_RETRIES = 10;
	
	/**
	 * Copy a file.
	 * @param source
	 * @param destination
	 * @throws Throwable
	 */
	public static void copy(File source, File destination) throws Throwable {
		
		InputStream ins = null;
		OutputStream outs = null;
		try {
			
			ins = new FileInputStream(source);
			outs = new FileOutputStream(destination);
			CopyUtils.copy(ins, outs);
			
		} catch (Throwable t) {
			throw t;
		} finally {
			try {
				ins.close();
			} catch (Throwable tt) {}
			try {
				outs.close();
			} catch (Throwable tt) {}		
		}
	}
	
	/**
	 * Copy a stream.  I'll implement this myself some day.
	 * @param source
	 * @param destination
	 * @throws Throwable
	 */
	public static void copy(InputStream source, OutputStream destination) throws Throwable {
		try {
			CopyUtils.copy(source, destination);	
		} catch (Throwable t) {
			throw t;
		} 
	}
	
	/**
	 * Delete a file.
	 * @param theFile the file.  Null will be ignored.
	 * @throws Throwable
	 */
	public static void delete(File theFile) throws Throwable  {
		if ((theFile==null)||(!theFile.exists())) return;// Dismiss null or not there.
	    try {
		    int tries = 5;
	        while( theFile.exists() && !theFile.delete() ) {
	              System.gc();
                  try
                  {
                    Thread.sleep(10);
                  } catch(InterruptedException ex) {}
                  tries--;
                  if (tries == 0) throw new Exception("Exceeded delete try.  File doesn't want to die.");
	        }
	    } catch (Exception te) {
	        throw new ThingsException("Delete failed.",ThingsException.FILESYSTEM_ERROR_DELETE_FAILED, te, SystemNamespace.ATTR_PLATFORM_FILE_PATH, theFile.getAbsolutePath());
	    }
	}
	
	/**
	 * Destroy a file or directory and all it's contents (including sub directories).
	 * @param theFile the file to destroy (it can be a real file or a directory).
	 * @throws Throwable
	 */
	public static void destroy(File theFile) throws Throwable  {
		destroy(theFile, true);
	}
	
	/**
	 * Destroy a file or directory and all it's contents (including sub directories).
	 * @param theFile the file to destroy (it can be a real file or a directory).
	 * @param recurse if true, recurse into directories
	 * @throws Throwable
	 */
	private static void destroy(File theFile, boolean recurse) throws Throwable  {
		
		// If it is a file, kill it.
		if (theFile.isFile()) {
			int tries = FILE_DELETE_RETRIES;
	        while( !theFile.delete() ) {
	            System.gc();			// Give it a breather
	            tries--;
	            if (tries == 0) {
	            	if (theFile.exists()) throw new SystemException("Exceeded delete try.  File doesn't want to die.", SystemException.FILESYSTEM_ERROR_FILE_WONT_DELETE, SystemNamespace.ATTR_PLATFORM_FS_PATH_ABSOLUTE, theFile.getAbsolutePath());
	            }
	        }
			
		} else {
			
			// Must be a directory
			File[] subpaths = theFile.listFiles();
			if (subpaths == null) SystemException.softwareProblem("FSFileSystem.internalDelete got a File that isn't isFile() or isDirectory().  wtf?", new Exception(), SystemNamespace.ATTR_PLATFORM_FS_PATH_ABSOLUTE, theFile.getAbsolutePath());

			// Whack any subs.  We'll hazard some recursion for this.
			if (subpaths.length > 0) {
				
				// make the recursion decision here to save time
				if (recurse) {
					for (int index = 0; index < subpaths.length; index++) {
						destroy(subpaths[index],true);
					}
				} else {
					
					// Files only
					for (int index = 0; index < subpaths.length; index++) {
						if (subpaths[index].isFile()) {
							destroy(subpaths[index],false);
						}
					}				
				}
				
			} // end IF SUBDIRECTORIES
			
			// Get rid of the directory (the error means it wasn't empty)
			// Try once 
			if (!theFile.delete()) {
				
				// Is it really there?
				if (theFile.exists()) {
					
					// Is it empty?  Check above first as a fast fail, before we hit the filesystem again.
					if ((subpaths.length > 0)&&(theFile.listFiles().length <= 0)) {
						
						// Ok, go to work on it, because it is supposed to be gone
						int tries = FILE_DELETE_RETRIES;
				        while( !theFile.delete() ) {
				            System.gc();			// Give it a breather
				            tries--;
				            if (tries == 0) throw new SystemException("Exceeded delete try.  File doesn't want to die.",SystemException.FILESYSTEM_ERROR_FILE_WONT_DELETE, SystemNamespace.ATTR_PLATFORM_FS_PATH, theFile.getAbsolutePath());
				        }
				        
					} else {
						if (recurse) throw new SystemException("Directory will not delete because it is not empty (though it should have been emptied already).", SystemException.FILESYSTEM_ERROR_FILE_WONT_DELETE, SystemNamespace.ATTR_PLATFORM_FS_PATH, theFile.getAbsolutePath());
					}
					
				} 
				
			} // end kill DIRECTORY
			
		} // end if FILE
	}

	
	/**
	 * Make a file.  If the file already exists, delete it.  
	 * It will let any exception escape.
	 * @param name Path to the file
	 * @return A file object
	 * @throws Throwable
	 */
	public static File makeFile(String name) throws Throwable {

		File directory;
		String directoryPath;
		int fileSegment;

		// construct the path
		File target = new File(name);

		if (target.exists()) {
			int tries = ThingsConstants.FS_FILE_DELETE_RETRIES;
			boolean status = target.delete();
			while(status != true) {
				tries--;
				if (tries == 0) throw new ThingsException("Could not delete existing file where call wanted to makeFile().", ThingsException.FILESYSTEM_ERROR_COULD_NOT_REPLACE, ThingsNamespace.ATTR_PLATFORM_FILE_PATH);
				System.gc();
				Thread.yield();
				status = target.delete();
			}
		} else {
		    fileSegment = name.lastIndexOf('/');
			if (fileSegment > 1) {
			    directoryPath = name.substring(0, fileSegment);
			    directory = new File(directoryPath);
				if (!directory.exists()) {
				    directory.mkdirs();
				}
			}
		}
		return target;
	}

	/**
	 * Make a file.  If the file already exists, it will be available for write.
	 * It will let any exception escape.
	 * @param name Path to the file
	 * @return A file object
	 * @throws Exception
	 */
	public static File appendFile(String name) throws Exception {

		File directory;
		String directoryPath;
		int fileSegment;

		// construct the path
		File target = new File(name);

		if (!target.exists()) {

		    fileSegment = name.lastIndexOf('/');
			if (fileSegment > 1) {
			    directoryPath = name.substring(0, fileSegment);
			    directory = new File(directoryPath);
				if (!directory.exists()) {
				    directory.mkdirs();
				}
			}
		}
		return target;
	}

	/**
	 * This will detect if the path has any relative pointers (such as ..).  
	 * @param path The path
	 * @return true if it does, otherwise false.
	 */
	public static boolean detectRelativePath(String path) {
		if (path!=null) {
			try {
				// detect no root
				String trimmed = path.trim();
				if ( ((trimmed.charAt(1)!=':')&&(trimmed.charAt(0)!='/')&&(trimmed.charAt(0)!='\\'))||
					 (trimmed.charAt(0)=='.')) return true;
				
				// find dots
				StringReader rin = new StringReader(path);
				State rpState = State.SEEK;
				int currentChar = rin.read();
				while (currentChar >= 0) {
					switch (rpState) {
						case SEEK:
							if (currentChar=='\\' || currentChar=='/') rpState = State.START;
							break;
						case START:
							if (currentChar=='.') rpState = State.DOT;
							else if (currentChar!='\\' && currentChar!='/') rpState = State.SEEK;
							break;
						case DOT:
							if (currentChar=='\\' || currentChar=='/') return true;
							if (currentChar!='.') rpState = State.START;
							break;
					}
					currentChar = rin.read();
				}
			} catch (Exception ee) {
				// Don't care.  Should only be a spurious EOF
			}
		}
		return false;
	}
	private enum State { SEEK, START, DOT }
	
	
	
	/**
	 * Read a file into a String.  This should be used only is absolutely needed.
	 * @param path File to load
	 * @return String if successful or otherwise null
	 * @throws Certain IOException, like if the file cannot be found or opened.
	 */
	public static String loadFile2String(File   path) throws IOException {
		String result = null;
		if (path == null) throw new IOException("Path is null in loadFile2String(path).");
		
		try {
			// Setup
			StringBuffer	buffer = new StringBuffer();
			char[] buf = new char[1024];
			int sbuf;
			BufferedReader bis = new BufferedReader(new FileReader(path));
	
			// Run it
			sbuf = bis.read(buf, 0, 1024);
			while (sbuf > 0) {
				buffer.append(buf, 0, sbuf);
				sbuf = bis.read(buf, 0, 1024);
			}
			
			result =  buffer.toString();
			
		} catch (IOException ioe) {
			throw ioe;
		} catch (Throwable t) {
			//throw new IOException("Failed to loadFile2String.", t);
			throw new IOException("Failed to loadFile2String." + t.getMessage());
		}
		
		return result;
	}
	
	/**
	 * Read a stream into a buffer, as much as can fit in the buffer, .  This should be used only is absolutely needed.
	 * @param stream the input stream
	 * @param buffer the buffer;
	 * @return the number of bytes read intot he buffer.
	 * @throws Certain IOException, like if the file cannot be found or opened.
	 */
	public static int loadStream2Buffer(InputStream   stream, byte[] buffer) throws IOException {
		int bufferCursor = 0;
		try {
			int actualRead = stream.read(buffer, bufferCursor, buffer.length-bufferCursor);
			bufferCursor = actualRead;
			while ((actualRead > 0)&&(bufferCursor < buffer.length)) {
				actualRead = stream.read(buffer, bufferCursor, buffer.length-bufferCursor);
				bufferCursor+=actualRead;
			}
			
		} catch (IOException ioe) {
			throw ioe;
		} catch (Throwable t) {
			//throw new IOException("Failed to loadFile2String.", t);
			throw new IOException("Failed to loadStream2Buffer. " + t.getMessage());
		}
		return bufferCursor;	
	}
	
	/**
	 * Save a string to a file.  This should be used only is absolutely needed.
	 * @param path File to save
	 * @param theString the string.
	 * @throws Certain IOException, like if the file cannot be found or opened.
	 */
	public static void saveString2File(File   path, String theString) throws IOException {
		if (path == null) throw new IOException("Path is null in saveString2File(path).");
		if (theString == null) throw new IOException("theString is null in saveString2File(path).");
		OutputStream os = null;
		
		try {
			os = new BufferedOutputStream(new FileOutputStream(path));
			os.write(theString.getBytes());
			
		} catch (IOException ioe) {
			throw ioe;
		} catch (Throwable t) {
			throw new IOException("Failed to saveString2File." + t.getMessage(), t);
		} finally {
			try {
				os.close();
			} catch (Throwable tt) {
				// Oh well.
			}
		}
	}
}