package org.limewire.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CoderResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/**
 * Provides convenience functionality ranging from getting user information,
 * copying files to getting the stack traces of all current threads.
 * <DL>
 * <DT>User Information
 * <DD>Get a username, a user home directory, etc.
 * 
 * <DT>File Operation
 * <DD>Copy resource files, get the current directory, set, get and validate the 
 * directory to store user settings. Also, you can use convertFileName to replace 
 * operating system specific illegal characters.
 * 
 * <DT>Threads
 * <DD>Get the stack traces of all current threads.
 * 
 * <DT>Time
 * <DD>Convert an integer value representing the seconds into an appropriate days, 
 * hour, minutes and seconds format (d:hh:mm:ss). 
 * 
 * <DT>Decode
 * <DD>Decode a URL encoded from a string.
 * 
 * <DT>Resources
 * <DD>Retrieve a resource file and a stream.
 * </DL>
 */
public class CommonUtils {
    
    /**
     * Several arrays of illegal characters on various operating systems.
     * Used by convertFileName
     */
    private static final char[] ILLEGAL_CHARS_ANY_OS = {
        '/', '\n', '\r', '\t', '\0', '\f' 
    };
    private static final char[] ILLEGAL_CHARS_UNIX = {'`'};
    private static final char[] ILLEGAL_CHARS_WINDOWS = { 
        '?', '*', '\\', '<', '>', '|', '\"', ':'
    };
    private static final char[] ILLEGAL_CHARS_MACOS = {':'};
    
    /** The location where settings are stored. */
    private static volatile File settingsDirectory = null;

    /**
     * Returns the user home directory.
     *
     * @return the <tt>File</tt> instance denoting the abstract pathname of
     *  the user's home directory, or <tt>null</tt> if the home directory
     *  does not exist
     */
    public static File getUserHomeDir() {
        return new File(System.getProperty("user.home"));
    }

    /**
     * Return the user's name.
     *
     * @return the <tt>String</tt> denoting the user's name.
     */
    public static String getUserName() {
        return System.getProperty("user.name");
    }

    /**
     * Gets an InputStream from a resource file.
     * 
     * @param location the location of the resource in the resource file
     * @return an <tt>InputStream</tt> for the resource
     * @throws IOException if the resource could not be located or there was
     *  another IO error accessing the resource
     */
    public static InputStream getResourceStream(String location) 
      throws IOException {
       ClassLoader cl = CommonUtils.class.getClassLoader();            
       URL resource = null;
    
        if(cl == null) {
            resource = ClassLoader.getSystemResource(location);
        } else {
            resource = cl.getResource(location);
        }
        
        if( resource == null) 
            throw new IOException("null resource: "+location);
        else
            return resource.openStream();
    }

    /**
     * Copied from URLDecoder.java
     */
    public static String decode(String s) {
        StringBuilder sb = new StringBuilder();
        for(int i=0; i<s.length(); i++) {
            char c = s.charAt(i);
            switch (c) {
                case '+':
                    sb.append(' ');
                    break;
                case '%':
                    try {
                        sb.append((char)Integer.parseInt(
                                        s.substring(i+1,i+3),16));
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException(s);
                    }
                    i += 2;
                    break;
                default:
                    sb.append(c);
                    break;
            }
        }
        // Undo conversion to external encoding
        String result = sb.toString();
        try {
            byte[] inputBytes = result.getBytes("8859_1");
            result = new String(inputBytes);
        } catch (UnsupportedEncodingException e) {
            // The system should always have 8859_1
        }
        return result;
    }

    /**
     * Copies the specified resource file into the current directory from
     * the jar file. If the file already exists, no copy is performed.
     *
     * @param fileName the name of the file to copy, relative to the jar 
     *  file -- such as "org/limewire/gui/images/image.gif"
     * @param newFile the new <tt>File</tt> instance where the resource file
     *  will be copied to -- if this argument is null, the file will be
     *  copied to the current directory
     * @param forceOverwrite specifies whether or not to overwrite the 
     *  file if it already exists
     */
    public static void copyResourceFile(String fileName, File newFile,
            boolean forceOverwrite) throws IOException {
        if (newFile == null)
    	    newFile = new File(".", fileName);
    
    	// return quickly if the file is already there, no copy necessary
    	if( !forceOverwrite && newFile.exists() )
            return;
        
    	String parentString = newFile.getParent();
        if(parentString == null)
            return;
        
    	File parentFile = new File(parentString);
    	if(!parentFile.isDirectory())
    		parentFile.mkdirs();
    
    	ClassLoader cl = CommonUtils.class.getClassLoader();			
        //load resource using my class loader or system class loader
        //Can happen if Launcher loaded by system class loader
        URL resource = cl != null
            ?  cl.getResource(fileName)
            :  ClassLoader.getSystemResource(fileName);
            
        if(resource == null)
            throw new IOException("resource: " + fileName + " doesn't exist.");
        
    	saveStream(resource.openStream(), newFile);
    }
    
    /**
     * Copies the src file to the destination file.
     * This will always overwrite the destination.
     */
    public static void copyFile(File src, File dst) throws IOException {
        saveStream(new FileInputStream(src), dst);
    }
    
    /**
     * Saves all data from the stream into the destination file.
     * This will always overwrite the file.
     */
    public static void saveStream(InputStream inStream, File newFile) throws IOException {
    	BufferedInputStream bis = null;
    	BufferedOutputStream bos = null;            
    	try {
    		//buffer the streams to improve I/O performance
    		final int bufferSize = 2048;
            bis = new BufferedInputStream(inStream, bufferSize);
            bos = new BufferedOutputStream(new FileOutputStream(newFile), bufferSize);
            byte[] buffer = new byte[bufferSize];
            int c = 0;
    		
    		do { //read and write in chunks of buffer size until EOF reached
    			c = bis.read(buffer, 0, bufferSize);
                if (c > 0)
                    bos.write(buffer, 0, c);
    		} while (c == bufferSize); //(# of bytes read)c will = bufferSize until EOF
            
            bos.flush();
    	} catch(IOException e) {
    		//if there is any error, delete any portion of file that did write
    		newFile.delete();
    	} finally {
            if(bis != null) {
                try {
                    bis.close();
                } catch(IOException ignored) {}
            }
            if(bos != null) {
                try {
                    bos.close();
                } catch(IOException ignored) {}
            }
    	} 
    }

    /**
     * Returns the stack traces of all current Threads.
     */
    public static String getAllStackTraces() {
        try {
            Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
            
            List<Map.Entry<Thread, StackTraceElement[]>> sorted =
                new ArrayList<Map.Entry<Thread, StackTraceElement[]>>(map.entrySet());
            Collections.sort(sorted, new Comparator<Map.Entry<Thread, StackTraceElement[]>>() {
                public int compare(Map.Entry<Thread, StackTraceElement[]> a, Map.Entry<Thread, StackTraceElement[]> b) {
                    return a.getKey().getName().compareTo(b.getKey().getName());
                }
            });
            
            StringBuilder buffer = new StringBuilder();
            for(Map.Entry<Thread, StackTraceElement[]> entry : sorted) {
                Thread key = entry.getKey();
                StackTraceElement[] value = entry.getValue();
                
                buffer.append(key.getName()).append("\n");
                for(int i = 0; i < value.length; i++) {
                    buffer.append("    ").append(value[i]).append("\n");
                }
                buffer.append("\n");
            }
            
            // Remove the last '\n'
            if (buffer.length() > 0) {
                buffer.setLength(buffer.length()-1);
            }
            
            return buffer.toString();
        } catch (Exception err) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            pw.println("An error occured during getting the StackTraces of all active Threads");
            err.printStackTrace(pw);
            pw.flush();
            return sw.toString();
        }
    }

    /**
     * Converts a value in seconds to:
     *     "d:hh:mm:ss" where d=days, hh=hours, mm=minutes, ss=seconds, or
     *     "h:mm:ss" where h=hours<24, mm=minutes, ss=seconds, or
     *     "m:ss" where m=minutes<60, ss=seconds
     */
    public static String seconds2time(int seconds) {
        int minutes = seconds / 60;
        seconds = seconds - minutes * 60;
        int hours = minutes / 60;
        minutes = minutes - hours * 60;
        int days = hours / 24;
        hours = hours - days * 24;
        // build the numbers into a string
        StringBuilder time = new StringBuilder();
        if (days != 0) {
            time.append(Integer.toString(days));
            time.append(":");
            if (hours < 10) time.append("0");
        }
        if (days != 0 || hours != 0) {
            time.append(Integer.toString(hours));
            time.append(":");
            if (minutes < 10) time.append("0");
        }
        time.append(Integer.toString(minutes));
        time.append(":");
        if (seconds < 10) time.append("0");
        time.append(Integer.toString(seconds));
        return time.toString();
    }

    /**
     * Returns a normalized and shortened valid file name taking the length
     * of the path of the parent directory into account.
     * 
     * The name is cleared from illegal filesystem characters and it is ensured
     * that the maximum path system on the system is not exceeded unless the 
     * parent directory path has already the maximum path length.
     *
     * @param parentDir
     * @param name
     * @throws IOException if the parent directory's path takes up 
     * {@link OSUtils#getMaxPathLength()}.
     * @return
     */
    public static String convertFileName(File parentDir, String name) throws IOException {
        int parentLength = parentDir.getAbsolutePath().getBytes(Charset.defaultCharset().name()).length;
        if (parentLength >= OSUtils.getMaxPathLength() - 1 /* for the separator char*/) {
            throw new IOException("Path too long");
        }
        return convertFileName(name, Math.min(OSUtils.getMaxPathLength() - parentLength - 1, 180));
    }
    
    /**
     * Cleans up the filename and truncates it to length of 180 bytes by calling
     * {@link #convertFileName(String, int) convertFileName(String, 180)}. 
     */
    public static String convertFileName(String name) {
        return convertFileName(name, 180);
    }
        
    /**
     * Cleans up the filename from illegal characters and truncates it to the
     * length of bytes specified.
     * @param name the filename to clean up
     * @param maxBytes the maximumm number of bytes the cleaned up file name
     * can take up
     * @return the cleaned up file name
     */
    public static String convertFileName(String name, int maxBytes) {
        // use default encoding which is also used for files judging from the
        // property name "file.encoding"
        try {
            return convertFileName(name, maxBytes, Charset.defaultCharset());
        } catch (CharacterCodingException cce) {
            try {
                // UTF-8 should always be available
                return convertFileName(name, maxBytes, Charset.forName("UTF-8"));
            } catch (CharacterCodingException e) {
                // should not happen, UTF-8 can encode unicode and gives us a
                // good length estimate
                throw new RuntimeException("UTF-8 should have encoded: " + name, e);
            }
        }
    }

    /** 
     * Replaces OS specific illegal characters from any filename with '_', 
     * including ( / \n \r \t ) on all operating systems, ( ? * \  < > | " ) 
     * on Windows, ( ` ) on unix.
     *
     * @param name the filename to check for illegal characters
     * @param maxBytes the maximum number of bytes for the resulting file name,
     * must be > 0
     * @return String containing the cleaned filename
     * 
     * @throws CharacterCodingException if the charset could not encode the
     * characters in <code>name</code> 
     * @throws IllegalArgumentException if maxBytes <= 0
     */
    public static String convertFileName(String name, int maxBytes, Charset charSet) throws CharacterCodingException {
    	
        if (maxBytes <= 0) {
            throw new IllegalArgumentException("maxBytes must be > 0");
        }
        
    	// ensure that block-characters aren't in the filename.
        name = I18NConvert.instance().compose(name);
    
    	// if the name is too long, reduce it.  We don't go all the way
    	// up to 255 because we don't know how long the directory name is
    	// We want to keep the extension, though.
    	if(name.length() > maxBytes || name.getBytes().length > maxBytes) {
    	    int extStart = name.lastIndexOf('.');
    	    if ( extStart == -1) { // no extension, weird, but possible
    	        name = getPrefixWithMaxBytes(name, maxBytes, charSet);
    	    } else {
    	        // if extension is greater than 11, we truncate it.
    	        // ( 11 = '.' + 10 extension bytes )
    	        int extLength = name.length() - extStart;		        
    	        int extEnd = extLength > 11 ? extStart + 11 : name.length();
    	        byte[] extension = getMaxBytes(name.substring(extStart, extEnd), 16, charSet);
    	        try {
    	            // disregard extension if we lose too much of the name
    	            // since the name is also used for searching
    	            if (extension.length >= maxBytes - 10) {
    	                name = getPrefixWithMaxBytes(name, maxBytes, charSet);
    	            } else {
    	                name = getPrefixWithMaxBytes(name, maxBytes - extension.length, charSet) 
    	                + new String(extension, charSet.name());
    	            }
    	        } catch (UnsupportedEncodingException uee) {
    	            throw new RuntimeException("Could not handle string", uee);
    	        }
            }          
    	}
        for (int i = 0; i < ILLEGAL_CHARS_ANY_OS.length; i++) 
            name = name.replace(ILLEGAL_CHARS_ANY_OS[i], '_');
    	
        if ( OSUtils.isWindows() || OSUtils.isOS2() ) {
            for (int i = 0; i < ILLEGAL_CHARS_WINDOWS.length; i++) 
                name = name.replace(ILLEGAL_CHARS_WINDOWS[i], '_');
        } else if ( OSUtils.isLinux() || OSUtils.isSolaris() ) {
            for (int i = 0; i < ILLEGAL_CHARS_UNIX.length; i++) 
                name = name.replace(ILLEGAL_CHARS_UNIX[i], '_');
        } else if (OSUtils.isMacOSX()) {
            for(int i = 0; i < ILLEGAL_CHARS_MACOS.length; i++)
                name = name.replace(ILLEGAL_CHARS_MACOS[i], '_');
        }
        
        return name;
    }
    
    /**
     * Sanitizes a String for use in a directory and file name 
	 * and removes any illegal characters from it
	 * 
     * @param name String to check
     * @return sanitized String
     */
    public static String santizeString(String name) {
        for (int i = 0; i < ILLEGAL_CHARS_ANY_OS.length; i++) 
            name = name.replace(ILLEGAL_CHARS_ANY_OS[i], '_');
        
        if ( OSUtils.isWindows() || OSUtils.isOS2() ) {
            for (int i = 0; i < ILLEGAL_CHARS_WINDOWS.length; i++) 
                name = name.replace(ILLEGAL_CHARS_WINDOWS[i], '_');
        } else if ( OSUtils.isLinux() || OSUtils.isSolaris() ) {
            for (int i = 0; i < ILLEGAL_CHARS_UNIX.length; i++) 
                name = name.replace(ILLEGAL_CHARS_UNIX[i], '_');
        } else if (OSUtils.isMacOSX()) {
            for(int i = 0; i < ILLEGAL_CHARS_MACOS.length; i++)
                name = name.replace(ILLEGAL_CHARS_MACOS[i], '_');
        }
        return name;
    }
    
    /**
     * Returns the prefix of <code>string</code> which takes up a maximum
     * of <code>maxBytes</code>.
     * @throws CharacterCodingException 
     */
    static String getPrefixWithMaxBytes(String string, int maxBytes, Charset charSet) throws CharacterCodingException {
        try {
            return new String(getMaxBytes(string, maxBytes, charSet), charSet.name());
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException("Could not recreate string", uee);
        }
    }

    /**
     * Returns the first <code>maxBytes</code> of <code>string</code> encoded 
     * using the encoder of <code>charSet</code>
     * @param string whose prefix bytes to return
     * @param maxBytes the maximum number of bytes to return
     * @param charSet the char set used for encoding the characters into bytes
     * @return the array of bytes of length <= maxBytes
     * @throws CharacterCodingException if the char set's encoder could not
     * handle the characters in the string
     */
    static byte[] getMaxBytes(String string, int maxBytes, Charset charSet) throws CharacterCodingException {
        byte[] bytes = new byte[maxBytes];
        ByteBuffer out = ByteBuffer.wrap(bytes);
        CharBuffer in = CharBuffer.wrap(string.toCharArray());
        CharsetEncoder encoder = charSet.newEncoder();
        CoderResult cr = encoder.encode(in, out, true);
        encoder.flush(out);
        if (cr.isError()) {
            cr.throwException();
        }
        byte[] result = new byte[out.position()];
        System.arraycopy(bytes, 0, result, 0, result.length);
        return result;
    }

    /**
     * Returns the user's current working directory as a <tt>File</tt>
     * instance, or <tt>null</tt> if the property is not set.
     *
     * @return the user's current working directory as a <tt>File</tt>
     *  instance, or <tt>null</tt> if the property is not set
     */
    public static File getCurrentDirectory() {
    	return new File(System.getProperty("user.dir"));
    }
    
    /**
     * Validates a potential settings directory.
     * This returns the validated directory, or throws an IOException
     * if it can't be validated.
     */
    public static File validateSettingsDirectory(File dir) throws IOException {
        dir = dir.getAbsoluteFile();        
        if(!dir.isDirectory()) {
            dir.delete(); // delete whatever it may have been
            if(!dir.mkdirs()) 
                throw new IOException("could not create preferences directory: " + dir);
        }

        if(!dir.canWrite())
            throw new IOException("settings dir not writable: " + dir);

        if(!dir.canRead())
            throw new IOException("settings dir not readable: " + dir);
        
        // Validate that you can write a file into settings directory.
        //  catches vista problem where if settings directory is
        //  locked canRead and canWrite still return true
        File file = File.createTempFile("test", "test", dir);
        if(!file.exists())
            throw new IOException("can't write test file in directory: " + dir);
        file.delete();
        
        return dir;
    }
    
    /**
     * Sets the new settings directory.
     * The settings directory cannot be set more than once.
     * 
     * If the directory can't be set (because it isn't a folder, can't be made into
     * a folder, or isn't readable and writable), an IOException is thrown.
     * 
     * @param settingsDir
     * @throws IOException
     */
    public static void setUserSettingsDir(File settingsDir) throws IOException {
        if(settingsDirectory != null)
            throw new IllegalStateException("settings directory already set!");
        settingsDirectory = validateSettingsDirectory(settingsDir);
    }

    /**
     * Returns the directory where all user settings should be stored.  This
     * is where all application data should be stored.  If the directory is not
     * set, this returns the user's home directory.
     */
    public synchronized static File getUserSettingsDir() {  
        if(settingsDirectory != null)
            return settingsDirectory;
        else
            return getUserHomeDir();
    }
}
