package org.jenkinsci.plugins.sma;

import org.eclipse.jgit.api.DiffCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.TreeWalk;

import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

/**
 * Wrapper for git interactions using jGit.
 *
 */
public class SMAGit
{
    public enum Mode { STD, INI, PRB }

    private final String SOURCEDIR = "src/";

    private Git git;
    private Repository repository;
    private List<DiffEntry> diffs;
    private String prevCommit, curCommit;

    private static final Logger LOG = Logger.getLogger(SMAGit.class.getName());

    /**
     * Creates an SMAGit instance
     *
     * @param pathToWorkspace
     * @param curCommit
     * @param diffAgainst
     * @param smaMode
     * @throws Exception
     */
    public SMAGit(String pathToWorkspace,
                  String curCommit,
                  String diffAgainst,
                  Mode smaMode) throws Exception
    {
        String pathToRepo = pathToWorkspace + "/.git";
        File repoDir = new File(pathToRepo);
        FileRepositoryBuilder builder = new FileRepositoryBuilder();
        repository = builder.setGitDir(repoDir).readEnvironment().build();
        git = new Git(repository);
        this.curCommit = curCommit;

        if (smaMode == Mode.PRB)
        {
            ObjectId branchId = repository.resolve("refs/remotes/origin/" + diffAgainst);
            RevCommit targetCommit = new RevWalk(repository).parseCommit(branchId);

            this.prevCommit = targetCommit.getName();
        }
        else if (smaMode == Mode.STD)
        {
            this.prevCommit = diffAgainst;
        }

        if (smaMode != Mode.INI)
        {
            getDiffs();
        }
    }

    /**
     * Returns all of the items that were added in the current commit.
     *
     * @return The ArrayList containing all of the additions in the current commit.
     * @throws IOException
     */
    public Map<String, byte[]> getNewMetadata() throws Exception
    {
        Map<String, byte[]> additions = new HashMap<String, byte[]>();

        for (DiffEntry diff : diffs)
        {
            if (diff.getChangeType().toString().equals("ADD"))
            {
                String item = SMAUtility.checkMeta(diff.getNewPath());
                if (!additions.containsKey(item) && item.contains(SOURCEDIR))
                {
                    additions.put(diff.getNewPath(), getBlob(diff.getNewPath(), curCommit));
                }
            }
        }

        return additions;
    }

    /**
     * Returns all of the items that were deleted in the current commit.
     *
     * @return The ArrayList containing all of the items that were deleted in the current commit.
     */
    public Map<String, byte[]> getDeletedMetadata() throws Exception
    {
        Map<String, byte[]> deletions = new HashMap<String, byte[]>();

        for (DiffEntry diff : diffs)
        {
            if (diff.getChangeType().toString().equals("DELETE"))
            {
                String item = SMAUtility.checkMeta(diff.getOldPath());
                if (!deletions.containsKey(item) && item.contains(SOURCEDIR))
                {
                    deletions.put(diff.getOldPath(), getBlob(diff.getOldPath(), prevCommit));
                }
            }
        }

        return deletions;
    }

    /**
     * Returns all of the updated changes in the current commit.
     *
     * @return The ArrayList containing the items that were modified (new paths) and added to the repository.
     * @throws IOException
     */
    public Map<String, byte[]> getUpdatedMetadata() throws Exception
    {
        Map<String, byte[]> modifiedMetadata = new HashMap<String, byte[]>();

        for (DiffEntry diff : diffs)
        {
            if (diff.getChangeType().toString().equals("MODIFY"))
            {
                String item = SMAUtility.checkMeta(diff.getNewPath());
                if (!modifiedMetadata.containsKey(item) && item.contains(SOURCEDIR))
                {
                    modifiedMetadata.put(diff.getNewPath(), getBlob(diff.getNewPath(), curCommit));
                }
            }
        }
        return modifiedMetadata;
    }

    /**
     * Returns all of the modified (old paths) changes in the current commit.
     *
     * @return ArrayList containing the items that were modified (old paths).
     */
    public Map<String, byte[]> getOriginalMetadata() throws Exception
    {
        Map<String, byte[]> originalMetadata = new HashMap<String, byte[]>();

        for (DiffEntry diff : diffs)
        {
            if (diff.getChangeType().toString().equals("MODIFY"))
            {
                String item = SMAUtility.checkMeta(diff.getOldPath());
                if (!originalMetadata.containsKey(item) && item.contains(SOURCEDIR))
                {
                    originalMetadata.put(diff.getOldPath(), getBlob(diff.getOldPath(), prevCommit));
                }
            }
        }

        return originalMetadata;
    }

    /**
     * Returns the blob information for the file at the specified path and commit
     *
     * @param repoItem
     * @param commit
     * @return
     * @throws Exception
     */
    public byte[] getBlob(String repoItem, String commit) throws Exception
    {
        byte[] data;

        String parentPath = repository.getDirectory().getParent();

        ObjectId commitId = repository.resolve(commit);

        ObjectReader reader = repository.newObjectReader();
        RevWalk revWalk = new RevWalk(reader);
        RevCommit revCommit = revWalk.parseCommit(commitId);
        RevTree tree = revCommit.getTree();
        TreeWalk treeWalk = TreeWalk.forPath(reader, repoItem, tree);

        if (treeWalk != null)
        {
            data = reader.open(treeWalk.getObjectId(0)).getBytes();
        }
        else
        {
            throw new IllegalStateException("Did not find expected file '" + repoItem + "'");
        }

        reader.release();

        return data;
    }

    /**
     * Replicates ls-tree for the current commit.
     *
     * @return Map containing the full path and the data for all items in the repository.
     * @throws IOException
     */
    public Map<String, byte[]> getAllMetadata() throws Exception
    {
        Map<String, byte[]> contents = new HashMap<String, byte[]>();
        ObjectReader reader = repository.newObjectReader();
        ObjectId commitId = repository.resolve(curCommit);
        RevWalk revWalk = new RevWalk(reader);
        RevCommit commit = revWalk.parseCommit(commitId);
        RevTree tree = commit.getTree();
        TreeWalk treeWalk = new TreeWalk(reader);
        treeWalk.addTree(tree);
        treeWalk.setRecursive(false);

        while (treeWalk.next())
        {
            if (treeWalk.isSubtree())
            {
                treeWalk.enterSubtree();
            }
            else
            {
                String member = treeWalk.getPathString();
                if (member.contains(SOURCEDIR))
                {
                    byte[] data = getBlob(member, curCommit);
                    contents.put(member, data);
                }
            }
        }

        reader.release();

        return contents;
    }

    /**
     * Creates an updated package.xml file and commits it to the repository
     *
     * @param workspace The workspace.
     * @param userName  The user name of the committer.
     * @param userEmail The email of the committer.
     * @param manifest  The SMAPackage representation of a package manifest
     * @return A boolean value indicating whether an update was required or not.
     * @throws Exception
     */
    public boolean updatePackageXML(String workspace,
                                    String userName,
                                    String userEmail,
                                    SMAPackage manifest) throws Exception
    {
        File packageXml;

        // Only need to update the manifest if we have additions or deletions
        if (!getNewMetadata().isEmpty() || !getDeletedMetadata().isEmpty())
        {
            // Fine the existing package.xml file in the repository
            String packageLocation = SMAUtility.findPackage(new File(workspace));

            if (!packageLocation.isEmpty())
            {
                packageXml = new File(packageLocation);
            }
            else
            {
                // We couldn't find one, so just create one.
                packageXml = new File(workspace + "/unpackaged/package.xml");
                packageXml.getParentFile().mkdirs();
                packageXml.createNewFile();
            }

            // Write the manifest to the location of the package.xml in the fs
            FileOutputStream fos = new FileOutputStream(packageXml, false);
            fos.write(manifest.getPackage().getBytes());
            fos.close();

            String path = packageXml.getPath();

            // Commit the updated package.xml file to the repository
            git.add().addFilepattern(path).call();
            git.commit().setCommitter(userName, userEmail).setMessage("Jenkins updated package.xml").call();

            return true;
        }

        return false;
    }

    public Git getRepo()
    {
        return git;
    }

    public String getPrevCommit()
    {
        return prevCommit;
    }

    public String getCurCommit()
    {
        return curCommit;
    }

    /**
     * Returns the diff between two commits.
     *
     * @return List that contains DiffEntry objects of the changes made between the previous and current commits.
     * @throws Exception
     */
    private void getDiffs() throws Exception
    {
        OutputStream out = new ByteArrayOutputStream();
        CanonicalTreeParser oldTree = getTree(prevCommit);
        CanonicalTreeParser newTree = getTree(curCommit);
        DiffCommand diff = git.diff().setOutputStream(out).setOldTree(oldTree).setNewTree(newTree);
        diffs = diff.call();
    }

    /**
     * Returns the Canonical Tree Parser representation of a commit.
     *
     * @param commit Commit in the repository.
     * @return CanonicalTreeParser representing the tree for the commit.
     * @throws IOException
     */
    private CanonicalTreeParser getTree(String commit) throws IOException
    {
        CanonicalTreeParser tree = new CanonicalTreeParser();
        ObjectReader reader = repository.newObjectReader();
        ObjectId head = repository.resolve(commit + "^{tree}");
        tree.reset(reader, head);
        return tree;
    }
}