/*******************************************************************************
 * Copyright (c) 2007 Exadel, Inc. and Red Hat, Inc.
 * Distributed under license by Red Hat, Inc. All rights reserved.
 * This program is 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:
 *     Exadel, Inc. and Red Hat, Inc. - initial API and implementation
 ******************************************************************************/ 
package org.jboss.tools.common.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.jboss.tools.common.core.CommonCorePlugin;

public class FileUtils {

    public FileUtils() {}

    public static String getEncoding(IFile f) {
    	String encoding = null;
    	if(f != null && f.exists() && f.isSynchronized(0)) try {
    		encoding = f.getCharset();
    	} catch (CoreException e) {
    		CommonCorePlugin.getPluginLog().logError(e);
    	}
    	return encoding != null ? encoding : "8859_1"; //$NON-NLS-1$
    }

    public static String readFile(File f) {
        if(!f.isFile()) return ""; //$NON-NLS-1$
    	ReadBytes bs = readBytes(f);
    	if(bs == null) return ""; //$NON-NLS-1$
        String encoding = getEncoding(bs.bs);
        if(encoding == null) return new String(bs.bs, 0, bs.length);
        try {
        	return new String(bs.bs, 0, bs.length, encoding);
        } catch (UnsupportedEncodingException e) {
        	return new String(bs.bs, 0, bs.length);
        }
    }

    public static String readFileWithEncodingCheck(File f, String defaultEncoding) {
        if(!f.isFile()) return ""; //$NON-NLS-1$
    	ReadBytes bs = readBytes(f);
    	if(bs == null) return ""; //$NON-NLS-1$
        String encoding = getEncoding(bs.bs);
        if(encoding == null) encoding = validateEncoding(defaultEncoding, null);
        if(encoding == null) return new String(bs.bs, 0, bs.length);
        try {
        	return new String(bs.bs, 0, bs.length, encoding);
        } catch (UnsupportedEncodingException e) {
        	return new String(bs.bs, 0, bs.length);
        }
    }

    public static ReadBytes readBytes(File f) {
        if(!f.isFile()) return null;
        BufferedInputStream br = null;
        try {
            FileInputStream fr = new FileInputStream(f);
            br = new BufferedInputStream(fr);
            int l = (int)f.length();
            byte[] bs = new byte[l];
            l = br.read(bs, 0, l);
            br.close();
            fr.close();
            return new ReadBytes(bs, l);
        } catch (IOException e) {
        	return null;
        } finally {
        	if(br!=null) {
        		try {
					br.close();
				} catch (IOException e) {
					//ignore
				}
        	}
        }
    }
    
    static class ReadBytes {
    	byte[] bs;
    	int length;
    	
    	ReadBytes(byte[] bs, int l) {
    		this.bs = bs;
    		length = l;
    	}
    }

    public static String readFile(File f, String encoding) {
    	ReadBytes bs = readBytes(f);
    	if(bs == null) return null;
        try {
            return new String(bs.bs, 0, bs.length, encoding);
        } catch (UnsupportedEncodingException e) {
        	return null;
        }
    }

    public static boolean isTextFile(File f, int length) {
        if(!f.isFile()) return false;
        BufferedReader br = null;
        try {
            FileReader fr = new FileReader(f);
            br = new BufferedReader(fr);
            int l = (int)f.length();
            if(l > length) l = length;
            char[] cs = new char[l];
            br.read(cs, 0, l);
            br.close();
            fr.close();
            return isText(new String(cs));
        } catch (IOException e) {
            return false;
        } finally {
        	if(br!=null) {
        		try {
					br.close();
				} catch (IOException e) {
					// ignore
				}
        	}
        }
    }

    public static boolean isText(String body) {
        if(body == null) return false;
        int l = body.length();
        for (int i = 0; i < l; i++) {
            char c = body.charAt(i);
            if(((int)c) < 32 && c != '\n' && c != '\r' && c != 't') return false;
        }
        return true;
    }

    // FIXME Size of string buffer should be set to size of file by default 
    // to avoid StringBuffer extension on each append
    public static String readStream(InputStream is) {
        StringBuffer sb = new StringBuffer(""); //$NON-NLS-1$
        try {
            byte[] b = new byte[4096];
            while(true) {
                int l = is.read(b, 0, b.length);
                if(l < 0) break;
                sb.append(new String(b, 0, l));
            }
        } catch (IOException e) {
        	CommonCorePlugin.getPluginLog().logError(e);
        } finally {
            try {
				is.close();
			} catch (IOException e) {
				// Ignore
			}
        }
        return sb.toString();
    }
    
    public static String readStream(IFile file) throws CoreException {
		String content = null;
		InputStream in = null;
		try {
			String charset = file.getCharset();
			if(charset != null) {
				charset = validateEncoding(charset, null);
			}
			in = file.getContents();
			content = (charset == null) ? readStream(in) : readStream(in, charset);
		} finally {
			if(in!=null) {
				try {
					in.close();
				} catch (IOException e) {
					CommonCorePlugin.getPluginLog().logError(e);
				}
			}
		}
		return content;
    }

    public static String readStream(InputStream is, String charset) {
        StringBuffer sb = new StringBuffer(""); //$NON-NLS-1$
        try {
            byte[] b = new byte[4096];
            while(true) {
                int l = is.read(b, 0, b.length);
                if(l < 0) break;
                sb.append(new String(b, 0, l, charset));
            }
        } catch (IOException e) {
        	CommonCorePlugin.getPluginLog().logError(e);
        } finally {
            try {
				is.close();
			} catch (IOException e) {
				// Ignore
			}
        }

        return sb.toString();
    }

    public static void copyContent(IFile from, IFile to, boolean force, boolean keepHistory, IProgressMonitor monitor) throws CoreException {
		InputStream is = null;
		try {
			is = from.getContents();
			to.setContents(is, force, keepHistory, monitor);
		} finally {
			if(is!=null) {
				try {
					is.close();
				} catch (IOException e) {
					CommonCorePlugin.getPluginLog().logError(e);
				}
			}
		}
    }

    public static boolean writeFile(File f, String value) {
    	return writeFileWithEncodingCheck(f, value, null);
    }

    public static boolean writeFileWithEncodingCheck(File f, String value, String defaultEncoding) {
    	if(value == null) return false;
    	String encoding = getEncoding(value);
    	if(encoding == null) encoding = validateEncoding(defaultEncoding, null);
    	if(value.startsWith("<?xml")) { //$NON-NLS-1$
    		String s = validateEncoding(encoding, "UTF-8"); //$NON-NLS-1$
    		if(encoding == null) {
    			encoding = s;
    		} else if(s == null || !s.equals(encoding)) {
    			return false;
    		}
    	}
    	if(encoding == null) return writeFileDefault(f, value);
    	return writeFile(f, value, encoding);
    }

    public static boolean writeFileDefault(File f, String value) {
        try {
            try {
                if(f.isFile() && !isSameFile(f)) f.delete();
                if(!f.exists()) f.createNewFile();
            } catch (IOException e) {
            	CommonCorePlugin.getPluginLog().logError("Problem writing to file " + f, e); //$NON-NLS-1$
            } catch (SecurityException e) {
            	CommonCorePlugin.getPluginLog().logError("Problem writing to file " + f, e); //$NON-NLS-1$
            }
            PrintWriter pw = new PrintWriter(new FileWriter(f));
            pw.print(value);
            pw.flush();
            pw.close();
            return true;
        } catch (IOException e) {
            return false;
        }
    }


    public static boolean copyFile(File source, File dest, boolean mkdirs) {
        return copyFile(source, dest, mkdirs, true);
    }

    public static boolean copyFile(File source, File dest) {
        return copyFile(source, dest, false, true);
    }

    public static boolean copyFile(File source, File dest, boolean mkdirs, boolean overwrite) {
        if (mkdirs) dest.getParentFile().mkdirs();
        if(!source.isFile()) return false;
        if(dest.isFile() && !isSameFile(dest)) dest.delete();
        if(dest.isFile() && !overwrite) return false;
        if(!dest.exists())
			try {
				dest.createNewFile();
			} catch (IOException e1) {
				CommonCorePlugin.getPluginLog().logError(e1);
			}
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new BufferedInputStream(new FileInputStream(source), 16 * 1024);
            os = new BufferedOutputStream(new FileOutputStream(dest), 16 * 1024);
            copyStream(is, os);
            return true;
        } catch (IOException e) {
        	CommonCorePlugin.getPluginLog().logError(e);
            return false;
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
            	CommonCorePlugin.getPluginLog().logError(e);
            }
            try {
                if (os != null) os.close();
            } catch (IOException e) {
            	CommonCorePlugin.getPluginLog().logError(e);
            }
        }
    }

    public static boolean updateFile(File source, File dest, boolean mkdirs) {
        if(!source.isFile()) return false;
        if(dest.isFile() && (dest.lastModified()<source.lastModified())) {
            dest.delete();
        }
        return copyFile(source, dest, mkdirs);
    }

    public static void copyStream(InputStream is, OutputStream os) throws IOException {
        byte[] buffer = new byte[1<<14];
        while (true) {
            int r = is.read(buffer);
            if (r > 0) {
                os.write(buffer, 0, r);
            } else if (r == -1) break;
        }
        os.flush();
    }

	public static void clear(File f) {
		if (f.isDirectory()) {
			File[] fs = f.listFiles();
			for (int i = 0; i < fs.length; i++) {
				remove(fs[i]);
			}
		}
	}

	public static void remove(File f) {
		if (f.isFile()) {
			f.delete();
		} else if (f.isDirectory()) {
			File[] fs = f.listFiles();
			for (int i = 0; i < fs.length; i++) {
				remove(fs[i]);
			}
			f.delete();
		}
	}

    public static boolean isSameFile(File f) {
        if(!f.exists()) return false;
        String fn = f.getName();
        try {
           String cn = f.getCanonicalFile().getName();
           return fn.equals(cn);
        } catch (IOException e) {
            return false;
        } 
    }

    public static void copyDir(File from, File to) {
        copyDir(from, to, false);
    }

    public static void copyDir(File from, File to, boolean mkdirs) {
        copyDir(from, to, mkdirs, true);
    }

    public static void copyDir(File from, File to, boolean mkdirs, boolean includeSubdirs) {
        copyDir(from, to, includeSubdirs, mkdirs, false);
    }

    public static void copyDir(File from, boolean includeSubdirs, File to) {
        copyDir(from, to, includeSubdirs, false, false);
    }

    public static void copyDir(File from, File to, boolean includeSubdirs, boolean mkdirs, boolean overwriteOnlyOlderFiles) {
    	copyDir(from, to, includeSubdirs, mkdirs, overwriteOnlyOlderFiles, null);
    }

    public static void copyDir(File from, File to, boolean includeSubdirs, boolean mkdirs, boolean overwriteOnlyOlderFiles, FileFilter filter) {
        if(filter != null && !filter.accept(from)) return;
        if (mkdirs) to.mkdirs();
        if(from == null || !from.isDirectory() || !to.isDirectory()) return;
        File[] fs = from.listFiles();
        if(fs == null) return;
        for (int i = 0; i < fs.length; i++) {
            String n = fs[i].getName();
            File c = new File(to, n);
            if (fs[i].isDirectory() && !includeSubdirs) continue;
        	if(filter != null && !filter.accept(new File(from, n))) continue;

            if(fs[i].isDirectory()) {
                c.mkdirs();
                copyDir(fs[i], c, includeSubdirs, mkdirs, overwriteOnlyOlderFiles, filter);
            } else if (overwriteOnlyOlderFiles && fs[i].isFile() && c.isFile()) {
                copyFile(fs[i], c, false, c.lastModified() < fs[i].lastModified());
            } else {
                copyFile(fs[i], c);
            }
        }
    }

    public static void unjar(File dest, String jar) throws IOException {
        dest.mkdirs();
        JarFile jf = new JarFile(jar);
        try {
            Enumeration es = jf.entries();
            while(es.hasMoreElements()) {
                JarEntry je = (JarEntry)es.nextElement();
                String n = je.getName();
                File f = new File(dest, n);
                if (je.isDirectory()) {
                    f.mkdirs();
                } else {
                    if (f.exists()) {
                        f.delete();
                    } else {
                        f.getParentFile().mkdirs();
                    }
                    InputStream is = jf.getInputStream(je);
                    FileOutputStream os = new FileOutputStream(f);
                    try {
                        copyStream(is, os);
                    } finally {
                        os.close();
                    }
                }
                long time = je.getTime();
                if (time != -1) f.setLastModified(time);
            }
        } finally {
            jf.close();
        }
    }

    public static void unjar(File dest, InputStream is) throws IOException {
        dest.mkdirs();
        JarInputStream jis = new JarInputStream(is);
        try {
            while(true) {
                JarEntry je = jis.getNextJarEntry();
                if (je == null) break;
                String n = je.getName();
                File f = new File(dest, n);
                if (je.isDirectory()) {
                    f.mkdirs();
                } else {
                    if (f.exists()) {
                        f.delete();
                    } else {
                        f.getParentFile().mkdirs();
                    }
                    FileOutputStream os = new FileOutputStream(f);
                    try {
                        copyStream(jis, os);
                    } finally {
                        os.close();
                    }
                }
                long time = je.getTime();
                if (time != -1) f.setLastModified(time);
            }
        } finally {
            jis.close();
        }
    }

    public static void jar(File[] fs, String path) throws IOException {
        jar(fs, path, null);
    }

    public static void jar(File[] fs, String path, Manifest mf) throws IOException  {
        File f = new File(path);
        FileOutputStream fos = new FileOutputStream(f);
        JarOutputStream jos = mf == null ? new JarOutputStream(fos) : new JarOutputStream(fos, mf);
        try {
            for (int i = 0; i < fs.length; i++) add(fs[i].getParentFile(), fs[i], jos);
        } finally {
            jos.close();
            fos.close();
        }
    }

    public static void add(File root, File f, JarOutputStream jos) throws IOException {
        int l = root.getAbsolutePath().length();
        String en = f.getAbsolutePath().substring(l + 1).replace('\\', '/');
        add(f, en, jos);
    }

    public static void add(File f, String name, JarOutputStream jos) throws IOException {
        String en = name;
        if(f.isDirectory()) en += "/"; //$NON-NLS-1$
        JarEntry entry = (en.endsWith("/")) ? null : new JarEntry(en); //$NON-NLS-1$
        if(f.isDirectory()) {
            if("/".equals(en)) en = ""; //$NON-NLS-1$ //$NON-NLS-2$
            File[] fs = f.listFiles();
            if(fs != null) for (int i = 0; i < fs.length; i++)
              add(fs[i], en + fs[i].getName(), jos);
        } else {
            try {
                jos.putNextEntry(entry);
            } catch (IOException e) {
                return;
            }
            FileInputStream is = new FileInputStream(f);
            byte[] b = new byte[1024];
            int q = 0;
            while((q = is.available()) > 0) {
                if(q > 1024) q = 1024;
                q = is.read(b, 0, q);
                jos.write(b, 0, q);
            }
            is.close();
        }
        if(entry != null) jos.closeEntry();
    }

    public static void copy(InputStream f, OutputStream t) throws IOException {
    	try {
    		byte[] b = new byte[1024];
    		int q = 0;
    		while((q = f.read(b, 0, b.length)) > 0) t.write(b, 0, q);
    	} finally {
    		f.close();    
    		t.close();
    	}
    }

    public static void unzip(File dest, String jar) throws IOException {
        dest.mkdirs();
        ZipFile zf = new ZipFile(jar);
        try {
            Enumeration es = zf.entries();
            while(es.hasMoreElements()) {
                ZipEntry je = (ZipEntry)es.nextElement();
                String n = je.getName();
                File f = new File(dest, n);
                if (je.isDirectory()) {
                    f.mkdirs();
                } else {
                    if (f.exists()) {
                        f.delete();
                    } else {
                        f.getParentFile().mkdirs();
                    }
                    InputStream is = zf.getInputStream(je);
                    FileOutputStream os = new FileOutputStream(f);
                    try {
                        copyStream(is, os);
                    } finally {
                        os.close();
                    }
                }
                long time = je.getTime();
                if (time != -1) f.setLastModified(time);
            }
        } finally {
            zf.close();
        }
    }

    public static String fileURLToFilePath(String url) {
        if(url == null) return null;
        String resultUrl = url.replace('\\', '/');
///        if(!url.startsWith("file:/")) return url;
		if(!resultUrl.startsWith("file:")) return resultUrl; //$NON-NLS-1$
        int iLast = resultUrl.lastIndexOf(':'), iFirst = resultUrl.indexOf(':');
        return (iLast == iFirst) ? resultUrl.substring(5) : resultUrl.substring(iLast - 1);
    }

    //// Relative path

	public static String getRelativePath(String rootpath, String path) {
		String[] r = tokenizePath(rootpath);
		String[] p = tokenizePath(path);
		if(r.length == 0 || p.length == 0 || !r[0].equalsIgnoreCase(p[0])) return null;
		int i = 0;
		while(i < r.length && i < p.length && r[i].equalsIgnoreCase(p[i])) ++i;
		StringBuffer sb = new StringBuffer();
		for (int k = i; k < r.length; k++) sb.append("/.."); //$NON-NLS-1$
		for (int k = i; k < p.length; k++) sb.append("/").append(p[k]); //$NON-NLS-1$
		return sb.toString();
	}

	private static String[] tokenizePath(String path) {
		String tokenizedPath = path.replace('\\', '/');
		StringTokenizer st = new StringTokenizer(tokenizedPath, "/"); //$NON-NLS-1$
		ArrayList l = new ArrayList();
		while(st.hasMoreTokens()) {
			String t = st.nextToken();
			if(t.isEmpty() || ".".equals(t)) continue; //$NON-NLS-1$
			if("..".equals(t)) { //$NON-NLS-1$
				if(!l.isEmpty()) l.remove(l.size() - 1);
				continue;
			}
			l.add(t);
		}
		return (String[])l.toArray(new String[l.size()]);
	}

    public static boolean writeFile(File f, String value, String encoding) {
        try {
            try {
                if(f.isFile() && !isSameFile(f)) f.delete();
                if(!f.exists()) {
                	f.getParentFile().mkdirs();
                	f.createNewFile();
                }
            } catch (IOException e) {
            	CommonCorePlugin.getPluginLog().logError(e);
            }
            FileOutputStream fs = new FileOutputStream(f);
            OutputStreamWriter osw = new OutputStreamWriter(fs, encoding);
            PrintWriter pw = new PrintWriter(osw);
            pw.print(value);
            pw.flush();
            pw.close();
            return true;
        } catch (FileNotFoundException e) {
			//ignore
            return writeFileDefault(f, value);
        } catch (UnsupportedEncodingException e) {
			//ignore
            return writeFileDefault(f, value);        	
        }
    }
    
    
    public static String getEncoding(String s) {
    	if(s == null) return null;
    	if(s.startsWith("<?xml")) { //$NON-NLS-1$
    		int i = s.indexOf("encoding="); //$NON-NLS-1$
    		if(i < 0) return null;
    		i += "encoding=".length() + 1; //$NON-NLS-1$
    		int j = s.indexOf('\"', i);
    		if(j < 0) return null;
    		return s.substring(i, j);
    	}    	
    	return null;
    }
    
    static Set validEncodings = new HashSet();
    static Set invalidEncodings = new HashSet();
    
    public static String validateEncoding(String encoding, String defaultEncoding) {
    	if(encoding == null || encoding.equals(defaultEncoding)) return defaultEncoding;
    	if(validEncodings.contains(encoding)) return encoding;
    	if(invalidEncodings.contains(encoding)) return defaultEncoding;
    	try {
    		if(defaultEncoding != null && defaultEncoding.equals("UTF-16")) { //$NON-NLS-1$
    			new String(XML_16, 0, XML_16.length, encoding);
    		} else {
    			new String(XML_8, 0, XML_8.length, encoding);
    		}
    		validEncodings.add(encoding);
    		return encoding;
    	} catch (UnsupportedEncodingException e) {
    		invalidEncodings.add(encoding);
    		return defaultEncoding;
    	}
    }
    
    static byte[] XML_8 = {(byte)'<',(byte)'?',(byte)'x',(byte)'m',(byte)'l'};
    static byte[] XML_16 = {(byte)-2,(byte)-1,(byte)0,(byte)60,(byte)0,(byte)63,(byte)0,(byte)120,(byte)0,(byte)109,(byte)0,(byte)108};

    public static String getEncoding(byte[] bs) {
    	if(bs.length < 20) return null;
    	if(startsWith(bs, XML_8)) {
    		int i = getIndex(bs, (byte)'?', 5);
    		if(i < 0) return "UTF-8"; //$NON-NLS-1$
    		String encoding = getEncoding(new String(bs, 0, i));
    		return validateEncoding(encoding, "UTF-8"); //$NON-NLS-1$
    	} else if(startsWith(bs, XML_16)) {
    		int i = getIndex(bs, (byte)'?', XML_16.length);
    		if(i < 0) return "UTF-16"; //$NON-NLS-1$
    		try {
    			String encoding = getEncoding(new String(bs, 0, i, "UTF-16")); //$NON-NLS-1$
        		return validateEncoding(encoding, "UTF-16"); //$NON-NLS-1$
    		} catch (UnsupportedEncodingException e) {
    			return null;
    		}
    	}
    	return null;
    }

    static boolean startsWith(byte[] bs, byte[] prefix) {
    	for (int i = 0; i < prefix.length; i++) {
    		if(bs[i] != prefix[i]) return false;
    	}
    	return true;
    }

    static int getIndex(byte[] bs, byte b, int offset) {
    	for (int i = offset; i < bs.length; i++) {
    		if(bs[i] == b) return i;
    	}
    	return -1;
    }
}