package org.netbeans.gradle.project.util;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collection;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jtrim2.cancel.CancellationToken;
import org.jtrim2.event.ListenerRef;
import org.jtrim2.utils.ExceptionHelper;
import org.netbeans.api.project.Project;
import org.netbeans.gradle.project.others.ChangeLFPlugin;
import org.openide.filesystems.FileChangeAdapter;
import org.openide.filesystems.FileChangeListener;
import org.openide.filesystems.FileEvent;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;

public final class NbFileUtils {
    private static final Logger LOGGER = Logger.getLogger(NbFileUtils.class.getName());

    private static final int FILE_BUFFER_SIZE = 8 * 1024;

    private static boolean isLineEndingByte(byte ch) {
        return ch == 13 || ch == 10;
    }

    public static String tryGetLineSeparatorForTextFile(Path file) {
        if (!Files.isRegularFile(file)) {
            return null;
        }

        // This method won't work with every encoding but we save properties
        // file with UTF-8, with which this should be fine.
        byte prevChar = 0;
        byte[] buffer = new byte[FILE_BUFFER_SIZE];
        try (InputStream input = Files.newInputStream(file)) {
            int readCount = input.read(buffer);
            while (readCount > 0) {
                for (int i = 0; i < readCount; i++) {
                    byte ch = buffer[i];
                    if (isLineEndingByte(prevChar)) {
                        switch (ch) {
                            case 10:
                                return prevChar == 13 ? "\r\n" : "\n";
                            case 13:
                                // \n\r is not valid, returning null is safer.
                                return prevChar == 10 ? null : "\r";
                            default:
                                return Character.toString((char)prevChar);
                        }
                    }

                    prevChar = ch;
                }

                readCount = input.read(buffer);
            }

            return isLineEndingByte(prevChar) ? Character.toString((char)prevChar) : null;
        } catch (IOException ex) {
            LOGGER.log(Level.INFO, "Failed to read config file for determining its line separator", ex);
            return null;
        }
    }

    public static String tryMakeRelative(File parent, File file) {
        File normParent = FileUtil.normalizeFile(parent);
        File normFile = FileUtil.normalizeFile(file);

        if (normParent == null || normFile == null) {
            return null;
        }

        FileObject parentObj = FileUtil.toFileObject(normParent);
        FileObject fileObj = FileUtil.toFileObject(normFile);

        if (fileObj == null || parentObj == null) {
            return null;
        }

        String relPath = FileUtil.getRelativePath(parentObj, fileObj);
        return relPath != null ? relPath.replace("/", File.separator) : null;
    }

    private static boolean isSafeChar(char ch) {
        if (ch >= 'A' && ch <= 'Z') return true;
        if (ch >= 'a' && ch <= 'z') return true;
        if (ch >= '0' && ch <= '9') return true;

        return "_-$. ".indexOf(ch) >= 0;
    }

    public static String toSafeFileName(String name) {
        Objects.requireNonNull(name, "name");

        StringBuilder result = new StringBuilder(name.length());
        for (int i = 0; i < name.length(); i++) {
            char ch = name.charAt(i);
            result.append(isSafeChar(ch) ? ch : "_");
        }
        return result.toString();
    }

    public static ListenerRef addDirectoryContentListener(
            final FileObject dir,
            final Runnable listener) {
        return addDirectoryContentListener(dir, false, listener);
    }

    public static ListenerRef addDirectoryContentListener(
            final FileObject dir,
            final boolean listenForDirs,
            final Runnable listener) {
        Objects.requireNonNull(dir, "dir");
        Objects.requireNonNull(listener, "listener");

        final FileChangeListener fileChangeListener = new FileChangeAdapter() {
            @Override
            public void fileFolderCreated(FileEvent fe) {
                if (listenForDirs) {
                    listener.run();
                }

            }
            @Override
            public void fileDeleted(FileEvent fe) {
                listener.run();
            }

            @Override
            public void fileDataCreated(FileEvent fe) {
                listener.run();
            }
        };
        dir.addFileChangeListener(fileChangeListener);
        return EventUtils.asSafeListenerRef(() -> {
            dir.removeFileChangeListener(fileChangeListener);
        });
    }

    public static URL getSafeURL(String url) {
        try {
            return new URL(url);
        } catch (MalformedURLException ex) {
            throw new RuntimeException(ex);
        }
    }

    public static String getFileNameStr(Path path) {
        Path result = path.getFileName();
        return result != null ? result.toString() : "";
    }

    public static FileObject asArchiveOrDir(File file) {
        FileObject result = FileUtil.toFileObject(file);
        return asArchiveOrDir(result);
    }

    public static FileObject asArchiveOrDir(FileObject file) {
        if (file == null) {
            return null;
        }

        if (FileUtil.isArchiveFile(file)) {
            return FileUtil.getArchiveRoot(file);
        }
        else {
            return file;
        }
    }

    public static FileObject getFileFromASubDir(FileObject root, String fileName) {
        File rootDir = FileUtil.toFile(root);
        if (rootDir == null) {
            return null;
        }

        File[] subDirs = rootDir.listFiles(File::isDirectory);
        if (subDirs == null) {
            return null;
        }

        for (File dir: subDirs) {
            File srcFileName = new File(dir, fileName);
            if (srcFileName.isFile()) {
                return asArchiveOrDir(FileUtil.toFileObject(srcFileName));
            }
        }
        return null;
    }

    public static Path asPath(FileObject fileObj) {
        return fileObj != null ? asPath(FileUtil.toFile(fileObj)) : null;
    }

    public static Path asPath(File file) {
        try {
            return file != null ? file.toPath() : null;
        } catch (InvalidPathException ex) {
            return null;
        }
    }

    public static File asFile(FileObject fileObj) {
        return fileObj != null ? FileUtil.toFile(fileObj) : null;
    }

    public static FileObject asFileObject(File file) {
        return file != null ? FileUtil.toFileObject(file) : null;
    }

    public static boolean isParentOrSame(File parent, File child) {
        for (File current = child; current != null; current = current.getParentFile()) {
            if (current.equals(parent)) {
                return true;
            }
        }
        return false;
    }

    public static void deleteDirectory(final CancellationToken cancelToken, Path directory) throws IOException {
        Objects.requireNonNull(cancelToken, "cancelToken");
        Objects.requireNonNull(directory, "directory");

        Files.walkFileTree(directory, new FileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                cancelToken.checkCanceled();
                return FileVisitResult.CONTINUE;
            }

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

            @Override
            public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
                cancelToken.checkCanceled();
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                cancelToken.checkCanceled();
                Files.deleteIfExists(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    public static void deleteDirectory(CancellationToken cancelToken, FileObject directory) throws IOException {
        Objects.requireNonNull(cancelToken, "cancelToken");
        Objects.requireNonNull(directory, "directory");

        File asFile = FileUtil.toFile(directory);
        if (asFile == null) {
            throw new FileNotFoundException("Cannot find " + directory);
        }

        deleteDirectory(cancelToken, asFile.toPath());
    }

    public static void writeLinesToFile(
            Path file,
            Collection<String> lines,
            Charset charset) throws IOException {
        writeLinesToFile(file, lines, charset, (Project)null);
    }

    public static void writeLinesToFile(
            Path file,
            Collection<String> lines,
            Charset charset,
            Project ownerProject) throws IOException {
        String lineSeparator = ChangeLFPlugin.getPreferredLineSeparator(ownerProject);
        writeLinesToFile(file, lines, charset, lineSeparator);
    }

    public static void writeLinesToFile(
            Path file,
            Collection<String> lines,
            Charset charset,
            String lineSeparator) throws IOException {
        Objects.requireNonNull(file, "file");
        Objects.requireNonNull(charset, "charset");
        ExceptionHelper.checkNotNullElements(lines, "lines");

        if (lineSeparator == null) {
            Files.write(file, lines, charset);
        }
        else {
            byte[] lineSeparatorBytes = lineSeparator.getBytes(charset);
            try (OutputStream fileOutput = Files.newOutputStream(file);
                    OutputStream output = new BufferedOutputStream(fileOutput, 16 * 1024)) {
                for (String line: lines) {
                    output.write(line.getBytes(charset));
                    output.write(lineSeparatorBytes);
                }
            }
        }
    }

    public static Path toSafeRealPath(Path src) {
        try {
            return src.toRealPath();
        } catch (IOException ex) {
            return src.normalize();
        }
    }

    private NbFileUtils() {
        throw new AssertionError();
    }
}
