package com.topjohnwu.net;

import android.os.AsyncTask;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Scanner;
import java.util.concurrent.Executor;

public class Request implements Closeable {
    private HttpURLConnection conn;
    private Executor executor = null;
    private DownloadProgressListener progress = null;
    private int code = -1;

    ErrorHandler err = null;

    private interface Requestor<T> {
        T request() throws Exception;
    }

    public class Result<T> {
        T result;

        public T getResult() {
            return result;
        }

        public int getCode() {
            return code;
        }

        public boolean isSuccess() {
            return code >= 200 && code <= 299;
        }

        public HttpURLConnection getConnection() {
            return conn;
        }
    }

    Request(HttpURLConnection c) {
        conn = c;
    }

    @Override
    public void close() {
        conn.disconnect();
    }

    public Request addHeaders(String key, String value) {
        conn.setRequestProperty(key, value);
        return this;
    }

    public Request setDownloadProgressListener(DownloadProgressListener listener) {
        progress = listener;
        return this;
    }

    public Request setErrorHandler(ErrorHandler handler) {
        err = handler;
        return this;
    }

    public Request setExecutor(Executor e) {
        executor = e;
        return this;
    }

    public Result<Void> connect() {
        try {
            connect0();
        } catch (IOException e) {
            if (err != null)
                err.onError(conn, e);
        }
        return new Result<>();
    }

    public Result<InputStream> execForInputStream() {
        return exec(this::getInputStream);
    }

    public void getAsFile(File out, ResponseListener<File> rs) {
        submit(() -> dlFile(out), rs);
    }

    public void execForFile(File out) {
        exec(() -> dlFile(out));
    }

    public void getAsBytes(ResponseListener<byte[]> rs) {
        submit(this::dlBytes, rs);
    }

    public Result<byte[]> execForBytes() {
        return exec(this::dlBytes);
    }

    public void getAsString(ResponseListener<String> rs) {
        submit(this::dlString, rs);
    }

    public Result<String> execForString() {
        return exec(this::dlString);
    }

    public void getAsJSONObject(ResponseListener<JSONObject> rs) {
        submit(this::dlJSONObject, rs);
    }

    public Result<JSONObject> execForJSONObject() {
        return exec(this::dlJSONObject);
    }

    public void getAsJSONArray(ResponseListener<JSONArray> rs) {
        submit(this::dlJSONArray, rs);
    }

    public Result<JSONArray> execForJSONArray() {
        return exec(this::dlJSONArray);
    }

    private void connect0() throws IOException {
        conn.connect();
        code = conn.getResponseCode();
    }

    private <T> Result<T> exec(Requestor<T> req) {
        Result<T> res = new Result<>();
        try {
            res.result = req.request();
        } catch (Exception e) {
            if (err != null)
                err.onError(conn, e);
        }
        return res;
    }

    private <T> void submit(Requestor<T> req, ResponseListener<T> rs) {
        AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
            try {
                T t = req.request();
                Runnable cb = () -> rs.onResponse(t);
                if (executor == null)
                    Networking.mainHandler.post(cb);
                else
                    executor.execute(cb);
            } catch (Exception e) {
                if (err != null)
                    err.onError(conn, e);
            }
        });
    }

    private BufferedInputStream getInputStream() throws IOException {
        connect0();
        InputStream in = conn.getInputStream();
        if (progress != null) {
            in = new ProgressInputStream(in, conn.getContentLength(), progress) {
                @Override
                public void close() throws IOException {
                    super.close();
                    conn.disconnect();
                }
            };
        } else {
            in = new FilterInputStream(in) {
                @Override
                public void close() throws IOException {
                    super.close();
                    conn.disconnect();
                }
            };
        }
        return new BufferedInputStream(in);
    }

    private String dlString() throws IOException {
        try (Scanner s = new Scanner(getInputStream(), "UTF-8")) {
            s.useDelimiter("\\A");
            return s.next();
        }
    }

    private JSONObject dlJSONObject() throws IOException, JSONException {
        return new JSONObject(dlString());
    }

    private JSONArray dlJSONArray() throws IOException, JSONException {
        return new JSONArray(dlString());
    }

    private File dlFile(File f) throws IOException {
        try (InputStream in  = getInputStream();
             OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
            int len;
            byte[] buf = new byte[4096];
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
        }
        return f;
    }

    private byte[] dlBytes() throws IOException {
        int len = conn.getContentLength();
        len = len > 0 ? len : 32;
        ByteArrayOutputStream out = new ByteArrayOutputStream(len);
        try (InputStream in  = getInputStream()) {
            byte[] buf = new byte[4096];
            while ((len = in.read(buf)) != -1) {
                out.write(buf, 0, len);
            }
        }
        return out.toByteArray();
    }
}
