package info.knacki.pass.ui;

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.pass.R;
import info.knacki.pass.git.GitInterface;
import info.knacki.pass.git.GitInterfaceFactory;
import info.knacki.pass.git.GitLocal;
import info.knacki.pass.git.GitSha1;
import info.knacki.pass.git.entities.GitCommit;
import info.knacki.pass.git.entities.GitObject;
import info.knacki.pass.io.FileUtils;
import info.knacki.pass.io.OnResponseListener;
import info.knacki.pass.io.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;

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;

    private GitInterface fGitInterface;
    private GitCommit fHeadCommit;

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

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

        setContentView(R.layout.activity_git_pull);
        setTitle(R.string.pref_vcs_git_pull);

        if (!(versioning instanceof SettingsManager.Git)) {
            finish();
            return;
        }
        LOCAL_GIT_HASH_VERSION_FILE = PathUtils.GetGitFile(this);
        fGitInterface = GitInterfaceFactory.factory((SettingsManager.Git) versioning);
        if (fGitInterface != null) {
            fGitInterface.FetchHead(new OnStreamResponseListener<GitCommit>() {
                @Override
                public void OnMsg(final String msg) {
                    GitPullActivity.this.OnMsg(msg);
                }

                @Override
                public void OnResponse(GitCommit result) {
                    fHeadCommit = result;
                    GitPullActivity.this.runOnUiThread(GitPullActivity.this::OnTreeStructureFetched);
                }

                @Override
                public void OnError(final String msg, Throwable e) {
                    GitPullActivity.this.runOnUiThread(() -> {
                        ProgressBar pg = findViewById(R.id.progressBar);
                        pg.setIndeterminate(false);
                        pg.setMax(1);
                        pg.setProgress(1);
                        TextView logView = findViewById(R.id.logView);
                        logView.append(msg +"\n");
                        findViewById(R.id.close_bt).setEnabled(true);
                    });
                }
            });
        }
        findViewById(R.id.close_bt).setOnClickListener(v -> GitPullActivity.this.finish());
    }

    protected void OnTreeStructureFetched() {
        final GitLocal localVersion = new GitLocal(new File(LOCAL_GIT_HASH_VERSION_FILE));
        HashMap<String, GitObject.GitBlob> filesToPull = new HashMap<>();
        Set<String> filesToPush = new HashSet<>();
        HashMap<String, GitObject.GitBlob> conflictingFiles = new HashMap<>();

        OnMsg("Done reading remote tree");
        OnMsg("Building change list");
        for (Map.Entry<String, GitObject.GitBlob>i: fHeadCommit.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))
                    filesToPull.put(i.getKey(), i.getValue());
                else
                    conflictingFiles.put(i.getKey(), i.getValue());
            } else if (remoteChanged) {
                // remote changed
                filesToPull.put(i.getKey(), i.getValue());
            } else if (localChanged) {
                // local changed
                filesToPush.add(i.getKey());
            }
        }
        for (String i: localVersion.FileNames()) {
            if (fHeadCommit.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))
                    filesToPull.put(i, null);
                else
                    conflictingFiles.put(i, null);
            }
        }
        CheckNewFiles(localVersion, new File(PathUtils.GetPassDir(this)), "", filesToPull.keySet(), filesToPush, conflictingFiles.keySet());
        if (conflictingFiles.isEmpty())
            SyncFiles(localVersion, filesToPull, filesToPush);
        else
            AskForConflicts(localVersion, conflictingFiles, filesToPull, filesToPush);
    }

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

    void AskForConflicts(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> conflicts, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
        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) {
                            filesToPull.put(s, conflicts.get(s));
                        }
                        filesToPush.addAll(viewResult.fUseMine);
                        SyncFiles(localVersion, filesToPull, filesToPush);
                    })
                    .setTitle(R.string.conflictingFiles);
            ConflictView view = new ConflictView(GitPullActivity.this, pt, conflicts.keySet());
            pt.setView(view).show();
            view.UpdateButtonState();
        });
    }

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

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

    void SyncFiles(final GitLocal localVersion, final HashMap<String, GitObject.GitBlob> filesToPull, final Set<String> filesToPush) {
        final OnStreamResponseListener<Void> allDone = new OnStreamResponseListener<Void>() {
            @Override
            public void OnResponse(Void result) {
                GitPullActivity.this.runOnUiThread(() -> {
                    ProgressBar pg = findViewById(R.id.progressBar);
                    pg.setIndeterminate(false);
                    pg.setMax(1);
                    pg.setProgress(1);
                    localVersion.Write(new File(LOCAL_GIT_HASH_VERSION_FILE));
                    RmEmptyDirs(new File(PathUtils.GetPassDir(GitPullActivity.this)), true);
                    findViewById(R.id.close_bt).setEnabled(true);
                });
            }

            @Override
            public void OnError(String msg, Throwable e) {
                GitPullActivity.this.runOnUiThread(() -> {
                    ProgressBar pg = findViewById(R.id.progressBar);
                    pg.setIndeterminate(false);
                    pg.setMax(1);
                    pg.setProgress(1);
                    findViewById(R.id.close_bt).setEnabled(true);
                });
            }

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

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

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

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

    void DownloadBlobs(Map<String, GitObject.GitBlob> blobs, final GitLocal localVersion, final OnResponseListener<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 OnResponseListener<byte[]> downloader = new OnResponseListener<byte[]>() {
            @Override
            public void OnResponse(final byte[] result) {
                final String filename = files.peek().getKey();
                final GitObject.GitBlob blob = files.peek().getValue();
                final OnResponseListener<byte[]> _this = this;

                GitPullActivity.this.runOnUiThread(() -> {
                    WriteFile(filename, localVersion, blob, result);
                    logView.append("Done fetching " +files.peek().getValue().GetFilename() +"\n");
                    files.pop();
                    DownloadNext(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()) {
                    fGitInterface.FetchBlob(files.peek().getValue(), this);
                } else {
                    resp.OnResponse(null);
                }
            }
        };

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

    void DownloadNext(Stack<Map.Entry<String, GitObject.GitBlob>> files, GitLocal localCache, OnResponseListener<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(files, localCache, downloader, resp);
                log.info("Removed file " +filename);
            } else {
                fGitInterface.FetchBlob(files.peek().getValue(), downloader);
            }
        } else {
            resp.OnResponse(null);
        }
    }

    void WriteFile(String filename, final GitLocal localVersion, GitObject.GitBlob blob, byte[] result) {
        File f = new File(PathUtils.GetPassDir(this) +filename);
        f.getParentFile().mkdirs();
        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()));
    }

    void PushBlobs(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) SettingsManager.GetVCS(this);
            final GitCommit.Builder commit = new GitCommit.Builder(fHeadCommit, config.GetUsername(), config.GetUserEmail(), COMMIT_MSG);
            for (String i : files)
                commit.AddFile(i, new File(PathUtils.GetPassDir(this) + i));
            fGitInterface.PushBlobs(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) {
                    Logger.getAnonymousLogger().log(Level.SEVERE, msg, e);
                }
            });
        }
    }
}
