/*
 * @(#)FileTools.java
 *
 * Copyright (c) 2002-2003 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.util;

import com.jscape.util.crypto.CryptoServicesProvider;

import java.io.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileTime;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Vector;

import static com.jscape.util.Parameters.assertNotNull;


/**
 * @author Alex
 */
@SuppressWarnings("unchecked")
public class FileTools {

    public static final FileFilter FILE_FILTER = pathname -> pathname.isFile();

    public static final FilenameFilter FILE_NAME_FILTER = (dir, name) -> new File(dir, name).isFile();

    public static final FileFilter DIRECTORY_FILTER = pathname -> pathname.isDirectory();

    public static final FilenameFilter DIRECTORY_NAME_FILTER = (dir, name) -> new File(dir, name).isDirectory();

    public static final DirectoryStream.Filter<Path> ACCEPT_ALL_FILTER = entry -> true;

    public static File canonical(File target) {
        try {
            return target.getCanonicalFile();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static File createDirectory(String path)
            throws IOException {
        return createDirectory(new File(path));
    }

    public static File createDirectory(File path)
            throws IOException {
        if (path.isDirectory()) {
            return path;
        }

        Files.createDirectories(path.toPath());

        return path;
    }

    public static Path overwriteFileData(Path victim, boolean randomDataRequired, int bufferSize, int passes, boolean truncateRequired)
            throws Exception {
        try (RandomAccessFile file = new RandomAccessFile(victim.toFile(), "rwd")) {
            byte[] buffer = new byte[bufferSize];
            SecureRandom random = randomDataRequired ? CryptoServicesProvider.secureRandom() : null;

            for (int pass = 0; pass < passes; ++pass) {
                for (long position = 0, size = file.length(); position < size; position += buffer.length) {
                    file.seek(position);

                    if (randomDataRequired) {
                        random.nextBytes(buffer);
                    }

                    file.write(buffer);
                }
            }

            if (truncateRequired) {
                file.setLength(0);
            }
        }

        return victim;
    }
    /**
     * Effectively deletes the specified file.
     *
     * @param victim    file to delete
     * @param recursive if <code>true</code> directories will be deleted recursively
     *
     * @return <code>true</code> if file was deleted; <code>false</code> otherwise
     */
    public static boolean delete(File victim, boolean recursive) {
        assertNotNull(victim);

        if (victim.isDirectory()) {
            File[] files = victim.listFiles();

            if (files != null && files.length > 0 && recursive) {
                for (File file : files) {
                    delete(file, true);
                }
            }
        }

        return victim.delete();
    }

    public static void delete(Path victim, boolean recursive)
            throws IOException {
        if (Files.isDirectory(victim) && recursive) {
            Files.walkFileTree(victim,
                               new SimpleFileVisitor<Path>() {
                                   @Override
                                   public FileVisitResult postVisitDirectory(Path dir, IOException e)
                                           throws IOException {
                                       Files.delete(dir);
                                       return FileVisitResult.CONTINUE;
                                   }

                                   @Override
                                   public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                                           throws IOException {
                                       Files.delete(file);
                                       return FileVisitResult.CONTINUE;
                                   }
                               });
        } else {
            Files.deleteIfExists(victim);
        }
    }

    public static void deleteFile(File file)
            throws IOException {
        Files.deleteIfExists(file.toPath());
    }

    public static void deleteFileSafe(File file) {
        try {
            deleteFileSafe(file.toPath());
        } catch (Exception e) {
            // ignore
        }
    }

    public static void deleteFileSafe(Path file) {
        try {
            Files.deleteIfExists(file);
        } catch (Exception e) {
            // ignore
        }
    }

    public static void deleteFilesSafe(Path directory, final String regexp) {
        try {
            Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                        throws IOException {
                    if (file.getFileName().toString().matches(regexp)) {
                        deleteFileSafe(file);
                    }

                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (Exception e) {
            // ignore
        }
    }

    public static void setLastModified(File file, long time)
            throws IOException {
        Files.setLastModifiedTime(file.toPath(), FileTime.fromMillis(time));
    }

    public static void setLastModifiedSafe(File file, long time) {
        try {
            setLastModified(file, time);
        } catch (Exception e) {
            // ignore
        }
    }

    public static int countDirectoryEntries(Path path, DirectoryStream.Filter<? super Path> filter)
            throws IOException {
        if (!Files.isDirectory(path)) {
            return 0;
        }

        int count = 0;
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(path, filter)) {
            for (Path ignored : stream) {
                ++count;
            }
        }

        return count;
    }

    public static void copyDirectory(File source, File destination)
            throws IOException {
        createDirectory(destination);

        File[] entries = source.listFiles();

        if (entries == null) {
            throw new IOException("Error reading directory contents.");
        }

        for (File entry : entries) {
            File destinationEntry = new File(destination, entry.getName());

            if (entry.isFile()) {
                IOTools.copy(entry, destinationEntry);
            } else if (entry.isDirectory()) {
                copyDirectory(entry, destinationEntry);
            }
        }
    }

    public static void renameFile(File source, File destination)
            throws IOException {
        boolean renamed = source.renameTo(destination);
        if (!renamed) {
            throw new IOException(String.format("Error renaming file '%s' to '%s'.", source, destination));
        }
    }

    public static void delete(String name, File directory) {
        File[] files = directory.listFiles();

        if (files == null || files.length == 0) {
            return;
        }

        for (File file : files) {
            if (file.getName().equals(name)) {
                delete(file, true);
            } else if (file.isDirectory()) {
                delete(name, file);
            }
        }
    }

    public static void clearDirectory(File directory) {
        File[] files = directory.listFiles();

        if (files == null || files.length == 0) {
            return;
        }

        for (int i = 0; i < files.length; ++i) {
            File file = files[i];
            delete(file, true);
        }
    }

    public static byte[] getTail(String file, int lines, byte[] sep)
            throws IOException {
        RandomAccessFile reader = new RandomAccessFile(file, "r");

        try {
            Buffer buf = new Buffer(lines * 70);
            byte[] inversedSep = new StringBuffer(new String(sep, Charset.defaultCharset())).reverse().toString().getBytes(Charset.defaultCharset());
            long filePos = reader.length() - 1;

            while (lines >= 0 && filePos >= 0) {
                do {
                    reader.seek(filePos);
                    int ch = reader.read();
                    buf.append((byte) ch);
                    --filePos;
                } while (!buf.endsWith(inversedSep) && filePos >= 0);

                --lines;
            }

            buf.reverse();

            if (buf.startsWith(sep)) {
                buf.delete(0, sep.length);
            }

            return buf.toByteArray();
        } finally {
            IOTools.close(reader);
        }
    }

    public static long getLength(File file, boolean recursive) {
        File[] files = file.listFiles();
        if (files == null) {
            return file.length();
        }

        long total = 0;
        for (int i = 0; i < files.length; ++i) {
            File candidate = files[i];

            if (candidate.isFile()) {
                total += candidate.length();
            } else if (recursive) {
                total += getLength(candidate, recursive);
            }
        }

        return total;
    }

    /**
     * Compares file content.
     *
     * @param fileA first file
     * @param fileB second file
     *
     * @return the first difference position
     * or <code>-1</code> if no difference was found
     *
     * @throws IOException if an IO error occurs
     */
    public static long compareFiles(File fileA, File fileB)
            throws IOException {
        if (fileA.isDirectory() || fileB.isDirectory()) {
            return 0;
        }

        try (InputStream inA = Files.newInputStream(fileA.toPath()); InputStream inB = Files.newInputStream(fileB.toPath());) {
            BufferedInputStream inOne = new BufferedInputStream(inA);
            BufferedInputStream inTwo = new BufferedInputStream(inB);
            long pos = 0;
            int readOne;
            int readTwo;

            while (((readOne = inOne.read()) != -1) && ((readTwo = inTwo.read()) != -1) && (readOne == readTwo)) {
                ++pos;
            }

            return (pos == fileA.length() && pos == fileB.length()) ? -1 : pos;
        }
    }

    /**
     * Gets remote directory name removing any path information.
     *
     * @param path the remote directory path
     *
     * @return the remote directory name
     */
    public static String getDirectoryName(String path) {
        path = path.trim();

        if (path.startsWith("\"")) {
            path = path.substring(1, path.lastIndexOf("\""));
        }

        if (path.equals("/") || path.equals("\\")) {
            return path;
        }

        if (path.endsWith("/") || path.endsWith("\\")) {
            path = path.substring(0, path.length() - 1);
        }

        if (path.indexOf("/") != -1) {
            return path.substring(path.lastIndexOf("/") + 1);
        } else if (path.indexOf("\\") != -1) {
            return path.substring(path.lastIndexOf("\\") + 1);
        } else {
            return path;
        }
    }

    /**
     * Gets a empty directory vector
     *
     * @param dir a directory which will be consulted
     *
     * @return A collection of empty directories
     */
    public static Vector getEmptyDirList(File dir) {
        Vector vector = new Vector();
        File[] entries = dir.listFiles();

        if (entries != null) {
            for (int i = 0; i < entries.length; i++) {
                File entry = entries[i];

                if (entry.isDirectory()) {
                    if (entry.listFiles().length == 0) {
                        String path = entry.getAbsolutePath();
                        vector.add(path);
                    } else {
                        vector.addAll(getEmptyDirList(entry));
                    }
                }
            }
        }

        return vector;
    }

    /**
     * Gets a file list from the system file
     *
     * @param dir Local directory which will be consulted
     *
     * @return A collection of path files
     */
    public static Vector listFiles(File dir) {
        Vector ret = new Vector();
        File[] entries = dir.listFiles();

        if (entries == null) {
            return ret;
        }

        for (int i = 0; i < entries.length; i++) {
            File entry = entries[i];

            if (entry.isDirectory()) {
                ret.addAll(listFiles(entry));
            } else {
                ret.add(entry.getAbsolutePath());
            }
        }

        return ret;
    }

    public static List listFiles(File dir, FileFilter filter) {
        List ret = new LinkedList();
        File[] entries = dir.listFiles();

        if (entries == null) {
            if (filter.accept(dir)) {
                ret.add(dir);
            }

            return ret;
        }

        for (int i = 0; i < entries.length; ++i) {
            File entry = entries[i];

            if (entry.isDirectory()) {
                ret.addAll(listFiles(entry, filter));
            } else if (filter.accept(entry)) {
                ret.add(entry);
            }
        }

        return ret;
    }

    public static List<Path> contentOf(Path directory, boolean recursive)
            throws Exception {
        List<Path> paths = new LinkedList<>();

        try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory)) {
            for (Path path : directoryStream) {
                paths.add(path);

                if (recursive && Files.isDirectory(path)) {
                    List<Path> subPaths = contentOf(path, true);
                    paths.addAll(subPaths);
                }
            }
        }

        return paths;
    }

    public static String getRelativePath(File homeDirectory, File filePath, boolean isFile)
            throws IOException {
        List homelist = getPathList(homeDirectory);
        List filelist = getPathList(filePath);

        return matchPathLists(homelist, filelist, isFile);
    }

    public static String getRelativePath(File homeDirectory, File filePath)
            throws IOException {
        return getRelativePath(homeDirectory, filePath, true);
    }

    /**
     * Substitute reserved characters.
     * <p/>
     * The following characters are reserved:
     * <ul>
     * <li>< (less than)</li>
     * <li>> (greater than)</li>
     * <li>: (colon)</li>
     * <li>" (double quote)</li>
     * <li>/ (backslash)</li>
     * <li>\ (forward slash)</li>
     * <li>| (vertical bar or pipe)</li>
     * <li>? (question mark)</li>
     * <li>* (asterisk)</li>
     * </ul>
     *
     * @param filename   the filename
     * @param substitute the replace character
     *
     * @return normalized filename
     *
     * @see <a href=http://msdn.microsoft.com/en-us/library/aa365247.aspx#naming_conventions>Naming Conventions</a>
     */
    public static String normalizeFilename(String filename, char substitute) {
        return filename.replaceAll("[/\\\\<>:\"|?*]", Character.toString(substitute));
    }

    public static File[] roots() {
        String os = System.getProperty("os.name");
        return os != null && os.toUpperCase(Locale.ENGLISH).startsWith("MAC") ? macRoots() : File.listRoots();
    }

    private static File[] macRoots() {
        File[] roots = new File("/Volumes").listFiles(DIRECTORY_FILTER);
        return roots != null ? roots : File.listRoots();
    }

    private static List getPathList(File file)
            throws IOException {
        List list = new LinkedList();
        File parent = file.getCanonicalFile();

        while (parent != null) {
            list.add(parent.getName());
            parent = parent.getParentFile();
        }

        return list;
    }

    private static String matchPathLists(List homeList, List filePathList, boolean isFile) {
        StringBuilder path = new StringBuilder();

        int i = homeList.size() - 1;
        int j = filePathList.size() - 1;

        // Remove common root
        while ((i >= 0) && (j >= 0) && (homeList.get(i).equals(filePathList.get(j)))) {
            i--;
            j--;
        }

        int top = 0;
        if (isFile) {
            top = 1;
        }

        while (j >= top) {
            path.append(filePathList.get(j));

            if (j > top) {
                path.append('/');
            }

            j--;
        }

        return path.toString();
    }


    /**
     * Private constructor to avoid unnecessary instantiation.
     */
    private FileTools() {
        // empty
    }

}
