package jenkins.plugins.git;

import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.EnvVars;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.model.TaskListener;
import hudson.plugins.git.GitTool;
import hudson.plugins.git.util.GitUtils;
import jenkins.model.Jenkins;
import org.apache.commons.io.FileUtils;
import org.jenkinsci.plugins.gitclient.Git;
import org.jenkinsci.plugins.gitclient.GitClient;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

/**
 * A class which allows Git Plugin to choose a git implementation by estimating the size of a repository from a distance
 * without requiring a local checkout.
 */
public class GitToolChooser {

    private long sizeOfRepo = 0L;
    private String implementation;
    private String gitTool;
    /**
     * Size to switch implementation in KiB
     */
    public static final int SIZE_TO_SWITCH = 5000;

    /**
     * Instantiate class using {@link AbstractGitSCMSource}. It looks for a cached .git directory first, calculates the
     * size if it is found else checks if the extension point has been implemented and asks for the size.
     * @param source the {@link AbstractGitSCMSource}
     * @throws IOException
     * @throws InterruptedException
     */
    public GitToolChooser(@NonNull AbstractGitSCMSource source) throws IOException, InterruptedException {
        boolean useCache;
        boolean useAPI = false;

        implementation = determineSwitchOnSize(sizeOfRepo);
        useCache = setSizeFromCache(source);

        if (useCache) {
            implementation = determineSwitchOnSize(sizeOfRepo);
        } else {
            useAPI = setSizeFromAPI(source.getRemote());
        }

        if (useAPI) {
            implementation = determineSwitchOnSize(sizeOfRepo);
        }
        determineGitTool(implementation);
    }

    /**
     * Estimate size of a repository using the extension point
     * @param remoteName: The URL of the repository
     */
    public GitToolChooser(String remoteName) {
        implementation = determineSwitchOnSize(sizeOfRepo);

        boolean useAPI = setSizeFromAPI(remoteName);
        if (useAPI) {
            implementation = determineSwitchOnSize(sizeOfRepo);
        }
        determineGitTool(implementation);
    }

    /**
     * Determine and estimate the size of a .git cached directory
     * @param source: Use a {@link AbstractGitSCMSource} to access a cached Jenkins directory, we do not lock it.
     * @return useCache
     * @throws IOException
     * @throws InterruptedException
     */
    private boolean setSizeFromCache(@NonNull AbstractGitSCMSource source) throws IOException, InterruptedException {
        boolean useCache = false;
        String cacheEntry = source.getCacheEntry();
        File cacheDir = AbstractGitSCMSource.getCacheDir(cacheEntry);
        if (cacheDir != null) {
            Git git = Git.with(TaskListener.NULL, new EnvVars(EnvVars.masterEnvVars)).in(cacheDir).using("git");
            GitClient client = git.getClient();
            if (client.hasGitRepo()) {
                sizeOfRepo = FileUtils.sizeOfDirectory(cacheDir);
                sizeOfRepo = (sizeOfRepo/1000); // Conversion from Bytes to Kilo Bytes
                useCache = true;
            }
        }
        return useCache;
    }

    /**
     * Check if the desired implementation of extension is present and ask for the size of repository if it does
     * @param repoUrl: The remote name derived from {@link GitSCMSource} object
     * @return boolean useAPI or not.
     */
    private boolean setSizeFromAPI(String repoUrl) {
        List<RepositorySizeAPI> acceptedRepository = Objects.requireNonNull(RepositorySizeAPI.all())
                .stream()
                .filter(r -> r.isApplicableTo(repoUrl))
                .collect(Collectors.toList());

        if (acceptedRepository.size() > 0) {
            try {
                sizeOfRepo = acceptedRepository.get(0).getSizeOfRepository(repoUrl);
            } catch (Exception e) {
                LOGGER.log(Level.INFO, "Not using size API estimation based performance improvement");
                return false;
            }
            return sizeOfRepo != 0; // Check if the size of the repository is zero
        } else {
            return false;
        }
    }

    /**
     * Recommend a git implementation on the basis of the given size of a repository
     * @param sizeOfRepo: Size of a repository (in KiBs)
     * @return a git implementation, "git" or "jgit"
     */
    private String determineSwitchOnSize(Long sizeOfRepo) {
        if (sizeOfRepo != 0L) {
            if (sizeOfRepo >= SIZE_TO_SWITCH) {
                return "git";
            } else {
                return "jgit";
            }
        }
        return "NONE";
    }

    /**
     * For a given recommended git implementation, validate if the installation exists and provide no suggestion if
     * implementation doesn't exist.
     * @param gitImplementation: The recommended git implementation, "git" or "jgit" on the basis of the heuristics.
     */
    private void determineGitTool(String gitImplementation) {
        if (gitImplementation.equals("NONE")) {
            gitTool = "NONE";
            return; // Recommend nothing (GitToolRecommendation = NONE)
        }
        final Jenkins jenkins = Jenkins.get();
        GitTool tool = GitUtils.resolveGitTool(gitImplementation, jenkins, null, TaskListener.NULL);
        if (tool != null) {
            gitTool = tool.getGitExe();
        }
    }

    /**
     * Recommend git tool to be used by the git client
     * @return git implementation recommendation in the form of a string
     */
    public String getGitTool() {
        return gitTool;
    }

    /**
     * Other plugins can estimate the size of repository using this extension point
     * The size is assumed to be in KiBs
     */
    public static abstract class RepositorySizeAPI implements ExtensionPoint {

        public abstract boolean isApplicableTo(String remote);

        public abstract Long getSizeOfRepository(String remote) throws Exception;

        public static ExtensionList<RepositorySizeAPI> all() {
            return Jenkins.get().getExtensionList(RepositorySizeAPI.class);
        }
    }

    private static final Logger LOGGER = Logger.getLogger(GitToolChooser.class.getName());
}

@@ -1530,32 +1530,8 @@
 ache
-;%0A        boolean useAPI
  = f
@@ -1566,41 +1566,14 @@
 n = 
-determineSwitchOnSize(sizeOfRepo)
+%22NONE%22
 ;%0A  
@@ -1589,27 +1589,28 @@
 Cache = 
-setSizeFrom
+decideAndUse
 Cache(so
@@ -1734,36 +1734,28 @@
         
-useAPI = setSizeFrom
+decideAndUse
 API(sour
@@ -1784,105 +1784,8 @@
    %7D
-%0A%0A        if (useAPI) %7B%0A            implementation = determineSwitchOnSize(sizeOfRepo);%0A        %7D
 %0A   
@@ -2068,25 +2068,24 @@
 fRepo);%0A
-%0A
         
 boolean 
@@ -2080,148 +2080,36 @@
     
-boolean useAPI = setSizeFromAPI(remoteName);%0A        if (useAPI) %7B%0A            implementation = determineSwitchOnSize(sizeOfRepo);%0A        %7D
+decideAndUseAPI(remoteName);
 %0A   
@@ -2457,27 +2457,28 @@
 boolean 
-setSizeFrom
+decideAndUse
 Cache(@N
@@ -3178,32 +3178,209 @@
 seCache;%0A    %7D%0A%0A
+    private void decideAndUseAPI(String remoteName) %7B%0A        if (setSizeFromAPI(remoteName)) %7B%0A            implementation = determineSwitchOnSize(sizeOfRepo);%0A        %7D%0A    %7D%0A%0A
     /**%0A     * C

Remove duplicate code ->