package info.knacki.pass.ui;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.ProgressBar;
import android.widget.TextView;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;

import info.knacki.gitdroid.GitInterface;
import info.knacki.gitdroid.GitInterfaceFactory;
import info.knacki.gitdroid.GitLocal;
import info.knacki.gitdroid.GitSha1;
import info.knacki.gitdroid.entities.GitCommit;
import info.knacki.gitdroid.entities.GitObject;
import info.knacki.gitdroid.entities.GitRef;
import info.knacki.pass.R;
import info.knacki.pass.io.FileUtils;
import info.knacki.gitdroid.callback.OnResponseListener;
import info.knacki.gitdroid.callback.OnStreamResponseListener;
import info.knacki.pass.io.PathUtils;
import info.knacki.pass.settings.SettingsManager;
import info.knacki.pass.ui.alertPrompt.AlertPrompt;
import info.knacki.pass.ui.alertPrompt.AlertPromptGenerator;
import info.knacki.pass.ui.alertPrompt.views.ConflictView;

import static info.knacki.pass.settings.SettingsManager.GetVCS;

public class GitPullActivity extends AppCompatActivity {
    private final static Logger log = Logger.getLogger(GitPullActivity.class.getName());
    public final static String COMMIT_MSG = "Android pass sync";
    public static String LOCAL_GIT_HASH_VERSION_FILE;

    public final static String GIT_COMMAND = GitPullActivity.class.getName() +"_GIT_COMMAND";
    public final static String BRANCH_NAME = GitPullActivity.class.getName() +"_BRANCH_NAME";
    public final static char COMMAND_SYNC = 1;
    public final static char COMMAND_CREATE_BRANCH = 2;

    private class Status {
        HashSet<String> filesToPush = new HashSet<>();
        HashMap<String, GitObject.GitBlob> filesToPull = new HashMap<>();
        HashMap<String, GitObject.GitBlob> conflictingFiles = new HashMap<>();
    }

    private void OnMsg(final String msg) {
        log.info(msg);
        GitPullActivity.this.runOnUiThread(() -> {
            TextView logView = findViewById(R.id.logView);
            logView.append(msg +"\n");
        });
    }

    public static void StartSyncActivity(Context ctx) {
        Intent i = new Intent(ctx, GitPullActivity.class);
        i.putExtra(GIT_COMMAND, COMMAND_SYNC);
        ctx.startActivity(i);
    }

    public static void StartCreateBranchActivity(Context ctx, String branchName) {
        Intent i = new Intent(ctx, GitPullActivity.class);
        i.putExtra(GIT_COMMAND, COMMAND_CREATE_BRANCH);
        i.putExtra(BRANCH_NAME, branchName);
        ctx.startActivity(i);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        final SettingsManager.VCS versioning = GetVCS(this);

        setContentView(R.layout.activity_git_pull);
        setTitle(R.string.pref_vcs_git_pull);
        findViewById(R.id.close_bt).setOnClickListener(v -> GitPullActivity.this.finish());

        LOCAL_GIT_HASH_VERSION_FILE = PathUtils.GetGitFile(this);

        if (!(versioning instanceof SettingsManager.Git)) {
            finish();
            return;
        }
        final GitInterface gitInterface;
        try {
            gitInterface = GitInterfaceFactory.factory((SettingsManager.Git) versioning);
        } catch (GitInterfaceFactory.GitInterfaceException e) {
            GitPullActivity.this.runOnUiThread(() -> {
                OnMsg(e.getMessage());
                FinishLoading();
            });
            return;
        }

        final char command = getIntent().getCharExtra(GIT_COMMAND, (char) 0);
        if (COMMAND_SYNC == command)
            FetchHead(gitInterface);
        else if (COMMAND_CREATE_BRANCH == command)
            CreateBranch(gitInterface, getIntent().getStringExtra(BRANCH_NAME));
        else
            finish();
    }

    private void CreateBranch(GitInterface gitInterface, String branchName) {
        if (branchName == null)
            finish();
        OnMsg("Creating branch " +branchName);
        final String trimmedBranchName = branchName.trim();
        if ("".equals(trimmedBranchName)) {
            OnMsg("Invalid branch name");
            finish();
        }

        OnMsg("Pushing to origin");
        gitInterface.CreateBranch(trimmedBranchName, new OnStreamResponseListener<GitRef[]>() {
            @Override
            public void OnResponse(GitRef[] result) {
                runOnUiThread(GitPullActivity.this::FinishLoading);
                for (GitRef i: result)
                    if (i.GetBranchName().equals(trimmedBranchName)) {
                        SettingsManager.VCS git = SettingsManager.GetVCS(GitPullActivity.this);
                        if (git instanceof SettingsManager.Git) {
                            ((SettingsManager.Git) git).SetBranch(i.GetBranch());
                            SettingsManager.SetVCS(GitPullActivity.this, git);
                            OnMsg("Branch set up to track " +i.GetBranch());
                        }
                    }
            }

            @Override
            public void OnError(String msg, Throwable e) {
                runOnUiThread(() -> {
                    GitPullActivity.this.OnMsg(msg);
                    FinishLoading();
                });
            }

            @Override
            public void OnMsg(String msg) {
                runOnUiThread(() -> GitPullActivity.this.OnMsg(msg));
            }
        });
    }

    private void FetchHead(GitInterface gitInterface) {
        gitInterface.FetchHead(new OnStreamResponseListener<GitCommit>() {
            @Override
            public void OnMsg(final String msg) {
                GitPullActivity.this.OnMsg(msg);
            }

            @Override
            public void OnResponse(GitCommit result) {
                GitPullActivity.this.runOnUiThread(() -> GitPullActivity.this.OnTreeStructureFetched(gitInterface, result));
            }

            @Override
            public void OnError(final String msg, Throwable e) {
                GitPullActivity.this.runOnUiThread(() -> {
                    OnMsg(msg);
                    FinishLoading();
                });
            }
        });
    }

    private void OnTreeStructureFetched(GitInterface gitInterface, GitCommit headCommit) {
        final GitLocal localVersion = new GitLocal(new File(LOCAL_GIT_HASH_VERSION_FILE));
        Status status = new Status();

        OnMsg("Done reading remote tree");
        OnMsg("Building change list");
        for (Map.Entry<String, GitObject.GitBlob>i: headCommit.GetTree().FindAllBlobs().entrySet()) {
            final String remoteKnownHash = localVersion.GetHash(i.getKey(), "");
            final String remoteHash = GitSha1.BytesToString(i.getValue().GetHash());
            final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i.getKey())));

            final boolean remoteChanged = !remoteKnownHash.equals(remoteHash);
            final boolean localChanged = !remoteKnownHash.equals(currentHash);

            if (remoteChanged && localChanged) {
                // Conflict (but file can still has the same content)
                if (currentHash.equals(remoteHash))
                    status.filesToPull.put(i.getKey(), i.getValue());
                else
                    status.conflictingFiles.put(i.getKey(), i.getValue());
            } else if (remoteChanged) {
                // remote changed
                status.filesToPull.put(i.getKey(), i.getValue());
            } else if (localChanged) {
                // local changed
                status.filesToPush.add(i.getKey());
            }
        }
        for (String i: localVersion.FileNames()) {
            if (headCommit.GetTree().GetObjectFullPath(i) == null) {
                log.finer("removed from remote " +i);
                final String currentHash = GitSha1.BytesToString(GitSha1.getRawSha1OfFile(new File(PathUtils.GetPassDir(this) +i)));
                final boolean localChanged = !currentHash.equals(localVersion.GetHash(i, ""));

                if (!localChanged || "".equals(currentHash))
                    status.filesToPull.put(i, null);
                else
                    status.conflictingFiles.put(i, null);
            }
        }
        CheckNewFiles(localVersion, new File(PathUtils.GetPassDir(this)), "", status);
        if (status.conflictingFiles.isEmpty())
            SyncFiles(localVersion, gitInterface, headCommit, status);
        else
            AskForConflicts(localVersion, gitInterface, headCommit, status);
    }

    private int CheckNewFiles(GitLocal localVersion, File root, String rootPath, Status status) {
        int newFiles = 0;
        for (final File i: root.listFiles()) {
            if (!PathUtils.IsHidden(i.getAbsolutePath())) {
                if (i.isDirectory()) {
                    newFiles += CheckNewFiles(localVersion, i, rootPath + "/" + i.getName(), status);
                } else if (i.isFile()) {
                    String path = rootPath + "/" + i.getName();
                    if (!localVersion.HasHash(path) && !status.filesToPull.containsKey(path) && !status.conflictingFiles.containsKey(path)) {
                        // New file
                        status.filesToPush.add(path);
                        newFiles++;
                    }
                }
            }
        }
        return newFiles;
    }

    private void AskForConflicts(final GitLocal localVersion, final GitInterface gitInterface, final GitCommit headCommit, final Status status) {
        runOnUiThread(() -> {
            AlertPrompt pt = AlertPromptGenerator.StaticMake(GitPullActivity.this)
                    .setCancelable(true)
                    .setNegativeButton(R.string.cancel, (dialogInterface, view) -> GitPullActivity.this.finish())
                    .setPositiveButton(R.string.ok, (dialogInterface, v) -> {
                        ConflictView.ConflictViewResult viewResult = ((ConflictView) v).GetResult();
                        for (String s: viewResult.fUseTheir) {
                            status.filesToPull.put(s, status.conflictingFiles.get(s));
                        }
                        status.filesToPush.addAll(viewResult.fUseMine);
                        SyncFiles(localVersion, gitInterface, headCommit, status);
                    })
                    .setTitle(R.string.conflictingFiles);
            ConflictView view = new ConflictView(GitPullActivity.this, pt, status.conflictingFiles.keySet());
            pt.setView(view).show();
            view.UpdateButtonState();
        });
    }

    private void RmEmptyDirs(File dir, boolean isRoot) {
        File[] content = dir.listFiles();

        if (null == content || content.length == 0) {
            if (!isRoot)
                FileUtils.DeleteFile(dir);
            return;
        }
        for (File i: content)
            if (i.isDirectory())
                RmEmptyDirs(i, false);
    }

    private void FinishLoading() {
        ProgressBar pg = findViewById(R.id.progressBar);
        pg.setIndeterminate(false);
        pg.setMax(1);
        pg.setProgress(1);
        RmEmptyDirs(new File(PathUtils.GetPassDir(GitPullActivity.this)), true);
        findViewById(R.id.close_bt).setEnabled(true);
    }

    private void SyncFiles(final GitLocal localVersion, final GitInterface gitInterface, final GitCommit headCommit, final Status status) {
        final OnStreamResponseListener<Void> allDone = new OnStreamResponseListener<Void>() {
            @Override
            public void OnResponse(Void result) {
                GitPullActivity.this.runOnUiThread(() -> {
                    localVersion.Write(new File(LOCAL_GIT_HASH_VERSION_FILE));
                    FinishLoading();
                });
            }

            @Override
            public void OnError(String msg, Throwable e) {
                GitPullActivity.this.runOnUiThread(() -> FinishLoading());
            }

            @Override
            public void OnMsg(String message) {
                GitPullActivity.this.OnMsg(message);
            }
        };

        final Runnable afterFetching = () -> {
            if (status.filesToPush.size() > 0) {
                GitPullActivity.this.OnMsg("Updating remote repository");
                PushBlobs(gitInterface, headCommit, status.filesToPush, localVersion, allDone);
            } else {
                GitPullActivity.this.OnMsg("Nothing to push");
                allDone.OnResponse(null);
            }
        };

        if (status.filesToPull.isEmpty()) {
            OnMsg("Nothing to pull");
            afterFetching.run();
        } else {
            OnMsg("Updating local repository");
            DownloadBlobs(gitInterface, status.filesToPull, localVersion, new OnStreamResponseListener<Void>() {
                @Override
                public void OnResponse(Void result) {
                    afterFetching.run();
                }

                @Override
                public void OnError(String msg, Throwable e) {
                    allDone.OnError(msg, e);
                }

                @Override
                public void OnMsg(String message) {
                    GitPullActivity.this.OnMsg(message);
                }
            });
        }
    }

    private void DownloadBlobs(GitInterface gitInterface, Map<String, GitObject.GitBlob> blobs, final GitLocal localVersion, final OnStreamResponseListener<Void> resp) {
        if (blobs.size() == 0) {
            resp.OnResponse(null);
            return;
        }
        final TextView logView = findViewById(R.id.logView);
        logView.append(blobs.size() +" files to update locally\n");
        for (String i: blobs.keySet())
            logView.append(" > " +i +"\n");
        final Stack<Map.Entry<String, GitObject.GitBlob>> files = new Stack<>();
        files.addAll(blobs.entrySet());
        final OnStreamResponseListener<byte[]> downloader = new OnStreamResponseListener<byte[]>() {
            @Override
            public void OnResponse(final byte[] result) {
                final String filename = files.peek().getKey();
                final GitObject.GitBlob blob = files.peek().getValue();
                final OnStreamResponseListener<byte[]> _this = this;

                GitPullActivity.this.runOnUiThread(() -> {
                    WriteFile(filename, localVersion, blob, result);
                    logView.append("Done fetching " +files.peek().getValue().GetFilename() +"\n");
                    files.pop();
                    DownloadNext(gitInterface, files, localVersion, _this, resp);
                });
            }

            @Override
            public void OnError(final String msg, Throwable e) {
                log.log(Level.SEVERE, msg, e);

                GitPullActivity.this.runOnUiThread(() -> logView.append("Error while fetching " +files.peek().getValue().GetFilename() +": " +msg));

                files.pop();
                if (!files.empty()) {
                    gitInterface.FetchBlob(files.peek().getValue(), this);
                } else {
                    resp.OnResponse(null);
                }
            }

            @Override
            public void OnMsg(String message) {
                resp.OnMsg(message);
            }
        };

        DownloadNext(gitInterface, files, localVersion, downloader, resp);
    }

    private void DownloadNext(GitInterface gitInterface, Stack<Map.Entry<String, GitObject.GitBlob>> files, GitLocal localCache, OnStreamResponseListener<byte[]> downloader, OnResponseListener<Void> resp) {
        if (!files.empty()) {
            if (files.peek().getValue() == null) {
                // remove file
                String filename = files.pop().getKey();
                FileUtils.TrashFile(new File(PathUtils.GetPassDir(this) +filename));
                localCache.remove(filename);
                DownloadNext(gitInterface, files, localCache, downloader, resp);
                log.info("Removed file " +filename);
            } else {
                gitInterface.FetchBlob(files.peek().getValue(), downloader);
            }
        } else {
            resp.OnResponse(null);
        }
    }

    private void WriteFile(String filename, final GitLocal localVersion, GitObject.GitBlob blob, byte[] result) {
        File f = new File(PathUtils.GetPassDir(this) +filename);
        FileUtils.MkDir(f.getParentFile(), true);
        final int chunkSize = 1024;
        final int nbChunk = (int) Math.ceil((double) result.length / chunkSize);

        try {
            OutputStream writer = new FileOutputStream(f);
            for (int i = 0; i < nbChunk; ++i)
                writer.write(result, i * chunkSize, Math.min(chunkSize, result.length -Math.max(0, ((i -1) * chunkSize))));
            writer.close();
        }
        catch (IOException e) {
            log.log(Level.SEVERE, e.getMessage(), e);
            return;
        }
        localVersion.SetHash(filename, GitSha1.BytesToString(blob.GetHash()));
    }

    private void PushBlobs(final GitInterface gitInterface, final GitCommit headCommit, final Set<String> files, final GitLocal localVersion, final OnStreamResponseListener<Void> resp) {
        if (files.isEmpty()) {
            resp.OnMsg("Nothing to commit");
            resp.OnResponse(null);
        } else {
            SettingsManager.Git config = (SettingsManager.Git) GetVCS(this);
            final GitCommit.Builder commit = new GitCommit.Builder(headCommit, config.GetUsername(), config.GetUserEmail(), COMMIT_MSG);
            for (String i : files)
                commit.AddFile(i, new File(PathUtils.GetPassDir(this) + i));
            gitInterface.PushCommitBuilder(commit, new OnStreamResponseListener<Void>() {
                @Override
                public void OnMsg(String message) {
                    resp.OnMsg(message);
                }

                @Override
                public void OnResponse(Void result) {
                    final GitObject.GitTree tree = commit.Build().GetTree();
                    for (String i: files) {
                        if (tree.GetObjectFullPath(i) == null)
                            localVersion.remove(i);
                        else
                            localVersion.SetHash(i, GitSha1.BytesToString(tree.GetObjectFullPath(i).GetHash()));
                    }
                    OnMsg("Done");
                    resp.OnResponse(result);
                }

                @Override
                public void OnError(String msg, Throwable e) {
                    log.log(Level.SEVERE, msg, e);
                }
            });
        }
    }
}
