/*
 * Copyright (c) 2005 Aduna.
 * All rights reserved.
 * 
 * Licensed under the Open Software License version 3.0.
 */
package org.semanticdesktop.aperture.util;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.StringTokenizer;

/**
 * Utility methods for operations on Files.
 */
public class FileUtil {

    /**
     * Characters that may not occur inside file names on most platforms.
     */
    private static final String ILLEGAL_FILE_NAME_CHARS = "\\/:*?\"<>|";

    /**
     * Gets the relative representations of a file compared to another.
     * 
     * @param subj The File to find the relative form for.
     * @param relativeTo The File 'subj' should be made relative to.
     * @return The relative representation of subj.
     */
    public static String getRelativePath(File subj, File relativeTo) {
        // Get the absolute path to both files.
        String subjPath = subj.getAbsolutePath();
        String relativeToPath = relativeTo.getAbsolutePath();

        // Remove the filenames
        if (!subj.isDirectory()) {
            int idx = subjPath.lastIndexOf(File.separator);
            if (idx != -1) {
                subjPath = subjPath.substring(0, idx);
            }
        }
        if (!relativeTo.isDirectory()) {
            int idx = relativeToPath.lastIndexOf(File.separator);
            if (idx != -1) {
                relativeToPath = relativeToPath.substring(0, idx);
            }
        }

        // Check all common directories, starting with the root.
        StringTokenizer subjPathTok = new StringTokenizer(subjPath, File.separator);
        StringTokenizer relativeToPathTok = new StringTokenizer(relativeToPath, File.separator);

        String subjTok = null;
        String relativeToTok = null;
        while (subjPathTok.hasMoreTokens() && relativeToPathTok.hasMoreTokens()) {
            subjTok = subjPathTok.nextToken();
            relativeToTok = relativeToPathTok.nextToken();
            if (!subjTok.equals(relativeToTok)) {
                break;
            }
        }

        // If there are any tokens left, than these should be made relative.
        // The number of tokens left in 'relativeToTok' is the number of '..'.
        StringBuilder relPath = new StringBuilder();

        if (!subjTok.equals(relativeToTok)) {
            // That's one dir
            relPath.append("..");
            relPath.append(File.separator);
        }
        while (relativeToPathTok.hasMoreTokens()) {
            relativeToPathTok.nextToken();
            relPath.append("..");
            relPath.append(File.separator);
        }

        // Now add the path to 'subj'
        if (!subjTok.equals(relativeToTok)) {
            relPath.append(subjTok);
            relPath.append(File.separator);
        }
        while (subjPathTok.hasMoreTokens()) {
            subjTok = subjPathTok.nextToken();
            relPath.append(subjTok);
            relPath.append(File.separator);
        }

        // Last but not least, add the filename of 'subj'
        relPath.append(subj.getName());

        return relPath.toString();
    }

    /**
     * Gets the relative representations of a file compared to another.
     * 
     * @param subj The File to find the relative form for.
     * @param relativeTo The File 'subj' should be made relative to.
     * @return The relative representation of subj.
     */
    public static File getRelativeFile(File subj, File relativeTo) {
        return new File(getRelativePath(subj, relativeTo));
    }

    /**
     * Gets the extension of the specified file name.
     * 
     * @param fileName The file name to process.
     * @return The file name extension (e.g. "exe" or "txt"), or null if the file name does not have a
     *         (valid) extension.
     */
    public static String getFileExtension(String fileName) {
        int lastDotIdx = fileName.lastIndexOf('.');

        if (lastDotIdx > 0 && lastDotIdx < fileName.length() - 1) {
            String extension = fileName.substring(lastDotIdx + 1).trim();

            if (isLegalFileName(extension)) {
                return extension;
            }
        }

        return null;
    }

    /**
     * Checks whether the specified file name is a legal (DOS/Windows-) file name.
     */
    public static boolean isLegalFileName(String fileName) {
        for (int i = 0; i < fileName.length(); i++) {
            char c = fileName.charAt(i);
            if (!isLegalFileNameChar(c)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Checks whether the specified character is a legal (DOS/Windows-) file name character.
     */
    public static boolean isLegalFileNameChar(char c) {
        return ILLEGAL_FILE_NAME_CHARS.indexOf(c) == -1;
    }

    /**
     * Copies the contents of file source to file destination.
     */
    public static void copyFile(File source, File destination) throws IOException {
        FileInputStream in = null;
        try {
            in = new FileInputStream(source);
            IOUtil.writeStream(in, destination);
        }
        finally {
            if (in != null)
                in.close();
        }
    }

    /**
     * Creates a directory if it doesn't exist yet.
     * 
     * @param dir The directory to create.
     * @exception IOException If the creation of the directory failed.
     */
    public static void createDirIfNotExists(File dir) throws IOException {
        if (!dir.exists() && !dir.mkdirs()) {
            throw new IOException("Unable to create directory: " + dir.toString());
        }
    }

    /**
     * Deletes the given file and everything under it.
     * 
     * @return Whether all files were deleted succesfully.
     */
    public static boolean deltree(File directory) {
        if (directory == null || !directory.exists()) {
            return true;
        }

        boolean result = true;
        if (directory.isFile()) {
            result = directory.delete();
        }
        else {
            File[] list = directory.listFiles();
            for (int i = list.length; i-- > 0;) {
                if (!deltree(list[i])) {
                    result = false;
                }
            }
            if (!directory.delete()) {
                result = false;
            }
        }
        return result;
    }

    /**
     * Deletes all files and directories in the specified directory. Nothing happens when the specified
     * File is not a directory.
     * 
     * @return true when all children were successfully deleted, when there were no children or when the
     *         file was not a directory.
     */
    public static boolean deleteChildren(File directory) {
        boolean result = true;

        if (directory.isDirectory()) {
            File[] list = directory.listFiles();
            for (int i = list.length; i-- > 0;) {
                result = result && deltree(list[i]);
            }
        }

        return result;
    }

    /**
     * Moves the given file and all files under it (if it's a directory) to the given location, excluding
     * the given collection of File objects!
     * 
     * @param from File or directory to be moved
     * @param to The file or directory to rename to
     * @param excludes The File objects to be excluded; if a directory is excluded, all files under it
     *            are excluded as well!
     * @return Whether moving was succesfull
     */
    public static boolean moveRecursive(File from, File to, Collection excludes) {
        if (from == null || !from.exists()) {
            return false;
        }

        boolean result = true;
        if (from.isFile()) {
            if (excludes == null || !excludes.contains(from)) {
                to.getParentFile().mkdirs();
                result = from.renameTo(to);
            }
        }
        else {
            boolean excludedFileFound = false;

            File[] list = from.listFiles();
            for (int i = list.length; i-- > 0;) {
                File listItem = list[i];
                if (excludes != null && excludes.contains(listItem)) {
                    excludedFileFound = true;
                }
                else {
                    if (!moveRecursive(listItem, new File(to, listItem.getName()), excludes)) {
                        result = false;
                    }
                }
            }

            // finally, move directory itself...
            if (!excludedFileFound) {
                if (!from.delete()) {
                    result = false;
                }
            }
        }
        return result;
    }
    
    /** Read a whole file as UTF-8
     * @param filename
     * @return String
     * @throws IOException
     * Stolen from Jena 
     **/
	public static String readWholeFileAsUTF8(String filename) throws IOException {
        return readWholeFileAsEncoding(filename,"utf-8") ;
    }
	public static String readStreamAsUTF8(InputStream in) throws IOException { 
		return readStreamAsEncoding(in,"utf-8");
	}
	public static String readStreamAsEncoding(InputStream in, String encoding) throws IOException { 
		Reader r = new BufferedReader(asEncoding(in,encoding),1024) ;
		StringWriter sw = new StringWriter(1024);
		char buff[] = new char[1024];
		while (r.ready()) {
			int l = r.read(buff);
			if (l <= 0)
				break;
			sw.write(buff, 0, l);
		}
		r.close();
		sw.close();
		return sw.toString();  
	}
	
	public static String readWholeFileAsEncoding(String filename, String encoding) throws IOException { 
		InputStream in = new FileInputStream(filename) ;
		return readStreamAsEncoding(in,encoding);
	}
	
	 /**
	 * @param in
	 * @param encoding
	 * @return
	 */
	private static Reader asEncoding(InputStream in, String encoding) {
		Charset charset=Charset.forName(encoding);
		
		return new InputStreamReader(in, charset.newDecoder());
	}
}
