package com.atlassian.bitbucket.mesh.git;

import com.atlassian.bitbucket.mesh.io.MoreFiles;
import de.schlichtherle.truezip.file.TArchiveDetector;
import de.schlichtherle.truezip.file.TFile;
import org.apache.commons.io.FilenameUtils;
import org.springframework.core.io.Resource;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;

/**
 * Provides constants for various Git test repositories.
 * <p>
 * These repositories are made available by Maven and the {@code maven-dependency-plugin}. To add new repositories,
 * add a new {@code &lt;type&gt;zip&lt;/type&gt;} dependency in the {@code pom.xml}, as well as a constant here for
 * its name (which will match the {@code &lt;artifactId/&gt;} for the dependency).
 */
public class GitRepositories {

    public static final String PREFIX = "classpath:git/";
    public static final String SUFFIX = ".zip";

    public static final String BINARY_FILES = PREFIX + "binary-files-repository" + SUFFIX;
    public static final String COMMENT_DRIFT = PREFIX + "comment-drift" + SUFFIX;
    public static final String DEFAULT = PREFIX + "default-repository" + SUFFIX;
    public static final String DIFF_TREE = PREFIX + "diff-tree" + SUFFIX;
    public static final String GARBAGE = PREFIX + "garbage" + SUFFIX;
    public static final String MERGE = PREFIX + "merge" + SUFFIX;
    public static final String MERGE_COMMITS = PREFIX + "merge-commits" + SUFFIX;
    public static final String PULL_REQUESTS = PREFIX + "pull-requests" + SUFFIX;
    public static final String QA_RESOURCES = PREFIX + "qa-resources" + SUFFIX;
    public static final String SUBREPOSITORIES = PREFIX + "subrepositories" + SUFFIX;

    private static final Path PATH_CONFIG = Paths.get("config");
    private static final Path PATH_HOOKS = Paths.get("hooks");
    private static final Path PATH_INFO = Paths.get("info");
    private static final Path PATH_OBJECTS = Paths.get("objects");

    private GitRepositories() {
        throw new UnsupportedOperationException("GitRepositories is a utility class and cannot be instantiated");
    }

    public static void copyFromArchive(@Nonnull Resource archive, @Nonnull Path repositoryDir, @Nonnull Path tempDir) {
        Path archiveTmp = stageArchive(archive, tempDir);

        try {
            //The tests (re)create repositories a lot. The following file tree walk is optimized for speed.
            //It prefers hard links over copies for immutable files such as loose objects and packs. For the remaining
            //files, faster regular copies are used instead of the NFS safe create-tmp-file-and-move semantics used by
            //MoreFiles.syncFileTree.
            Files.walkFileTree(archiveTmp, new SimpleFileVisitor<Path>() {

                boolean inObjectsDir;

                @Override
                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
                    Path relPath = archiveTmp.relativize(dir);

                    if (relPath.getNameCount() == 1) {
                        if (PATH_HOOKS.equals(relPath.getName(0))) {
                            return FileVisitResult.SKIP_SUBTREE;
                        }
                        inObjectsDir = PATH_OBJECTS.equals(relPath.getName(0));
                    }

                    MoreFiles.mkdir(repositoryDir.resolve(relPath));
                    if (inObjectsDir) {
                        //Skip objects/info because there's nothing of interest that needs to be restored
                        return relPath.getNameCount() == 2 && PATH_INFO.equals(relPath.getName(1)) ?
                                FileVisitResult.SKIP_SUBTREE : FileVisitResult.CONTINUE;

                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    if (PATH_OBJECTS.equals(archiveTmp.relativize(dir))) {
                        inObjectsDir = false;
                    }
                    return super.postVisitDirectory(dir, exc);
                }

                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    Path relPath = archiveTmp.relativize(file);
                    if (PATH_CONFIG.equals(relPath)) {
                        return FileVisitResult.CONTINUE;
                    }

                    Path destination = repositoryDir.resolve(relPath);
                    if (inObjectsDir) {
                        MoreFiles.createHardLinkOrCopy(destination, file, false);
                    } else {
                        Files.copy(file, destination, COPY_ATTRIBUTES, REPLACE_EXISTING);
                    }
                    return FileVisitResult.CONTINUE;
                }
            });
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to copy archive contents", e);
        }
    }

    @Nonnull
    public static Path stageArchive(@Nonnull Resource archive, @Nonnull Path tempDir) {
        String name = FilenameUtils.getBaseName(archive.getFilename());

        Path archiveTmp = MoreFiles.mkdir(tempDir, "git-test-repos").resolve(MoreFiles.pathSafe(name));
        if (!Files.isDirectory(archiveTmp)) {
            unpackArchive(archive, archiveTmp);
        }

        return archiveTmp;
    }

    public static void unpackArchive(@Nonnull Resource archive, @Nonnull Path repositoryDir) {
        try {
            TFile.cp_r(new TFile(archive.getFile()), repositoryDir.toFile(), TArchiveDetector.ALL);
        } catch (IOException e) {
            throw new UncheckedIOException("Failed to unpack " + archive, e);
        }
    }
}
