/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.iterators.IterableUtils;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.system.RuntimeConfiguration;
import net.ranides.assira.text.Wildcard;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Collections;
import java.util.Locale;

/**
 * Utility methods for working with Paths.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class PathUtils {

    /**
     * Creates Path from provided URL.
     *
     * Method converts URL to URI and then to Path.
     * It does not extract "path" part from URL.
     * It resolves full URL into Path, trying to use scheme supported by JDK.
     *
     * @param url url
     * @return Path
     */
    public static Path fromURL(URL url) {
        try {
            return Paths.get(url.toURI());
        } catch (URISyntaxException cause) {
            throw new IllegalArgumentException(cause);
        }
    }

    /**
     * Extracts long extension part from given path.
     * Long extension is fragment of filename after first dot "." separator, including dot.
     *
     * For example: "c:\\windows\\file.txt.bak" will give ".txt.bak"
     *
     * @param path path
     * @return string
     */
    public static String getLongExtension(String path) {
        return getLongExtension(Paths.get(path));
    }

    /**
     * Extracts long extension part from given path.
     * Long extension is fragment of filename after first dot "." separator, including dot.
     *
     * For example: "c:\\windows\\file.txt.bak" will give ".txt.bak"
     *
     * @param path path
     * @return string
     */
    public static String getLongExtension(Path path) {
        String name = path.getFileName().toString();
        int index = name.indexOf(".");
        return (-1==index) ? "" : name.substring(index);
    }

    /**
     * Extracts short extension part from given path.
     * Short extension is fragment of filename after last dot "." separator, including dot.
     *
     * For example: "c:\\windows\\file.txt.bak" will give ".bak"
     *
     * @param path path
     * @return string
     */
    public static String getShortExtension(String path) {
        return getShortExtension(Paths.get(path));
    }

    /**
     * Extracts short extension part from given path.
     * Short extension is fragment of filename after last dot "." separator, including dot.
     *
     * For example: "c:\\windows\\file.txt.bak" will give ".bak"
     *
     * @param path path
     * @return string
     */
    public static String getShortExtension(Path path) {
        String name = path.getFileName().toString();
        int index = name.lastIndexOf(".");
        return (-1==index) ? "" : name.substring(index);
    }

    /**
     * Creates new path, with long extesion replaced by provided one.
     * Long extension is fragment of filename after first dot "." separator, including dot.
     *
     * New extension should include dot, if necessary.
     * New extension could be empty, then old extension will just removed.
     *
     * For example, for path: "c:\\windows\\file.txt.bak"
     *      replace with ".log" will give "c:\\windows\\file.log"
     *      replace with "log" will give "c:\\windows\\filelog"
     *      replace with "\\log" will give "c:\\windows\\file\\log"
     *      replace with "" will give "c:\\windows\\file"
     *
     * @param path path
     * @param extension extension
     * @return path
     */
    public static Path changeLongExtension(Path path, String extension) {
        return changeLongExtension(path.toString(), extension);
    }

    /**
     * Creates new path, with long extesion replaced by provided one.
     * Long extension is fragment of filename after first dot "." separator, including dot.
     *
     * New extension should include dot, if necessary.
     * New extension could be empty, then old extension will be just removed.
     *
     * For example, for path: "c:\\windows\\file.txt.bak"
     *      replace with ".log" will give "c:\\windows\\file.log"
     *      replace with "log" will give "c:\\windows\\filelog"
     *      replace with "\\log" will give "c:\\windows\\file\\log"
     *      replace with "" will give "c:\\windows\\file"
     *
     * @param path path
     * @param extension extension
     * @return path
     */
    public static Path changeLongExtension(String path, String extension) {
        int from = Math.max(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
        int index = path.substring(from).indexOf(".");
        if(-1==index) {
            return Paths.get(path + extension);
        } else {
            return Paths.get(path.substring(0, from + index) + extension);
        }
    }

    /**
     * Creates new path, with short extesion replaced by provided one.
     * Short extension is fragment of filename after last dot "." separator, including dot.
     *
     * New extension should include dot, if necessary.
     * New extension could be empty, then old extension will be just removed.
     *
     * For example, for path: "c:\\windows\\file.txt.bak"
     *      replace with ".log" will give "c:\\windows\\file.txt.log"
     *      replace with "log" will give "c:\\windows\\file.txtlog"
     *      replace with "\\log" will give "c:\\windows\\file.txt\\log"
     *      replace with "" will give "c:\\windows\\file.txt"
     *
     * @param path path
     * @param extension extension
     * @return path
     */
    public static Path changeShortExtension(Path path, String extension) {
        return changeShortExtension(path.toString(), extension);
    }

    /**
     * Creates new path, with short extesion replaced by provided one.
     * Short extension is fragment of filename after last dot "." separator, including dot.
     *
     * New extension should include dot, if necessary.
     * New extension could be empty, then old extension will be just removed.
     *
     * For example, for path: "c:\\windows\\file.txt.bak"
     *      replace with ".log" will give "c:\\windows\\file.txt.log"
     *      replace with "log" will give "c:\\windows\\file.txtlog"
     *      replace with "\\log" will give "c:\\windows\\file.txt\\log"
     *      replace with "" will give "c:\\windows\\file.txt"
     *
     * @param path path
     * @param extension extension
     * @return path
     */
    public static Path changeShortExtension(String path, String extension) {
        int from = Math.max(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
        int index = path.substring(from).lastIndexOf(".");
        if(-1==index) {
            return Paths.get(path + extension);
        } else {
            return Paths.get(path.substring(0, from + index) + extension);
        }
    }

    /**
     * Returns true if path ends with provided suffix.
     *
     * Behaviour is platform dependent:
     * if current file system is case sensitive, then this methods is too.
     *
     * @param path path
     * @param extension extension
     * @return boolean
     */
    public static boolean hasExtension(String path, String extension) {
        if(RuntimeConfiguration.FEATURE_CASE_SENSITIVE_FILES()) {
            return path.endsWith(extension);
        } else {
            return path.toUpperCase(Locale.ROOT).endsWith(extension.toUpperCase(Locale.ROOT));
        }
    }

    /**
     * Returns true if path ends with provided suffix.
     *
     * Behaviour is platform dependent:
     * if current file system is case sensitive, then this methods is too.
     *
     * @param path path
     * @param extension extension
     * @return boolean
     */
    public static boolean hasExtension(Path path, String extension) {
        return hasExtension(path.toString(), extension);
    }

    /**
     * Extracts filename without long extension.
     * For example: "c:\\windows\\file.txt.bak" will give "file"
     *
     * @param path path
     * @return string
     */
    public static String getSimpleName(String path) {
        return getSimpleName(Paths.get(path));
    }

    /**
     * Extracts filename without long extension.
     * For example: "c:\\windows\\file.txt.bak" will give "file"
     *
     * @param path path
     * @return string
     */
    public static String getSimpleName(Path path) {
        String name = path.getFileName().toString();
        int index = name.indexOf(".");
        return index<0 ? name : name.substring(0, index);
    }

    /**
     * Returns normalized absolute path.
     * It uses filesystem calls to compute both absolute path and normalization.
     *
     * @param path path
     * @return Path
     */
    public static Path normalize(Path path) {
        return path.toAbsolutePath().normalize();
    }

    /**
     * Returns normalized absolute path.
     * It uses filesystem calls to compute both absolute path and normalization.
     *
     * @param path path
     * @return Path
     */
    public static String normalize(String path) {
        return normalize(Paths.get(path)).toString();
    }

    /**
     * Returns CQuery with all files inside directory.
     * Returns both subdirectories and ordinary files.
     * It is not recursive.
     *
     * The Path objects are obtained as if by resolving the name of the directory entry against dir.
     *
     * @param dir dir
     * @return query
     */
    public static CQuery<Path> list(Path dir) {
        if(RuntimeConfiguration.FIX_FILES_WALKER()) {
            return listJDK7(dir);
        } else {
            return listJDK8(dir);
        }
    }

    private static CQuery<Path> listJDK7(Path dir) {
        return CQuery.from().iterable(() -> {
            try {
                return IterableUtils.safe(Files.newDirectoryStream(dir));
            } catch (IOException e) {
                return Collections.emptyList();
            }
        });
    }

    private static CQuery<Path> listJDK8(Path dir) {
        return CQuery.from().stream(() -> Files.list(dir));
    }

    /**
     * Returns CQuery with all files inside directory matching provided wildcard
     * Returns both subdirectories and ordinary files.
     * It is not recursive.
     *
     * The Path objects are obtained as if by resolving the name of the directory entry against dir.
     *
     * @param dir dir
     * @param wildcard wildcard
     * @return query
     */
    public static CQuery<Path> list(Path dir, String wildcard) {
        return list(dir).filter(Wildcard.compile(wildcard).asPathFilter());
    }

    /**
     * Returns CQuery with all files inside directory.
     * Returns both subdirectories and ordinary files.
     * It is recursive.
     * Recursively visited directories are returned too.
     *
     * The Path objects are obtained as if by resolving the name of the directory entry against dir.
     *
     * @param dir dir
     * @return query
     */
    public static CQuery<Path> walk(Path dir) {
        return list(dir).flatIf(Files::isDirectory, file -> {
            return CQuery.builder().with(file).withQuery(PathUtils.walk(file)).build();
        });
    }

    /**
     * Returns CQuery with all files inside directory matching provided wildcard
     * Returns both subdirectories and ordinary files.
     * It is recursive.
     * Recursively visited directories are returned too.
     *
     * The Path objects are obtained as if by resolving the name of the directory entry against dir.
     *
     * @param dir dir
     * @param wildcard wildcard
     * @return query
     */
    public static CQuery<Path> walk(Path dir, String wildcard) {
        return PathUtils.walk(dir).filter(Wildcard.compile(wildcard).asPathFilter());
    }

    /**
     * Returns current working directory of application
     *
     * @return path
     */
    public static Path cwd() {
        return PathUtils.normalize(Paths.get("."));
    }

    /**
     * Returns common path part of two specified paths.
     * It means: path to directory which contains both arguments.
     * Returns empty path if there is no common root.
     *
     * @param a a
     * @param b b
     * @return path
     */
    public static Path common(Path a, Path b) {
        return common0(a,b);
    }


    /**
     * Returns common path part of all specified paths.
     * It means: path to directory which contains all paths.
     * Returns empty path if there is no common root.
     *
     * @param values values
     * @return path
     */
    public static Path common(Collection<Path> values) {
        if(values.isEmpty()) {
            return cwd();
        }
        return values.stream().reduce(values.iterator().next(), PathUtils::common0);
    }

    private static Path common0(Path a, Path b) {
        Path na = a.normalize();
        Path nb = b.normalize();

        if(na.isAbsolute() ^ nb.isAbsolute()) {
            return Paths.get("");
        }
        if(na.isAbsolute() && !na.getRoot().equals(nb.getRoot())) {
            return Paths.get("");
        }

        int i = 0;
        int n = Math.min(na.getNameCount(), nb.getNameCount());

        while(i<n && na.getName(i).equals(nb.getName(i))) {
            i++;
        }

        if(i == 0) {
            return Paths.get("");
        }

        if(na.isAbsolute()) {
            return na.getRoot().resolve(na.subpath(0,i));
        } else {
            return na.subpath(0, i);
        }
    }

}
