package info.knacki.pass.io;

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.PasswordAuthentication;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.InflaterInputStream;

public class NetworkUtils {
    private final static Logger log = Logger.getLogger(NetworkUtils.class.getName());

    public interface NetworkConfig {
        boolean HasAuthentication();
        String GetUser();
        String GetPassword();
    }

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

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

        DownloaderTask(URL url, NetworkConfig 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);
                byte[] buffer = FileUtils.ReadAllStream(in);
                in.close();
                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 NetworkConfig fConfig;
        private final OnResponseListener<OutputStream> fOnReadyWrite;
        private final Map<String, String> fHeaders;

        PostTask(URL url, NetworkConfig 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, NetworkConfig 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;

            for (i =0; 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, NetworkConfig config, OnResponseListener<String> resultHandler) {
            super(url, config);
            SetResultHandler(new OnResponseListener<byte[]>() {
                @Override
                public void OnResponse(byte[] result) {
                    StringDownloaderTask.this.onResp.OnResponse(CharsetHelper.ByteArrayToString(result));
                }

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

    public static void protoInflateGet(final URL url, NetworkConfig config, final OnResponseListener<byte[]> callback) {
        new DownloaderWithInflaterTask(url, config, callback).execute();
    }

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

    public static void protoGet(final URL url, NetworkConfig config, final OnResponseListener<String> callback) {
        new StringDownloaderTask(url, config, callback).execute();
    }
}
