package com.hdweiss.morgand.synchronizer.git;

import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;

import com.hdweiss.morgand.utils.FileUtils;
import com.hdweiss.morgand.utils.Utils;

import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.URIish;

import java.io.File;
import java.net.URISyntaxException;

public class JGitWrapper {

    private Git git;

    private final String localPath;
    private final String remotePath;

    private final String branch;

    private final String commitAuthor;
    private final String commitEmail;

    private CredentialsProvider credentialsProvider;

    private MergeStrategy mergeStrategy;

    // TODO Externalize strings
    public JGitWrapper(SharedPreferences preferences) throws Exception {
        localPath = preferences.getString("git_local_path", "");
        if (TextUtils.isEmpty(localPath))
            throw new IllegalArgumentException("Must specify local git path");

        String url = preferences.getString("git_url", "");
        if (TextUtils.isEmpty(url))
            throw new IllegalArgumentException("Must specify remote git url");
        try {
            URIish urIish = new URIish(url);
            if (urIish.getUser() == null) {
                String username = preferences.getString("git_username", "");
                urIish = urIish.setUser(username);
            }
            remotePath = urIish.toString();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Invalid remote git url");
        }

        branch = preferences.getString("git_branch", "master");
        if (branch.isEmpty())
            throw new IllegalArgumentException("Must specify a git branch");

        commitAuthor = preferences.getString("git_commit_author", "");
        commitEmail = preferences.getString("git_commit_email", "");

        String mergeStrategyString = preferences.getString("git_merge_strategy", "theirs");
        mergeStrategy = MergeStrategy.get(mergeStrategyString);
        if (mergeStrategy == null)
            throw new IllegalArgumentException("Invalid merge strategy: " + mergeStrategyString);

        setupJGitAuthentication(preferences);
    }

    private void setupJGitAuthentication(SharedPreferences preferences) {
        String username = preferences.getString("git_username", "");
        String password = preferences.getString("git_password", "");
        String keyLocation = preferences.getString("git_key_path", "");

        JGitConfigSessionFactory session = new JGitConfigSessionFactory(username, password, keyLocation);
        SshSessionFactory.setInstance(session);

        credentialsProvider = new JGitCredentialsProvider(username, password);
    }

    private Git initGitRepo(ProgressMonitor monitor) throws Exception {
        if (new File(localPath).exists() == false)
            createNewRepo(monitor);

        FileRepository fileRepository = new FileRepository(localPath + "/.git");
        return new Git(fileRepository);
    }

    private void createNewRepo(ProgressMonitor monitor) throws GitAPIException, IllegalArgumentException {
        File localRepo = new File(localPath);
        if (localRepo.exists()) // Safety check so we don't accidentally delete directory
            throw new IllegalStateException("Directory already exists");

        try {
            CloneCommand cloneCommand = Git.cloneRepository()
                    .setCredentialsProvider(credentialsProvider)
                    .setURI(remotePath)
                    .setBranch(branch)
                    .setDirectory(localRepo)
                    .setBare(false);
            if (monitor != null)
                cloneCommand.setProgressMonitor(monitor);
            cloneCommand.call();
        } catch (GitAPIException e) {
            FileUtils.deleteDirectory(localRepo);
            throw e;
        }
    }


    public Git getGit(ProgressMonitor monitor) throws Exception {
        if (this.git == null)
            this.git = initGitRepo(monitor);

        boolean hasConflicts = false;

        try {
            hasConflicts = this.git.status().call().getConflicting().isEmpty() == false;
        } catch (Exception ex) {}

        if (hasConflicts)
            throw new IllegalStateException("Unresolved conflict(s) in git repository");

        return this.git;
    }

    public void commitAllChanges(String commitMessage) throws Exception {
        Git git = getGit(null);

        Status status = git.status().call();

        if (status.getModified().isEmpty()) {
            Log.d("JGitWrapper", "No files modified, not committing");
            return;
        }

        git.add().addFilepattern(".").call();
        git.commit().setMessage(commitMessage).setAuthor(commitAuthor, commitEmail).call();
        Log.d("JGitWrapper", "Committed changes");
    }

    public void updateChanges(ProgressMonitor monitor) throws Exception {
        Git git = getGit(monitor);

        FetchCommand fetch = git.fetch();
        fetch.setCredentialsProvider(credentialsProvider);
        if (monitor != null)
            fetch.setProgressMonitor(monitor);
        fetch.call();

        SyncState state = getSyncState(git);
        Ref fetchHead = git.getRepository().getRef("FETCH_HEAD");
        switch (state) {
            case Equal:
                // Do nothing
                Log.d("Git", "Local branch is up-to-date");
                break;

            case Ahead:
                Log.d("Git", "Local branch ahead, pushing changes to remote");
                git.push().setCredentialsProvider(credentialsProvider).setRemote(remotePath).call();
                break;

            case Behind:
                Log.d("Git", "Local branch behind, fast forwarding changes");
                MergeResult result = git.merge().include(fetchHead).setFastForward(MergeCommand.FastForwardMode.FF_ONLY).call();
                if (result.getMergeStatus().isSuccessful() == false)
                    throw new IllegalStateException("Fast forward failed on behind merge");
                break;

            case Diverged:
                Log.d("Git", "Branches are diverged, merging with strategy " + mergeStrategy.getName());
                MergeResult mergeResult = git.merge().include(fetchHead).setStrategy(mergeStrategy).call();
                if (mergeResult.getMergeStatus().isSuccessful()) {
                    git.push().setCredentialsProvider(credentialsProvider).setRemote(remotePath).call();
                } else
                    throw new IllegalStateException("Merge failed for diverged branches using strategy " + mergeStrategy.getName());
                break;
        }
    }

    private enum SyncState {
        Equal, Ahead, Behind, Diverged
    }

    private SyncState getSyncState(Git git) throws Exception {
        Ref fetchHead = git.getRepository().getRef("FETCH_HEAD");
        Ref head = git.getRepository().getRef("HEAD");

        if (fetchHead == null)
            throw new IllegalStateException("fetchHead not found!");

        if (head == null)
            throw new IllegalStateException("head not found!");

        Iterable<RevCommit> call = git.log().addRange(fetchHead.getObjectId(), head.getObjectId()).call();
        int originToHead = Utils.getIteratorSize(call.iterator());

        Iterable<RevCommit> call2 = git.log().addRange(head.getObjectId(), fetchHead.getObjectId()).call();
        int headToOrigin = Utils.getIteratorSize(call2.iterator());

        Log.d("JGitWrapper", "fetchHead->head: " + originToHead + " head->fetchHead: " + headToOrigin);

        if (originToHead == 0 && headToOrigin == 0)
            return SyncState.Equal;
        else if (originToHead == 0)
            return SyncState.Behind;
        else if (headToOrigin == 0)
            return SyncState.Ahead;
        else
            return SyncState.Diverged;
    }

    public void cleanup() {
        if (git != null)
            git.close();
    }
}