package com.atlassian.bitbucket.mesh.junit;

import org.junit.rules.ExternalResource;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

/**
 * Similar to JUnit's {@code TemporaryFolder} rule, except built around {@link Path} instead of {@link File}.
 * <p>
 * As the system switches more completely from {@code File} to {@code Path}, more and more tests will likely
 * need to switch as well. The public methods on this class are patterned after {@code TemporaryFolder} to
 * allow using this as a drop-in replacement in most circumstances.
 */
public class TemporaryDirectory extends ExternalResource {

    private final Path parentDir;

    private Path dir;

    public TemporaryDirectory() {
        this(null);
    }

    public TemporaryDirectory(@Nullable Path parentDir) {
        this.parentDir = parentDir;
    }

    /**
     * Creates the temporary directory, to allow using this class directly rather than as a {@code @Rule}. When
     * such usage is complete, the caller should use {@link #delete} to clean up the temporary directory.
     *
     * @throws IOException if the temporary directory cannot be created
     */
    public void create() throws IOException {
        before();
    }

    /**
     * Deletes the temporary directory, and any directories/files it contains. This should only need to be called
     * when using {@link #create}; when using as a {@code @Rule}, deletion is handled by JUnit.
     */
    public void delete() {
        after();
    }

    /**
     * @return the location of the temporary path
     * @throws IllegalStateException if {@link #before} has not been called yet
     */
    @Nonnull
    public Path getRoot() {
        if (dir == null) {
            throw new IllegalStateException("The temporary folder has not yet been created");
        }
        return dir;
    }

    /**
     * @return a new, empty file with a randomly-generated name
     * @throws IOException if a new file cannot be created
     */
    @Nonnull
    public Path newFile() throws IOException {
        return Files.createTempFile(getRoot(), "junit", null);
    }

    /**
     * @param fileName the file to create
     * @return a new, empty file with the specified name
     * @throws IOException if a new file cannot be created
     */
    @Nonnull
    public Path newFile(@Nonnull String fileName) throws IOException {
        return Files.createFile(getRoot().resolve(fileName));
    }

    /**
     * @return a new, empty folder with a randomly-generated name
     * @throws IOException if a new folder cannot be created
     */
    @Nonnull
    public Path newFolder() throws IOException {
        return Files.createTempDirectory(getRoot(), "junit");
    }

    /**
     * @param folderName the folder to create
     * @return a new, empty folder with the specified name
     * @throws IOException if a new folder cannot be created
     */
    @Nonnull
    public Path newFolder(@Nonnull String folderName) throws IOException {
        return Files.createDirectory(getRoot().resolve(folderName));
    }

    /**
     * Creates a series of folders, of which only the <i>final folder</i> is required to be new. If no folder
     * names are specified, the {@link #getRoot root folder} is returned.
     * <p>
     * <b>Note</b>: Unlike {@code TemporaryFolder.newFolder(String...)}, this implementation does <i>not</i>
     * enforce the restriction that individual names cannot define multiple "layers" of path. So, for example,
     * where {@code newFolder("a/b/c", "d", "e/f")} would throw an exception with {@code TemporaryFolder},
     * here it will return a new folder at "a/b/c/d/e/f" and will only throw if a directory can't be created.
     *
     * @param folderNames zero or more folder names to create
     * @return a new, empty folder at the assembled path of all the provided names
     * @throws IOException if a new folder cannot be created
     */
    @Nonnull
    public Path newFolder(@Nonnull String... folderNames) throws IOException {
        Path file = getRoot();
        if (folderNames.length == 0) {
            return file;
        }
        if (folderNames.length == 1) {
            return newFolder(folderNames[0]);
        }

        String first = folderNames[0];
        String[] rest = new String[folderNames.length - 1];
        System.arraycopy(folderNames, 1, rest, 0, rest.length);

        file = file.resolve(Paths.get(first, rest));
        //Create all of the parent directories, allowing for any that already exist
        Files.createDirectories(file.getParent());

        //Require the final directory to be _newly created_. If it already exists, throw an exception
        return Files.createDirectory(file);
    }

    /**
     * Recursively deletes the temporary path and all files/folders it contains.
     */
    @Override
    protected void after() {
        if (dir != null) {
            try {
                Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {

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

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

                    private void deleteIfExists(Path path) throws IOException {
                        try {
                            Files.delete(path);
                        } catch (FileNotFoundException | NoSuchFileException e) {
                            //All good
                        }
                    }
                });
            } catch (IOException e) {
                //Ignored
            }
        }
    }

    /**
     * Creates a new temporary directory, either nested in the parent directory provided when the rule was constructed,
     * or in the {@code java.io.tmpdir} configured when the JVM was started.
     *
     * @throws IOException if the temporary directory cannot be created, or if it cannot be converted to a real
     *                     path afterward
     */
    @Override
    protected void before() throws IOException {
        dir = parentDir == null ?
                Files.createTempDirectory("junit") :           //Use the default JVM temp directory
                Files.createTempDirectory(parentDir, "junit"); //Use the specified parent folder
        dir = dir.toRealPath();
    }
}
