/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2007-2015 Broad Institute
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.broad.igv.util;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.output.StringBuilderWriter;
import org.broad.igv.logging.*;
import org.broad.igv.util.ftp.FTPUtils;

import java.io.*;
import java.net.URL;
import java.util.Arrays;
import java.util.Comparator;
import java.util.regex.Pattern;

/**
 * @author jrobinso
 */
public class FileUtils {

    private static final Logger log = LogManager.getLogger(FileUtils.class);

    final public static String LINE_SEPARATOR = System.getProperty("line.separator");
    final public static String FILE_SEP = System.getProperty("file.separator");
    final static String[] igvJnlpPrefixes = {"igv", "ichip", "29mammals", "hic"};


    public static boolean resourceExists(String path) {

        if (path == null) return false;

        if (isRemote(path)) {
            return HttpUtils.getInstance().resourceAvailable(path);
        } else {
            if (path.startsWith("file://")) {
                path = path.substring(7);
            }
            return (new File(path)).exists();
        }

    }


    public static boolean isRemote(String path) {
        if (path == null) {
            return false;
        }
        return path.startsWith("http://") || path.startsWith("https://") ||
                path.startsWith("ftp://") || path.startsWith("gs://") ||
                path.startsWith("s3://") || path.startsWith("htsget://");
    }


    public static boolean canWriteTo(File file) {
        FileOutputStream fos = null;
        try {
            file.createNewFile();
            return true;

            // Has permission
        } catch (Exception e) {
            return false;
        } finally {
            file.delete();
        }
    }

    public static String getInstallDirectory() {

        String path = FileUtils.class
                .getProtectionDomain()
                .getCodeSource()
                .getLocation().getPath();
        File f = new File(path);
        if (f.isDirectory()) {
            return f.getAbsolutePath();
        } else {
            return f.getParentFile().getAbsolutePath();
        }
    }

    /**
     * Get the relative path from one file to another, specifying the directory separator.
     * If one of the provided resources does not exist, it is assumed to be a file unless it ends with '/' or
     * '\'.
     *
     * @param targetPath    targetPath is calculated to this file
     * @param basePath      basePath is calculated from this file
     * @param pathSeparator directory separator. The platform default is not assumed so that we can test Unix behaviour when running on Windows (for example)
     * @return
     */
    public static String getRelativePath(String basePath, String targetPath, String pathSeparator) {

        // Normalize the paths
        String normalizedTargetPath = FilenameUtils.normalizeNoEndSeparator(targetPath);
        String normalizedBasePath = FilenameUtils.normalizeNoEndSeparator(basePath);

        // Undo the changes to the separators made by normalization
        if (pathSeparator.equals("/")) {
            normalizedTargetPath = FilenameUtils.separatorsToUnix(normalizedTargetPath);
            normalizedBasePath = FilenameUtils.separatorsToUnix(normalizedBasePath);

        } else if (pathSeparator.equals("\\")) {
            normalizedTargetPath = FilenameUtils.separatorsToWindows(normalizedTargetPath);
            normalizedBasePath = FilenameUtils.separatorsToWindows(normalizedBasePath);

        } else {
            throw new IllegalArgumentException("Unrecognised dir separator '" + pathSeparator + "'");
        }

        String[] base = normalizedBasePath.split(Pattern.quote(pathSeparator));
        String[] target = normalizedTargetPath.split(Pattern.quote(pathSeparator));

        // First get all the common elements. Store them as a string,
        // and also count how many of them there are.
        StringBuffer common = new StringBuffer();

        int commonIndex = 0;
        while (commonIndex < target.length && commonIndex < base.length
                && target[commonIndex].equals(base[commonIndex])) {
            common.append(target[commonIndex] + pathSeparator);
            commonIndex++;
        }

        if (commonIndex == 0) {
            // No single common path element. This most
            // likely indicates differing drive letters, like C: and D:.
            // These paths cannot be relativized.
            return targetPath;
        }

        // The number of directories we have to backtrack depends on whether the base is a file or a dir
        // For example, the relative path from
        //
        // /foo/bar/baz/gg/ff to /foo/bar/baz
        //
        // ".." if ff is a file
        // "../.." if ff is a directory
        //
        // The following is a heuristic to figure out if the base refers to a file or dir. It's not perfect, because
        // the resource referred to by this path may not actually exist, but it's the best I can do
        boolean baseIsFile = true;

        File baseResource = new File(normalizedBasePath);

        if (baseResource.exists()) {
            baseIsFile = baseResource.isFile();

        } else if (basePath.endsWith(pathSeparator)) {
            baseIsFile = false;
        }

        StringBuffer relative = new StringBuffer();

        if (base.length != commonIndex) {
            int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex;

            for (int i = 0; i < numDirsUp; i++) {
                relative.append(".." + pathSeparator);
            }
        }
        relative.append(normalizedTargetPath.substring(common.length()));
        return relative.toString();
    }

    static public String getPlatformIndependentPath(String path) {
        return path.replace('\\', '/');
    }


    // Convenience method
    public static String getRelativePath(String basePath, String targetPath) {
        return getRelativePath(basePath, targetPath, FILE_SEP);
    }

    /**
     * Delete a directory and all subdirectories
     *
     * @param dir
     * @return
     */
    public static boolean deleteDir(File dir) {
        if (dir.isDirectory()) {
            String[] children = dir.list();
            for (int i = 0; i < children.length; i++) {
                boolean success = deleteDir(new File(dir, children[i]));
                if (!success) {
                    return false;
                }
            }
        }

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


    /**
     * Test to see if the first non-comment line (first line not starting with #) is tab-delimited with the
     * given number of minimum columns.  Limit the test to the first 1,000 lines.
     */
    public static boolean isTabDelimited(BufferedReader reader, int minColumnCount) throws IOException {

        int nLinesTested = 0;
        String nextLine;
        while ((nextLine = reader.readLine()) != null && nLinesTested < 1000) {
            if (nextLine.startsWith("#")) {
                continue;
            }
            nLinesTested++;
            String[] tokens = nextLine.split("\t");
            if (tokens.length >= minColumnCount) {
                return true;
            }
        }
        return false;

    }

    /**
     * Return the first line not starting with commentChar
     *
     * @param path
     * @param commentChar
     * @return
     * @throws IOException
     */
    public static String getFirstLine(String path, String commentChar) throws IOException {

        if (path == null) return null;

        BufferedReader reader = null;
        try {
            reader = ParsingUtils.openBufferedReader(path);
            String nextLine;
            while ((nextLine = reader.readLine()) != null) {
                if (commentChar != null && nextLine.startsWith(commentChar)) {
                    continue;
                }
                return nextLine;
            }
            return null;
        } finally {
            if (reader != null) {
                reader.close();
            }
        }
    }

    /**
     * Copy a file from one location to another, using buffered writing
     *
     * @param inputFile
     * @param outputFile
     * @throws java.io.IOException
     */

    public static void copyFile(File inputFile, File outputFile) throws IOException {

        OutputStream out = null;
        InputStream in = null;
        try {
            in = new FileInputStream(inputFile);
            out = new FileOutputStream(outputFile);
            byte[] buffer = new byte[64000];
            int bytes_read;
            while ((bytes_read = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytes_read);
            }

        } catch (Exception e) {
            outputFile.delete();
            throw new RuntimeException("<html>Error copying file: " + outputFile.getAbsoluteFile() +
                    "<br/>" + e.toString());

        } finally {
            if (in != null) {
                in.close();
            }
            if (out != null) {
                out.flush();
                out.close();
            }
        }
    }

    private static void cleanup(File dir) {

        if (dir.exists() && dir.isDirectory()) {
            File[] jnlpFiles = dir.listFiles(new FileFilter() {

                public boolean accept(File arg0) {
                    final String name = arg0.getName();
                    for (String pre : igvJnlpPrefixes) {
                        if (name.startsWith(pre) && name.endsWith(".jnlp")) {
                            return true;
                        }
                    }
                    return false;
                }
            });

            // Sort files by ascending version number
            Arrays.sort(jnlpFiles, new Comparator<File>() {

                public int compare(File file1, File file2) {
                    if (org.apache.commons.io.FileUtils.isFileNewer(file1, file2)) {
                        return 1;
                    } else {
                        return -1;
                    }
                }
            });

            // Delete all but the highest version (newest) jnlp file
            for (int i = 0; i < jnlpFiles.length - 1; i++) {
                jnlpFiles[i].delete();
            }

            // Strip the version nuber fro the newest file
            if (jnlpFiles.length > 1) {
                File newestFile = jnlpFiles[jnlpFiles.length - 1];
                String fn = newestFile.getName();
                int dotIndex = fn.indexOf(".jnlp");
                int dashIndex = fn.lastIndexOf("-");
                if (dashIndex > 1) {
                    String newName = fn.substring(0, dashIndex) + fn.substring(dotIndex);
                    newestFile.renameTo(new File(newestFile.getParentFile(), newName));
                }
            }
        }
    }


//    public static void main(String[] args) throws IOException {
//        File inputDirectory = new File(args[0]);
//        File outputDirectory = new File(args[1]);
//        searchAndReplace(inputDirectory, outputDirectory, args[2], args[3]);
//    }

    public static String getFileExtension(String filePath) {
        String extension = null;
        int indexOfExtension = filePath.lastIndexOf(".");
        if (indexOfExtension >= 0) {
            extension = filePath.substring(indexOfExtension, filePath.length());
        }
        return extension;
    }

    /**
     * Returns an absolute path from the parent directory
     * of {@code referencePath} to the sub-element {@code inputPath}.
     * Safe to use with URLs.
     * If {@code inputPath} is an absolute path, it is returned unaltered.
     * <br/>
     * e.g.<br/>
     * String absPath = FileUtils.getAbsolutePath("test/mysession.xml", "/Users/bob/data/otherdata.xml");
     * System.out.println(absPath);<br/>
     * >>>> /Users/bob/data/test/mysession.xml
     *
     * @param inputPath     Relative path element
     * @param referencePath Absolute path root
     * @return
     */
    public static String getAbsolutePath(String inputPath, String referencePath) {
        if (isRemote(inputPath) || referencePath == null) {
            return inputPath;
        }
        File inFile = new File(inputPath);
        if (inFile.isAbsolute()) {
            return inFile.getAbsolutePath();
        }

        String absolutePath;

        if (isRemote(referencePath)) {
            int idx = referencePath.lastIndexOf("/");
            String basePath = referencePath.substring(0, idx);
            absolutePath = basePath + "/" + inputPath;
        } else {
            File referenceFile = new File(referencePath);
            File parent = referenceFile.isDirectory() ? referenceFile : referenceFile.getParentFile();
            File file = new File(parent, inputPath);
            try {
                absolutePath = file.getCanonicalPath();
            } catch (IOException e) {
                absolutePath = file.getAbsolutePath();
            }
        }

        return absolutePath;
    }

    /**
     * Return the path path.  The trailing "/" is not included.
     *
     * @param path
     * @return
     */
    public static String getParent(String path) {
        String piPath = getPlatformIndependentPath(path);
        int lastSlashIdx = piPath.lastIndexOf("/");
        return lastSlashIdx <= 0 ? path : path.substring(0, lastSlashIdx);
    }

    /**
     * Checks the system path for the provided executable.
     * If {@code executable} is a path (contains a path separator)
     * then it is returned unaltered
     *
     * @param executable
     * @return
     */
    public static String findExecutableOnPath(String executable) {
        if (executable.contains(File.separator)) return executable;

        String systemPath = System.getenv("PATH");
        if (systemPath == null) systemPath = System.getenv("path");
        if (systemPath == null || File.pathSeparator == null) return executable;

        String[] pathDirs = systemPath.split(File.pathSeparator);

        String fullPath = executable;
        for (String pathDir : pathDirs) {
            File file = new File(pathDir, executable);
            if (file.isFile()) {
                fullPath = file.getAbsolutePath();
                break;
            }
        }
        return fullPath;
    }


    /**
     * Return the length of the file, which might be remote.
     *
     * @param file
     * @return
     */
    public static long getLength(String file) {

        if (isRemote(file)) {
            try {
                URL url = HttpUtils.createURL(file);
                if (file.startsWith("ftp://")) {
                    return FTPUtils.getContentLength(url);
                } else {
                    return HttpUtils.getInstance().getContentLength(url);
                }
            } catch (Exception e) {
                log.error("Error fetching content length for: " + file, e);
                return -1;
            }
        } else {
            File f = new File(file);
            if (f.exists() && f.isFile()) {
                return f.length();
            } else {
                return -1;
            }
        }
    }

    /**
     * Read and return the file contents as a string
     *
     * @param path
     * @return
     */
    public static String getContents(String path) throws IOException {

        BufferedReader reader = null;

        try {
            reader = ParsingUtils.openBufferedReader(path);
            StringBuilder contents = new StringBuilder();
            PrintWriter pw = new PrintWriter(new StringBuilderWriter(contents));
            String nextLine;
            while ((nextLine = reader.readLine()) != null) {
                pw.println(nextLine);
            }
            return contents.toString();
        } finally {
            reader.close();
        }

    }

    public static byte[] readFully(String aInputFileName) {
        File file = new File(aInputFileName);
        byte[] result = new byte[(int) file.length()];
        try {
            InputStream input = null;
            try {
                int totalBytesRead = 0;
                input = new BufferedInputStream(new FileInputStream(file));
                while (totalBytesRead < result.length) {
                    int bytesRemaining = result.length - totalBytesRead;
                    //input.read() returns -1, 0, or more :
                    int bytesRead = input.read(result, totalBytesRead, bytesRemaining);
                    if (bytesRead > 0) {
                        totalBytesRead = totalBytesRead + bytesRead;
                    }
                }

            } finally {

                input.close();
            }
        } catch (FileNotFoundException ex) {

        } catch (IOException ex) {

        }
        return result;
    }


}
