package info.knacki.pass.git;

import android.os.AsyncTask;
import android.util.Base64;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.InflaterInputStream;

import info.knacki.pass.git.entities.GitCommit;
import info.knacki.pass.git.entities.GitObject;
import info.knacki.pass.git.entities.GitPackable;
import info.knacki.pass.git.entities.GitPackableUtil;
import info.knacki.pass.git.entities.GitRef;
import info.knacki.pass.io.OnResponseListener;
import info.knacki.pass.io.OnStreamResponseListener;
import info.knacki.pass.io.OutputStreamWithCheckSum;
import info.knacki.pass.settings.SettingsManager;

class HttpGitProtocol implements GitInterface {
    private final SettingsManager.Git fConfig;
    private GitRef[] fRefsCache = null;
    private static final Logger log = Logger.getLogger(HttpGitProtocol.class.getName());

    HttpGitProtocol(SettingsManager.Git config) {
        fConfig = config;
    }

    static class DownloaderTask extends AsyncTask<Void, Void, Integer> {
        private final URL fUrl;
        private OnResponseListener<byte[]> fResp;
        private final SettingsManager.Git fConfig;

        DownloaderTask(URL url, SettingsManager.Git config, OnResponseListener<byte[]> resp) {
            fUrl = url;
            fResp = resp;
            fConfig = config;
        }

        DownloaderTask(URL url, SettingsManager.Git config) {
            fUrl = url;
            fConfig = config;
        }

        void SetResultHandler(OnResponseListener<byte[]> handler) {
            fResp = handler;
        }

        protected InputStream GetInputFilter(URLConnection in) throws IOException {
            return in.getInputStream();
        }

        protected void ManageResponse(byte[] resp) {
            fResp.OnResponse(resp);
        }

        @Override
        protected Integer doInBackground(Void... voids) {
            try {
                log.log(Level.INFO, "fetching " +fUrl.toString());
                URLConnection httpClient = fUrl.openConnection();
                if (fConfig.HasAuthentication()) {
                    Authenticator.setDefault(new Authenticator() {
                        @Override
                        protected PasswordAuthentication getPasswordAuthentication() {
                            return new PasswordAuthentication(fConfig.GetUser(), fConfig.GetPassword().toCharArray());
                        }
                    });
                }
                InputStream in = GetInputFilter(httpClient);
                ArrayList<byte[]> fullBuffer = new ArrayList<>();
                int totalRead = 0;
                byte[] buffer = new byte[1024];
                int currentRead;
                while ((currentRead = in.read(buffer, 0, 1024)) != 0) {
                    fullBuffer.add(buffer);
                    buffer = new byte[1024];
                    totalRead += currentRead;
                    if (currentRead < 1024)
                        break;
                }
                in.close();
                buffer = new byte[totalRead];
                int i =0;
                for (byte []currentBuf: fullBuffer) {
                    for (int j =0; j < currentBuf.length && i*1024+j < totalRead; ++j) {
                        buffer[i*1024 +j] = currentBuf[j];
                    }
                    ++i;
                }
                ManageResponse(buffer);
            }
            catch (IOException e) {
                log.log(Level.WARNING, e.getMessage(), e);
                fResp.OnError(e.getClass().getSimpleName() +": " +e.getMessage(), e);
            }
            return 0;
        }
    }

    static class PostTask extends AsyncTask<Void, Void, Integer> {
        private final URL fUrl;
        private final OnResponseListener<byte[]> fResp;
        private final SettingsManager.Git fConfig;
        private final OnResponseListener<OutputStream> fOnReadyWrite;
        private final Map<String, String> fHeaders;

        PostTask(URL url, SettingsManager.Git config, Map<String, String> headers, OnResponseListener<byte[]> resp, OnResponseListener<OutputStream> onReadyWrite) {
            fUrl = url;
            fResp = resp;
            fConfig = config;
            fOnReadyWrite = onReadyWrite;
            fHeaders = headers;
        }

        protected InputStream GetInputFilter(URLConnection in) throws IOException {
            try {
                return in.getInputStream();
            } catch (Throwable e) {
                Map<String, List<String>> headers = in.getHeaderFields();
                String msg = null;
                if (!headers.values().isEmpty()) {
                    List<String> statuses = headers.values().iterator().next();
                    if (!statuses.isEmpty())
                        msg = in.getURL().toString() + ": " +statuses.iterator().next();
                }
                if (msg == null)
                    msg = "Tried to reach " +in.getURL().toString();
                log.log(Level.SEVERE, msg, e);
                throw e;
            }
        }

        protected void ManageResponse(byte[] resp) {
            fResp.OnResponse(resp);
        }

        @Override
        protected Integer doInBackground(Void... voids) {
            try {
                log.log(Level.INFO, "fetching " +fUrl.toString());
                HttpURLConnection httpClient = (HttpURLConnection) fUrl.openConnection();
                httpClient.setRequestMethod("POST");
                for (Map.Entry<String, String> header: fHeaders.entrySet()) {
                    httpClient.setRequestProperty(header.getKey(), header.getValue());
                }
                if (fConfig.HasAuthentication()) {
                    httpClient.setRequestProperty("Authorization", "basic " +Base64.encodeToString("fConfig.GetUser():fConfig.GetPassword()".getBytes(), Base64.NO_WRAP));
                }
                fOnReadyWrite.OnResponse(httpClient.getOutputStream());
                InputStream in = GetInputFilter(httpClient);
                ArrayList<byte[]> fullBuffer = new ArrayList<>();
                int totalRead = 0;
                byte[] buffer = new byte[1024];
                int currentRead;
                while ((currentRead = in.read(buffer, 0, 1024)) > 0) {
                    fullBuffer.add(buffer);
                    buffer = new byte[1024];
                    totalRead += currentRead;
                    if (currentRead < 1024)
                        break;
                }
                in.close();
                if (totalRead >= 0) {
                    buffer = new byte[totalRead];
                    int i = 0;
                    for (byte[] currentBuf : fullBuffer) {
                        for (int j = 0; j < currentBuf.length && i * 1024 + j < totalRead; ++j) {
                            buffer[i * 1024 + j] = currentBuf[j];
                        }
                        ++i;
                    }
                    ManageResponse(buffer);
                } else {
                    ManageResponse(new byte[] {});
                }
            }
            catch (IOException e) {
                log.log(Level.WARNING, e.getMessage(), e);
                fResp.OnError(e.getClass().getSimpleName() +": " +e.getMessage(), e);
            }
            return 0;
        }
    }

    static class DownloaderWithInflaterTask extends DownloaderTask {
        DownloaderWithInflaterTask(URL url, SettingsManager.Git config, OnResponseListener<byte[]> resp) {
            super(url, config, resp);
        }

        @Override
        protected InputStream GetInputFilter(URLConnection in) throws IOException {
            return new InflaterInputStream(super.GetInputFilter(in));
        }

        @Override
        protected void ManageResponse(byte[] result) {
            int i =0;

            while (i < result.length && result[i] != 0)
                ++i;
            if (i != result.length) {
                ++i;
                byte []arr = new byte[result.length -i];
                System.arraycopy(result, i, arr, 0, result.length -i);
                super.ManageResponse(arr);
            } else {
                super.ManageResponse(result);
            }
        }
    }

    static class StringDownloaderTask extends DownloaderTask {
        private final OnResponseListener<String> onResp;

        StringDownloaderTask(URL url, SettingsManager.Git config, OnResponseListener<String> resultHandler) {
            super(url, config);
            SetResultHandler(new OnResponseListener<byte[]>() {
                @Override
                public void OnResponse(byte[] result) {
                    StringDownloaderTask.this.onResp.OnResponse(new String(result, Charset.defaultCharset()));
                }

                @Override
                public void OnError(String msg, Throwable e) {
                    StringDownloaderTask.this.onResp.OnError(msg, e);
                }
            });
            onResp = resultHandler;
        }
    }

    private void protoInflateGet(final URL url, final OnResponseListener<byte[]> callback) {
        new DownloaderWithInflaterTask(url, fConfig, callback).execute();
    }

    private void ProtoPost(URL url, Map<String, String> headers, OnResponseListener<byte[]> callback, OnResponseListener<OutputStream> onReadyWrite) {
        new PostTask(url, fConfig, headers, callback, onReadyWrite).execute();
    }

    private void protoGet(final URL url, final OnResponseListener<String> callback) {
        new StringDownloaderTask(url, fConfig, callback).execute();
    }

    public void GetRefs(final OnResponseListener<GitRef[]> callback) {
        if (fRefsCache != null) {
            callback.OnResponse(fRefsCache);
            return;
        }
        try {
            URL url = new URL(fConfig.GetUrl() + "/info/REFS");
            protoGet(url, new OnResponseListener<String>() {
                @Override
                public void OnResponse(String result) {
                    String [] refStrings = result.split("\n");
                    fRefsCache = new GitRef[refStrings.length];
                    for (int i =0; i < refStrings.length; ++i) {
                        String refLine = refStrings[i];
                        for (int j =0; j < refLine.length(); ++j) {
                            if (refLine.charAt(j) == '\t') {
                                fRefsCache[i] = new GitRef(refLine.substring(0, j).trim(), refLine.substring(j +1).trim());
                                break;
                            }
                        }
                    }
                    callback.OnResponse(fRefsCache);
                }

                @Override
                public void OnError(String msg, Throwable e) {
                    callback.OnError(msg, e);
                }
            });
        } catch (MalformedURLException e) {
            callback.OnError(e.getMessage(), e);
        }
    }

    public void FetchCommit(final GitRef ref, final OnResponseListener<GitCommit> response) {
        PullHash(ref.GetHash(), new OnResponseListener<byte[]>() {
            @Override
            public void OnResponse(byte[] result) {
                response.OnResponse(new GitCommit(ref.GetHash(), new String(result, Charset.defaultCharset())));
            }

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

    public void FetchTree(final GitCommit ci, final OnStreamResponseListener<GitObject.GitTree> response) {
        class RecursiveFetchTreeWalker implements OnResponseListener<byte[]> {
            private final GitObject.GitTree fRoot;
            private GitObject.GitTree fCurrentTree;
            private final OnStreamResponseListener<GitObject.GitTree> fResponseListener;

            private RecursiveFetchTreeWalker(GitCommit ci, final OnStreamResponseListener<GitObject.GitTree> responseListener) {
                fRoot = fCurrentTree = ci.InitializeTree().Initialize();
                fResponseListener = responseListener;
            }

            public void run() {
                PullHash(ci.GetTreeHash());
            }

            @Override
            public void OnResponse(byte[] result) {
                FillTree(fCurrentTree, result);
                fCurrentTree = fRoot.FindNextNotInitializedTree();
                if (fCurrentTree != null)
                    PullHash(fCurrentTree.Initialize().GetHash());
                else
                    fResponseListener.OnResponse(fRoot);
            }

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

            private void PullHash(String hash) {
                fResponseListener.OnMsg("Reading tree " +hash);
                HttpGitProtocol.this.PullHash(hash, this);
            }

            private void PullHash(byte[] hash) {
                PullHash(GitSha1.BytesToString(hash));
            }

            private void FillTree(final GitObject.GitTree tree, byte[] data) {
                int i = 0;

                while (i < data.length) {
                    byte[] mode = null;
                    int len;
                    for (len =0; i +len <data.length && len < 8; ++len)
                        if (data[i +len] == ' ') {
                            mode = new byte[len];
                            System.arraycopy(data, i, mode, 0, len);
                            i += len +1;
                            break;
                        }
                    if (mode == null)
                        break;
                    len = 0;
                    while (i +len < data.length && data[i +len] != 0)
                        ++len;
                    byte[] filenameBytes = new byte[len];
                    System.arraycopy(data, i, filenameBytes, 0, len);
                    String fileName = new String(filenameBytes, Charset.defaultCharset());
                    i += len +1;
                    if (i +20 > data.length)
                        break;
                    byte []sha1 = new byte[20];
                    System.arraycopy(data, i, sha1, 0, 20);
                    i += 20;
                    tree.AddItem(GitObject.factory(tree, mode, fileName, sha1));
                }
            }
        }

        new RecursiveFetchTreeWalker(ci, response).run();
    }

    @Override
    public void FetchBlob(final GitObject.GitBlob blob, final OnResponseListener<byte[]> response) {
        PullHash(GitSha1.BytesToString(blob.GetHash()), response);
    }

    public void FetchHead(final OnStreamResponseListener<GitCommit> response) {
        GetRefs(new OnResponseListener<GitRef[]>() {
            @Override
            public void OnResponse(final GitRef[] result) {
                GitRef myRef = null;
                response.OnMsg("Found refs: ");
                for (GitRef ref: result) {
                    response.OnMsg("\t> "+ref.GetBranch());
                    if (ref.GetBranch().equals(fConfig.GetBranch()))
                        myRef = ref;
                }
                if (myRef != null) {
                    response.OnMsg("Checking out branch " +myRef.GetBranchName() +" revision " +myRef.GetHash());
                    FetchCommit(myRef, new OnResponseListener<GitCommit>() {
                        @Override
                        public void OnResponse(final GitCommit result) {
                            response.OnMsg("Finished read commit");
                            response.OnMsg(result.GetMessage());
                            response.OnMsg("Reading tree #" +GitSha1.BytesToString(result.GetTreeHash()));
                            FetchTree(result, new OnStreamResponseListener<GitObject.GitTree>() {
                                @Override
                                public void OnMsg(String message) {
                                    response.OnMsg(message);
                                }

                                @Override
                                public void OnResponse(GitObject.GitTree tree) {
                                    response.OnMsg("Finished reading tree");
                                    response.OnResponse(result);
                                }

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

                        @Override
                        public void OnError(String msg, Throwable e) {
                            response.OnError(msg, e);
                        }
                    });
                } else {
                    response.OnError("Branch " +fConfig.GetBranch() + " not found on remote", null);
                }
            }

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

    private void PullHash(String hash, final OnResponseListener<byte[]> response) {
        URL url;
        try {
            url = new URL(fConfig.GetUrl() + "/objects/" + hash.substring(0, 2) + "/" + hash.substring(2));
        }
        catch (MalformedURLException e) {
            response.OnError(e.getClass().getName() +": " +e.getMessage(), e);
            return;
        }
        protoInflateGet(url, response);
    }

    public boolean makePack(SortedSet<GitPackable> objectsToPack, OutputStream out) throws IOException
    {
        OutputStreamWithCheckSum msg = new OutputStreamWithCheckSum(out);
        msg.write("PACK")
            .write(new byte[] { 0, 0, 0, 2 } )
            .write(ByteBuffer.allocate(4).putInt(objectsToPack.size()).array());
        for (GitPackable i: objectsToPack) {
            byte[] pack = i.GetPack();
            if (null == pack)
                return false;
            msg.write(GitPackableUtil.getObjHeader(i.GetPackableType(), pack.length))
                .write(GitPackableUtil.deflate(pack));
            log.info("Writing pack " +i.GetPackableType() +", " +GitSha1.getSha1OfPackable(i) +new String(i.GetPack()));
        }
        msg.writeSha1();
        return true;
    }

    public void PushBlobs(final GitCommit.Builder commit, final OnStreamResponseListener<Void> response) {
        GetRefs(new OnResponseListener<GitRef[]>() {
            @Override
            public void OnResponse(final GitRef[] result) {
                GitRef myRef = null;
                for (GitRef ref: result) {
                    if (ref.GetBranch().equals(fConfig.GetBranch()))
                        myRef = ref;
                }
                if (myRef != null) {
                    final GitRef finalRef = myRef;
                    response.OnMsg("Pushing over " +myRef.GetBranch() +" revision " +myRef.GetHash());
                    try {
                        HashMap<String, String> headers = new HashMap<>();
                        headers.put("Content-Type", "application/x-git-receive-pack-request");
                        ProtoPost(new URL(fConfig.GetUrl() +"/git-receive-pack"), headers, new OnResponseListener<byte[]>() {
                            @Override
                            public void OnResponse(byte[] result) {
                                Logger.getAnonymousLogger().severe("git-receive-pack " +new String(result, Charset.defaultCharset()));
                                response.OnResponse(null);
                            }

                            @Override
                            public void OnError(String msg, Throwable e) {
                                Logger.getAnonymousLogger().severe("git-receive-pack ERROR");
                                response.OnError(msg, e);
                            }
                        }, new OnResponseListener<OutputStream>() {
                            @Override
                            public void OnResponse(OutputStream result) {
                                byte[] msgLine = (finalRef.GetHash() + " " + GitSha1.BytesToString(GitSha1.getRawSha1OfPackable(commit.Build())) +" " +finalRef.GetBranch()).getBytes();
                                try {
                                    result.write(String.format(Locale.US, "%04X", msgLine.length +4).getBytes());
                                    result.write(msgLine);
                                    result.write("0000".getBytes());
                                    if (!makePack(commit.PreparePack(), result)) {
                                        OnError("Pack error", null);
                                        return;
                                    }
                                    result.close();
                                }
                                catch (IOException e) {
                                    log.log(Level.SEVERE, "Cannot git-upload-pack: " +e.getMessage(), e);
                                    OnError(e.getMessage(), e);
                                }
                            }

                            @Override
                            public void OnError(String msg, Throwable e) {
                                response.OnError(msg, e);
                            }
                        });
                    }
                    catch (IOException e) {
                        response.OnError(e.getMessage(), e);
                    }
                } else {
                    response.OnError("Branch " +fConfig.GetBranch() + " not found on remote for pushing", null);
                }
            }

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