/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved.
 * 
 * The contents of this file are subject to the terms of the GNU
 * General Public License Version 3 only ("GPL").  
 * You may not use this file except in compliance with the License. 
 * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html
 * See the License for the specific language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each file.
 * 
 */

package org.jopendocument.util;

import org.jopendocument.util.cc.IClosure;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class FileUtils {

    private FileUtils() {
        // all static
    }

    /**
     * All the files (see {@link File#isFile()}) contained in the passed dir.
     * 
     * @param dir the root directory to search.
     * @return a List of String.
     */
    public static List<String> listR(File dir) {
        return listR_rec(dir, ".");
    }

    private static List<String> listR_rec(File dir, String prefix) {
        if (!dir.isDirectory())
            return null;

        final List<String> res = new ArrayList<String>();
        final File[] children = dir.listFiles();
        for (int i = 0; i < children.length; i++) {
            final String newPrefix = prefix + "/" + children[i].getName();
            if (children[i].isFile()) {
                // MAYBE add a way to restrict added files
                res.add(newPrefix);
            } else if (children[i].isDirectory()) {
                res.addAll(listR_rec(children[i], newPrefix));
            }
        }
        return res;
    }

    public static void walk(File dir, IClosure<File> c) {
        walk(dir, c, RecursionType.BREADTH_FIRST);
    }

    public static void walk(File dir, IClosure<File> c, RecursionType type) {
        if (type == RecursionType.BREADTH_FIRST)
            c.execute(dir);
        if (dir.isDirectory()) {
            for (final File child : dir.listFiles()) {
                walk(child, c, type);
            }
        }
        if (type == RecursionType.DEPTH_FIRST)
            c.execute(dir);
    }

    /**
     * Returns the relative path from one file to another in the same filesystem tree.
     * 
     * @param fromDir the starting directory, eg /a/b/.
     * @param to the file to get to, eg /a/x/y.txt.
     * @return the relative path, eg "../x/y.txt".
     * @throws IOException if an error occurs while canonicalizing the files.
     * @throws IllegalArgumentException if fromDir is not directory, or the files have no common
     *         ancestors (for example on Windows on 2 different letters).
     */
    public static final String relative(File fromDir, File to) throws IOException {
        if (!fromDir.isDirectory())
            throw new IllegalArgumentException(fromDir + " is not a directory");

        final File fromF = fromDir.getCanonicalFile();
        final File toF = to.getCanonicalFile();
        final List<File> toPath = getAncestors(toF);
        final List<File> fromPath = getAncestors(fromF);

        if (!toPath.get(0).equals(fromPath.get(0)))
            throw new IllegalArgumentException("'" + fromF + "' and '" + toF + "' have no common ancestor");

        int commonIndex = Math.min(toPath.size(), fromPath.size()) - 1;
        boolean found = false;
        while (commonIndex >= 0 && !found) {
            found = fromPath.get(commonIndex).equals(toPath.get(commonIndex));
            if (!found)
                commonIndex--;
        }

        // on remonte jusqu'à l'ancêtre commun
        final List<String> complete = new ArrayList<String>(Collections.nCopies(fromPath.size() - 1 - commonIndex, ".."));
        if (complete.isEmpty())
            complete.add(".");
        // puis on descend vers 'to'
        for (File f : toPath.subList(commonIndex + 1, toPath.size())) {
            complete.add(f.getName());
        }

        return CollectionUtils.join(complete, File.separator);
    }

    // return each ancestor of f (including itself)
    // eg [/, /folder, /folder/dir] for /folder/dir
    private static List<File> getAncestors(File f) {
        final List<File> path = new ArrayList<File>();
        File currentF = f;
        while (currentF != null) {
            path.add(0, currentF);
            currentF = currentF.getParentFile();
        }
        return path;
    }

    /**
     * Behave like the 'mv' unix utility, ie handle cross filesystems mv and <code>dest</code>
     * being a directory.
     * 
     * @param f the source file.
     * @param dest the destination file or directory.
     * @return the error or <code>null</code> if there was none.
     */
    public static String mv(File f, File dest) {
        final File destF;
        if (dest.isDirectory())
            destF = new File(dest, f.getName());
        else
            destF = dest;
        if (!f.renameTo(destF)) {
            try {
                copyFile(f, destF);
            } catch (IOException e) {
                return ExceptionUtils.getStackTrace(e);
            }
        }
        return null;
    }

    public static void copyFile(File in, File out) throws IOException {
        final FileChannel sourceChannel = new FileInputStream(in).getChannel();
        final FileChannel destinationChannel = new FileOutputStream(out).getChannel();
        sourceChannel.transferTo(0, sourceChannel.size(), destinationChannel);
        sourceChannel.close();
        destinationChannel.close();
    }

    public static void copyDirectory(File in, File out) throws IOException {
        if (in.isDirectory()) {
            if (!out.exists()) {
                out.mkdir();
            }

            String[] children = in.list();
            for (int i = 0; i < children.length; i++) {
                copyDirectory(new File(in, children[i]), new File(out, children[i]));
            }
        } else {
            if (!in.getName().equals("Thumbs.db")) {
                copyFile(in, out);
            }
        }
    }

    /**
     * Delete recursively the passed directory. If a deletion fails, the method stops attempting to
     * delete and returns false.
     * 
     * @param dir the dir to be deleted.
     * @return <code>true</code> if all deletions were successful.
     */
    public static boolean rmR(File dir) {
        if (dir.isDirectory()) {
            File[] children = dir.listFiles();
            for (int i = 0; i < children.length; i++) {
                boolean success = rmR(children[i]);

                if (!success) {

                    return false;
                }
            }
        }

        // The directory is now empty so delete it
        return dir.delete();
    }

    public static final void mkdir_p(File dir) throws IOException {
        if (!dir.exists()) {
            if (!dir.mkdirs()) {
                throw new IOException("cannot create directory " + dir);
            }
        }
    }

    /**
     * Read a file line by line with the default encoding and returns the concatenation of these.
     * 
     * @param f the file to read.
     * @return the content of f.
     * @throws IOException if a pb occur while reading.
     */
    public static final String read(File f) throws IOException {
        return read(f, null);
    }

    /**
     * Read a file line by line and returns the concatenation of these.
     * 
     * @param f the file to read.
     * @param charset the encoding of <code>f</code>, <code>null</code> means default encoding.
     * @return the content of f.
     * @throws IOException if a pb occur while reading.
     */
    public static final String read(File f, String charset) throws IOException {
        return read(new FileInputStream(f), charset);
    }

    public static final String read(InputStream ins, String charset) throws IOException {
        final Reader reader;
        if (charset == null)
            reader = new InputStreamReader(ins);
        else
            reader = new InputStreamReader(ins, charset);
        return read(reader);
    }

    public static final String read(final Reader reader) throws IOException {
        final BufferedReader in = new BufferedReader(reader);
        String line;
        String res = "";
        while ((line = in.readLine()) != null) {
            res += line + "\n";
        }
        in.close();
        return res;
    }

    /**
     * Read the whole content of a file.
     * 
     * @param f the file to read.
     * @return its content.
     * @throws IOException if a pb occur while reading.
     * @throws IllegalArgumentException if f is longer than <code>Integer.MAX_VALUE</code>.
     */
    public static final byte[] readBytes(File f) throws IOException {
        // no need for a Buffer since we read everything at once
        final InputStream in = new FileInputStream(f);
        if (f.length() > Integer.MAX_VALUE)
            throw new IllegalArgumentException("file longer than Integer.MAX_VALUE" + f.length());
        final byte[] res = new byte[(int) f.length()];
        in.read(res);
        in.close();
        return res;
    }

    public static void write(String s, File f) throws IOException {
        write(s, f, false);
    }

    public static void write(String s, File f, boolean append) throws IOException {
        final BufferedWriter w = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f, append)));
        try {
            w.write(s);
        } finally {
            w.close();
        }
    }

    /**
     * Tries to open the passed file as if it were graphicaly opened by the current user (respect
     * user's "open with"). If a native way to open the file can't be found, tries the passed list
     * of executables.
     * 
     * @param f the file to open.
     * @param executables a list of exectuables to try, eg ["ooffice", "soffice"].
     * @throws IOException if the file can't be opened.
     */
    public static final void open(File f, String[] executables) throws IOException {
        try {
            openNative(f);
        } catch (IOException exn) {
            for (int i = 0; i < executables.length; i++) {
                final String executable = executables[i];
                try {
                    Runtime.getRuntime().exec(new String[] { executable, f.getCanonicalPath() });
                    return;
                } catch (IOException e) {
                    // try the next one
                }
            }
            throw new IOException("unable to open " + f + " with: " + Arrays.asList(executables));
        }
    }

    /**
     * Open the passed file as if it were graphicaly opened by the current user (user's "open
     * with").
     * 
     * @param f the file to open.
     * @throws IOException if f couldn't be opened.
     */
    private static final void openNative(File f) throws IOException {
        final String os = System.getProperty("os.name");
        if (os.startsWith("Windows")) {
            Runtime.getRuntime().exec(new String[] { "cmd", "/c", "start", "\"\"", f.getCanonicalPath() });
        } else if (os.startsWith("Mac OS")) {
            Runtime.getRuntime().exec(new String[] { "open", f.getCanonicalPath() });
        } else if (os.startsWith("Linux") && gnomeRunning()) {
            Runtime.getRuntime().exec(new String[] { "gnome-open", f.getCanonicalPath() });
        } else {
            throw new IOException("unknown way to open " + f);
        }
    }

    private static final boolean gnomeRunning() {
        try {
            return Runtime.getRuntime().exec(new String[] { "pgrep", "-u", System.getProperty("user.name"), "nautilus" }).waitFor() == 0;
        } catch (Exception e) {
            return false;
        }
    }

    private static final Map<String, String> ext2mime;
    static {
        ext2mime = new HashMap<String, String>();
        ext2mime.put(".xml", "text/xml");
        ext2mime.put(".jpg", "image/jpeg");
        ext2mime.put(".png", "image/png");
        ext2mime.put(".tiff", "image/tiff");
    }

    /**
     * Try to guess the media type of the passed file name (see <a
     * href="http://www.iana.org/assignments/media-types">iana</a>).
     * 
     * @param fname a file name.
     * @return its mime type.
     */
    public static final String findMimeType(String fname) {
        for (final Map.Entry<String, String> e : ext2mime.entrySet()) {
            if (fname.toLowerCase().endsWith(e.getKey()))
                return e.getValue();
        }
        return null;
    }

}
