/*
 * MAME FILE MANAGER - MAME resources management tool
 * Copyright (c) 2017.  Author phweda : [email protected]
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package Phweda.utils;

import Phweda.MFM.MFM;
import Phweda.MFM.MFM_Constants;

import java.awt.*;
import java.io.*;
import java.net.URL;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.FileVisitResult.TERMINATE;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * Created by IntelliJ IDEA.
 * User: Phweda
 * Date: 11/28/11
 * Time: 5:41 PM
 */
public class FileUtils {
    // Slash for Resource URLs
    public static final char SLASH = '/';
    public static final String DIRECTORY_SEPARATOR = File.separator;
    public static final String NEWLINE = System.getProperty("line.separator");
    public static final String ZIPSUFFIX = ".zip";

    /* Used to limit search depth. 20 should be more than sufficient*/
    private static final int maxDepth = 25;
    static int MaxDepth = maxDepth;

    // This filter only returns directories
    public static FilenameFilter directoryFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return file.isDirectory();
        }
    };

    // This filter only returns .zip files
    public static FilenameFilter zipFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return name.toLowerCase().endsWith(".zip");
        }
    };

    // This filter only returns .jar files
    public static FilenameFilter jarFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return name.toLowerCase().endsWith(".jar");
        }
    };

    // This filter only returns .ini files
    public static FilenameFilter iniFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return name.toLowerCase().endsWith(".ini");
        }
    };

    // This filter only returns .gif files
    public static FilenameFilter GIFFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return name.toLowerCase().endsWith(".gif");
        }
    };

    // This filter only returns .avi files
    public static FilenameFilter AVIFilenameFilter = new FilenameFilter() {
        public boolean accept(File file, String name) {
            return name.toLowerCase().endsWith(".avi");
        }
    };

    // This filter only returns video files
    // TODO really is not needed
    public static FilenameFilter VideoFilenameFilter = new FilenameFilter() {
        HashMap<String, String> tempMap = new HashMap<String, String>();
        private final Map<String, String> VIDEO_EXT = Collections.unmodifiableMap(tempMap);

        {
            tempMap.put(".avi", ".avi");
            tempMap.put(".flv", ".flv");
            tempMap.put(".mov", ".mov");
            tempMap.put(".mp4", ".mp4");
            tempMap.put(".mpeg", ".mpeg");
            tempMap.put(".mpg", ".mpg");
            //    tempMap.put(".", ".");
            //    tempMap.put(".", ".");
            //    tempMap.put(".", ".");
        }

        public boolean accept(File file, String name) {
            return name.contains(".") && VIDEO_EXT.containsKey(name.toLowerCase().substring(name.lastIndexOf('.')));
        }
    };

    /*
     *  @param Object path - allows us to handle both Strings and URLs
     */
    public static String getFileContents(Object path) {
        File file = null;
        if (path instanceof URL) {
            file = new File(((URL) path).getPath());
        } else if (path instanceof String) {
            file = new File((String) path);
        }

        if (!file.exists()) {
            return null;
        }

        StringBuilder fileContents = new StringBuilder((int) file.length());
        try {
            BufferedReader br = new BufferedReader(new FileReader(file));
            String line = null;
            while ((line = br.readLine()) != null) {
                fileContents.append(line);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace(MFM.logger.Writer());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return fileContents.toString();
    }

    /* TODO  java.nio version */
    public static void copyDirectoryTree(File source, File destination) throws IOException {

        /* We only want a Directory this is just a sanity check */
        if (source.isDirectory()) {

            //if directory does not exists, create it
            if (!destination.exists()) {
                destination.mkdir();
                System.out.println("Directory copied from "
                        + source + "  to " + destination);
            }

            //list all the directory child directories
            String files[] = source.list(directoryFilenameFilter);

            for (String file : files) {
                //construct the src and dest file structure
                File srcFile = new File(source, file);
                File destFile = new File(destination, file);
                //recursive copy
                copyDirectoryTree(srcFile, destFile);
            }
        }
    }

    /*
    *
    *
    *
     */
    public static void copyFile(File file, String destinationDir, boolean replace) throws IOException {
        Path destinationDirPath = Paths.get(destinationDir, file.getName());
        if (replace) {
            Files.copy(file.toPath(), destinationDirPath, REPLACE_EXISTING);
        } else {
            Files.copy(file.toPath(), destinationDirPath);
        }
    }

    /*
    *
    *
    *
     */
    public static void copyFile(Path file, String destinationDir, boolean replace) throws IOException {
        Path destinationDirPath = Paths.get(destinationDir, file.getFileName().toString());
        try {
            if (replace) {
                Files.copy(file, destinationDirPath, REPLACE_EXISTING);
            } else {
                Files.copy(file, destinationDirPath);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void copyDirectory(Path directory, String destinationDir, boolean replace) throws IOException {
        if (Files.isDirectory(directory)) {
            File newDir = new File(destinationDir + DIRECTORY_SEPARATOR + directory.getFileName());
            if (!newDir.exists()) {
                newDir.mkdir();
            }
            String newDestDir = newDir.getAbsolutePath();
            File[] files = directory.toFile().listFiles();
            for (File file : files) {
                copyFile(file.toPath(), newDestDir, replace);
            }
        }
    }

    public static void moveFile(File file, String destinationDir, boolean replace) throws IOException {
        Files.move(file.toPath(), Paths.get(destinationDir, file.toPath().getFileName().toString()));
    }


    /* TODO  java.nio version */
    /*
   * Searches for this fileName from this directory path
   *
   *
   * returns false if this directory does not exist
    */

    public static boolean fileExists(String path, String name) {
        try {
            return new FileExists().find(path, name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    public static String stripSuffix(String fileName) {
        // Strip suffix
        int pos = fileName.lastIndexOf(".");
        if (pos > 0) {
            fileName = fileName.substring(0, pos);
        }
        return fileName;
    }

    /**
     * Double quotes a String
     * NOTE replaces double quotes in input string with 2 single quotes
     *
     * @param input
     * @return
     */
    public static String doubleQuoteString(String input) {
        StringBuilder stringBuilder = new StringBuilder("\"");
        if (input.contains("\"")) {
            input = input.replace("\"", "''");
        }
        stringBuilder.append(input);
        stringBuilder.append("\"");
        return stringBuilder.toString();
    }

    public static Set<String> listFromFile(File file) {
        Set<String> list = null;
        try (Stream<String> stream = Files.lines(Paths.get(file.getAbsolutePath()))) {
            list = stream.collect(Collectors.toSet());

        } catch (IOException e) {
            e.printStackTrace();
        }
        return list;
    }

    public static void openTextFileFromOS(Path file) {
        try {
            File file1 = file.toFile();
            if (file1.exists()) {
                if (Desktop.isDesktopSupported()) {
                    Desktop desktop = Desktop.getDesktop();
                    if (desktop.isSupported(Desktop.Action.EDIT)) {
                        desktop.edit(file1);
                    }

                } else {
                    System.err.println("Awt Desktop is not supported!");
                }

            } else {
                System.err.println("File does not exist!");
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    // TODO Maybe we need to throw an Exception to propagate message to the user?
    // Or change this to boolean??
    public static void openFileFromOS(Path file) {
        try {
            File file1 = file.toFile();
            if (file1.exists()) {
                if (Desktop.isDesktopSupported()) {
                    Desktop.getDesktop().open(file1);
                } else {
                    System.err.println("Awt Desktop is not supported!");
                }

            } else {
                System.err.println("File does not exist!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private static class FileExists {
        boolean exists = false;

        private boolean find(String path, String fileName) throws IOException {
            File testFile = new File(path);
            // TODO can we simplify the logic? We're testing isDirectory() twice!!
            if (!testFile.isDirectory() && testFile.getName().equalsIgnoreCase(fileName)) {
                exists = true;
                return exists;
            } else if (testFile.isDirectory()) {
                // System.out.println("Searching through Directory " + path);
                //list all the directory children
                String files[] = testFile.list();
                for (String file : files) {
                    // We found it break out
                    if (exists) {
                        break;
                    }
                    find(testFile.getAbsolutePath() + DIRECTORY_SEPARATOR + file, fileName);
                }
            }
            return exists;
        }
    }

    public static class MFMcacheResourceFiles extends SimpleFileVisitor<Path> {

        static final TreeSet<String> extrasDirectories = new TreeSet<String>(
                Arrays.asList(MFM_Constants.MAME_FOLDER_NAMES_ARRAY));
        static TreeMap<String, File> cache;
        static TreeMap<String, TreeMap<String, File>> extrasCache;
        static boolean directoryOnly = false;
        static boolean extras = false;
        static boolean SLCHDs = false;

        // NO Directories?
        public void cacheAllFiles(Path root, TreeMap<String, File> cacheIn, boolean directoryOnlyIn) {
            if (root.toString().isEmpty()) {
                return;
            }
            cache = cacheIn;
            directoryOnly = directoryOnlyIn;
            walkTree(root);
        }

        public void cacheSoftwarelistCHDsFiles(Path root, TreeMap<String, File> cacheIn) {
            SLCHDs = true;
            cacheAllFiles(root, cacheIn, true);
            SLCHDs = false;
        }

        public TreeMap<String, TreeMap<String, File>> cacheExtrasFiles(Path root) {
            extras = true;
            directoryOnly = false;
            extrasCache = new TreeMap<String, TreeMap<String, File>>();

            try {
                walkTree(root);
            } finally {
                extras = false;
            }
            return extrasCache;
        }

        private void walkTree(Path root) {
            try {
                Files.walkFileTree(root, EnumSet.noneOf(FileVisitOption.class),
                        FileUtils.MaxDepth, this);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException {
            if (directoryOnly) {
                return CONTINUE;
            } else if (!extras) {
                File file = path.toFile();
                if (MFM.isSystemDebug()) {
                    System.out.println("Caching file : " + path.toString());
                }
                cache.put(file.getName().substring(0, file.getName().lastIndexOf('.')), file);
            } else {
                File file = path.toFile();
                cacheExtraFile(file);
            }
            return CONTINUE;
        }

        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
            System.out.println("visitFileFailed for file: " + file.toString() + '\n' + exc);
            System.err.println("visitFileFailed for file: " + file.toString() + '\n' + exc);
            return CONTINUE;
        }

        @Override
        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
            if (extras) {
                cacheExtraFile(dir.toFile());
            }
            return CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
            File directory = dir.toFile();
            if (!extras && directory.exists()) {
                String key = null;
                if (SLCHDs) {
                    key = directory.getName() + ':' + directory.getParentFile().getName();
                } else {
                    return CONTINUE;
                }
                cache.put(key, directory);
            } else if (!extras) {
                MFM.logger.addToList("FileUtils 363: " + dir + " does not exist.");
            }
            return CONTINUE;
        }

        private void cacheExtraFile(File file) {
            if (file.isDirectory()) {
                String name = file.getName().toLowerCase(); // Just in case somebody plays with Capitals
                if (extrasDirectories.contains(name)) {
                    if (!extrasCache.containsKey(name)) {
                        extrasCache.put(name, new TreeMap<String, File>());
                    }
                }
            } else {
                String fileName = file.getName();
                if (fileName.endsWith(".zip") &&
                        extrasDirectories.contains(fileName.substring(0, fileName.lastIndexOf('.')))) {
                    return;
                }

                String filePath = file.getAbsolutePath().substring(0,
                        file.getAbsolutePath().lastIndexOf(FileUtils.DIRECTORY_SEPARATOR));
                // Find which Extras folder is contained in the path and add file to that map
                for (String folderName : extrasDirectories) {
                    if (folderName.equalsIgnoreCase(MFM_Constants.MAME_FOLDER_SNAPS)) {
                        continue;
                    }
                    if (filePath.contains(folderName) && !filePath.contains(MFM_Constants.MAME_FOLDER_SNAPS)) {
                        try {
                            // TODO needs testing!!!! 3/14/16
                            // fixme is this conditional extraneous? Does it HAVE to be the parent?
                            if (folderName.equalsIgnoreCase(file.getParentFile().getName())) {
                                extrasCache.get(folderName).put(
                                        file.getName().substring(0, file.getName().lastIndexOf('.')), file);
                            }
                            break;
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        return;
                    }
                }
            }
        }

        /**
         * Not needed? TODO
         *
         * @param file
         * @return
         */
        private String extrasKey(File file) {
            String path = file.getAbsolutePath();

            return null;
        }

    }

    /*
     * TODO convert to singleton??
     *
     * */
    /*For java.nio MFM only!! */
    public static class MFM_FindFile extends SimpleFileVisitor<Path> {
        /*    For MFM :
         * We want to support finding multiple files of one name - Extras for a single Machine
         * And finding multiple Directories of multiple names on a single pass
         * Try to do everything on a single pass of a directory tree
         */

        private static Path fileName = null;
        private static String[] directoryNames = null;
        private static boolean matchWithoutSuffix = false;
        private static boolean matchDirectory = false;

        private static boolean exists = false;
        private static Path startFile = null;
        private static Path file = null;

        /*
         * For MAME extras we will have multiple
         * files with the same rootName
         */
        private static ArrayList<Path> files = new ArrayList<>(15);
        private static HashMap<String, String> directories = new HashMap<String, String>(15);

        /*
         * Use with true matchWithoutSuffix for getting more than one file result
         * Use with true matchDirectory for getting Directory(ies)
         */
        public MFM_FindFile(Path directory, Path fileName, boolean matchWithoutSuffix,
                            boolean matchDirectory) throws IOException {
            MFM_FindFile.fileName = fileName;
            if (matchDirectory) {
                MFM_FindFile.directoryNames = new String[]{fileName.getFileName().toString()};
            }
            MFM_FindFile.matchWithoutSuffix = matchWithoutSuffix;
            MFM_FindFile.matchDirectory = matchDirectory;
            startFile = Files.walkFileTree(directory, EnumSet.noneOf(FileVisitOption.class),
                    FileUtils.MaxDepth, this);
        }

        public MFM_FindFile(Path directory, String[] fileNames, boolean matchWithoutSuffix,
                            boolean matchDirectory) throws IOException {
            MFM_FindFile.directoryNames = fileNames;
            MFM_FindFile.matchWithoutSuffix = matchWithoutSuffix;
            MFM_FindFile.matchDirectory = matchDirectory;
            startFile = Files.walkFileTree(directory, EnumSet.noneOf(FileVisitOption.class),
                    FileUtils.MaxDepth, this);
        }

        public MFM_FindFile(Path directory, Path fileName) throws IOException {
            new MFM_FindFile(directory, fileName, matchWithoutSuffix, false);
        }

        /*
                //Print information about each type of file.
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
                    if (attr.isSymbolicLink()) {
                        System.out.format("Symbolic link: %s ", file);
                    } else if (attr.isRegularFile()) {
                        System.out.format("Regular file: %s ", file);
                    } else {
                        System.out.format("Other: %s ", file);
                    }
                    return CONTINUE;
                }

        */

        // TODO Do not need?? See outerclass FileUtils
        public static boolean exists() {
            return exists;
        }

        public static Path File() {
            return file;
        }

        public static ArrayList<Path> Files() {
            return (ArrayList<Path>) files.clone();
        }

        public static HashMap<String, String> directories() {
            return (HashMap<String, String>) directories.clone();
        }

        // TODO break this up to multiple methods
        @Override
        public FileVisitResult visitFile(Path file, BasicFileAttributes attr) {
            // NOTE if we are searching for Directories also file may be NULL ??
            if (file == null) {
                System.out.println("visitFile File: is NULL");
                return CONTINUE;
            }
            if (file.getFileName().equals(fileName)) {
                System.out.println("Located file: " + file);
                if (!matchWithoutSuffix) {
                    MFM_FindFile.file = file;
                    exists = true;
                    return TERMINATE;
                }
            }  // Do we have a base file name, extension not included, match??
            else if (compareFileBaseName(file.getFileName(), fileName)) {
                System.out.println("Located file: " + file);
                files.add(file);
                exists = true;
            }
            return CONTINUE;
        }

        @Override
        public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
            // System.out.println("postVisitDirectory Directory: " + dir);
            if (matchDirectory) {
                for (String dirName : directoryNames) {
                    if (dir.getFileName().toString().equals(dirName)) {
                        directories.put(dirName, dir.toString());
                        exists = true;
                        return CONTINUE;
                    }
                }
            }
            return CONTINUE;
        }

        //If there is some error accessing the file, let the user know.
        //If you don't override this method and an error occurs, an IOException
        //is thrown.
        @Override
        public FileVisitResult visitFileFailed(Path file, IOException exc) {
            System.err.println(exc);
            return CONTINUE;
        }

        /*
         * Compares file names without extension
         * Used to find MAME Extras files using naming convention of all being
         * named after getMachine short name
         *
         */
        private boolean compareFileBaseName(Path fileIn, Path fileNameIn) {
            if (fileIn == null || fileNameIn == null) {
                return false;
            }
            // TODO This is ugly and wasteful - StringBuilder ??
            String name = fileNameIn.toString();
            String file;
            if (name.contains(".")) {
                file = name.substring(0, name.indexOf('.'));
            } else {
                file = name;
            }
            return file.equalsIgnoreCase(name);
        }


        public void clear() {
            files.clear();
            directories.clear();
            // TODO figure out if we need this
            file = null;
        }
    }

}