package org.xutils.http.request;

import android.annotation.TargetApi;
import android.os.Build;
import android.text.TextUtils;

import org.xutils.cache.DiskCacheEntity;
import org.xutils.cache.LruDiskCache;
import org.xutils.common.util.IOUtil;
import org.xutils.common.util.KeyValue;
import org.xutils.common.util.LogUtil;
import org.xutils.ex.HttpException;
import org.xutils.http.HttpMethod;
import org.xutils.http.RequestParams;
import org.xutils.http.body.ProgressBody;
import org.xutils.http.body.RequestBody;
import org.xutils.http.cookie.DbCookieStore;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;

/**
 * Created by wyouflf on 15/7/23.
 * Uri请求发送和数据接收
 */
public class HttpRequest extends UriRequest {

    private String cacheKey = null;
    private boolean isLoading = false;
    private InputStream inputStream = null;
    private HttpURLConnection connection = null;
    private int responseCode = 0;

    // cookie manager
    private static final CookieManager COOKIE_MANAGER =
            new CookieManager(DbCookieStore.INSTANCE, CookiePolicy.ACCEPT_ALL);

    public HttpRequest(RequestParams params, Type loadType) throws Throwable {
        super(params, loadType);
    }

    // build query
    @Override
    protected String buildQueryUrl(RequestParams params) throws IOException {
        String uri = params.getUri();
        StringBuilder queryBuilder = new StringBuilder(uri);
        if (!uri.contains("?")) {
            queryBuilder.append("?");
        } else if (!uri.endsWith("?")) {
            queryBuilder.append("&");
        }
        List<KeyValue> queryParams = params.getQueryStringParams();
        if (queryParams != null) {
            for (KeyValue kv : queryParams) {
                String name = kv.key;
                String value = kv.getValueStrOrNull();
                if (!TextUtils.isEmpty(name) && value != null) {
                    queryBuilder.append(URLEncoder.encode(name, params.getCharset()).replaceAll("\\+", "%20"))
                            .append("=")
                            .append(URLEncoder.encode(value, params.getCharset()).replaceAll("\\+", "%20"))
                            .append("&");
                }
            }
        }

        if (queryBuilder.charAt(queryBuilder.length() - 1) == '&') {
            queryBuilder.deleteCharAt(queryBuilder.length() - 1);
        }

        if (queryBuilder.charAt(queryBuilder.length() - 1) == '?') {
            queryBuilder.deleteCharAt(queryBuilder.length() - 1);
        }
        return queryBuilder.toString();
    }

    @Override
    public String getRequestUri() {
        String result = queryUrl;
        if (connection != null) {
            URL url = connection.getURL();
            if (url != null) {
                result = url.toString();
            }
        }
        return result;
    }

    /**
     * invoke via Loader
     */
    @Override
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public void sendRequest() throws Throwable {
        isLoading = false;
        responseCode = 0;

        URL url = new URL(queryUrl);
        { // init connection
            Proxy proxy = params.getProxy();
            if (proxy != null) {
                connection = (HttpURLConnection) url.openConnection(proxy);
            } else {
                connection = (HttpURLConnection) url.openConnection();
            }

            // try to fix bug: accidental EOFException before API 19
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
                connection.setRequestProperty("Connection", "close");
            }

            connection.setReadTimeout(params.getReadTimeout());
            connection.setConnectTimeout(params.getConnectTimeout());
            connection.setInstanceFollowRedirects(params.getRedirectHandler() == null);
            if (connection instanceof HttpsURLConnection) {
                SSLSocketFactory sslSocketFactory = params.getSslSocketFactory();
                if (sslSocketFactory != null) {
                    ((HttpsURLConnection) connection).setSSLSocketFactory(sslSocketFactory);
                }

                HostnameVerifier hostnameVerifier = params.getHostnameVerifier();
                if (hostnameVerifier != null) {
                    ((HttpsURLConnection) connection).setHostnameVerifier(hostnameVerifier);
                }
            }
        }

        if (params.isUseCookie()) {// add cookies
            try {
                Map<String, List<String>> singleMap =
                        COOKIE_MANAGER.get(url.toURI(), new HashMap<String, List<String>>(0));
                List<String> cookies = singleMap.get("Cookie");
                if (cookies != null) {
                    connection.setRequestProperty("Cookie", TextUtils.join(";", cookies));
                }
            } catch (Throwable ex) {
                LogUtil.e(ex.getMessage(), ex);
            }
        }

        {// add headers
            List<RequestParams.Header> headers = params.getHeaders();
            if (headers != null) {
                for (RequestParams.Header header : headers) {
                    String name = header.key;
                    String value = header.getValueStrOrNull();
                    if (!TextUtils.isEmpty(name)) {
                        if (header.setHeader) {
                            connection.setRequestProperty(name, value);
                        } else {
                            connection.addRequestProperty(name, value);
                        }
                    }
                }
            }
        }

        // intercept response
        if (responseParser != null) {
            responseParser.beforeRequest(this);
        }
        if (requestInterceptListener != null) {
            requestInterceptListener.beforeRequest(this);
        }

        { // write body
            HttpMethod method = params.getMethod();
            try {
                connection.setRequestMethod(method.toString());
            } catch (ProtocolException ex) {
                try { // fix: HttpURLConnection not support PATCH method.
                    Field methodField = HttpURLConnection.class.getDeclaredField("method");
                    methodField.setAccessible(true);
                    methodField.set(connection, method.toString());
                } catch (Throwable ignored) {
                    throw ex;
                }
            }
            if (HttpMethod.permitsRequestBody(method)) {
                RequestBody body = params.getRequestBody();
                if (body != null) {
                    if (body instanceof ProgressBody) {
                        ((ProgressBody) body).setProgressHandler(progressHandler);
                    }
                    String contentType = body.getContentType();
                    if (!TextUtils.isEmpty(contentType)) {
                        connection.setRequestProperty("Content-Type", contentType);
                    }
                    boolean isChunkedMode = false;
                    long contentLength = body.getContentLength();
                    if (contentLength < 0) {
                        connection.setChunkedStreamingMode(256 * 1024);
                        isChunkedMode = true;
                    } else {
                        if (contentLength < Integer.MAX_VALUE) {
                            connection.setFixedLengthStreamingMode((int) contentLength);
                        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                            connection.setFixedLengthStreamingMode(contentLength);
                        } else {
                            connection.setChunkedStreamingMode(256 * 1024);
                            isChunkedMode = true;
                        }
                    }

                    if (isChunkedMode) {
                        connection.setRequestProperty("Transfer-Encoding", "chunked");
                    } else {
                        connection.setRequestProperty("Content-Length", String.valueOf(contentLength));
                    }

                    connection.setDoOutput(true);
                    body.writeTo(connection.getOutputStream());
                }
            }
        }

        if (params.isUseCookie()) { // save cookies
            try {
                Map<String, List<String>> headers = connection.getHeaderFields();
                if (headers != null) {
                    COOKIE_MANAGER.put(url.toURI(), headers);
                }
            } catch (Throwable ex) {
                LogUtil.e(ex.getMessage(), ex);
            }
        }

        // check response code
        responseCode = connection.getResponseCode();
        {   // intercept response
            if (responseParser != null) {
                responseParser.afterRequest(this);
            }
            if (requestInterceptListener != null) {
                requestInterceptListener.afterRequest(this);
            }
        }
        if (responseCode == 204 || responseCode == 205) { // empty content
            throw new HttpException(responseCode, this.getResponseMessage());
        } else if (responseCode >= 300) {
            HttpException httpException = new HttpException(responseCode, this.getResponseMessage());
            try {
                httpException.setResult(IOUtil.readStr(this.getInputStream(), params.getCharset()));
            } catch (Throwable ex) {
                LogUtil.w(ex.getMessage(), ex);
            }
            LogUtil.e(httpException.toString() + ", url: " + queryUrl);
            throw httpException;
        }

        isLoading = true;
    }

    @Override
    public boolean isLoading() {
        return isLoading;
    }

    @Override
    public String getCacheKey() {
        if (cacheKey == null) {

            cacheKey = params.getCacheKey();

            if (TextUtils.isEmpty(cacheKey)) {
                cacheKey = params.toString();
            }
        }
        return cacheKey;
    }

    @Override
    public Object loadResult() throws Throwable {
        isLoading = true;
        return super.loadResult();
    }

    /**
     * 尝试从缓存获取结果, 并为请求头加入缓存控制参数.
     */
    @Override
    public Object loadResultFromCache() throws Throwable {
        isLoading = true;
        DiskCacheEntity cacheEntity = LruDiskCache.getDiskCache(params.getCacheDirName())
                .setMaxSize(params.getCacheSize())
                .get(this.getCacheKey());

        if (cacheEntity != null) {
            if (HttpMethod.permitsCache(params.getMethod())) {
                Date lastModified = cacheEntity.getLastModify();
                if (lastModified.getTime() > 0) {
                    params.setHeader("If-Modified-Since", toGMTString(lastModified));
                }
                String eTag = cacheEntity.getEtag();
                if (!TextUtils.isEmpty(eTag)) {
                    params.setHeader("If-None-Match", eTag);
                }
            }
            return loader.loadFromCache(cacheEntity);
        } else {
            return null;
        }
    }

    @Override
    public void clearCacheHeader() {
        params.setHeader("If-Modified-Since", null);
        params.setHeader("If-None-Match", null);
    }

    @Override
    public InputStream getInputStream() throws IOException {
        if (connection != null && inputStream == null) {
            inputStream = connection.getResponseCode() >= 400 ?
                    connection.getErrorStream() : connection.getInputStream();
        }
        return inputStream;
    }

    @Override
    public void close() throws IOException {
        if (inputStream != null) {
            IOUtil.closeQuietly(inputStream);
            inputStream = null;
        }
        if (connection != null) {
            connection.disconnect();
            //connection = null;
        }
    }

    @Override
    public long getContentLength() {
        long result = -1;
        if (connection != null) {
            try {
                String value = connection.getHeaderField("content-length");
                if (value != null) {
                    result = Long.parseLong(value);
                }
            } catch (Throwable ex) {
                LogUtil.e(ex.getMessage(), ex);
            }
        }
        if (result < 1) {
            try {
                result = this.getInputStream().available();
            } catch (Throwable ignored) {
            }
        }
        return result;
    }

    @Override
    public int getResponseCode() throws IOException {
        if (connection != null) {
            return responseCode;
        } else {
            if (this.getInputStream() != null) {
                return 200;
            } else {
                return 404;
            }
        }
    }

    @Override
    public String getResponseMessage() throws IOException {
        if (connection != null) {
            return URLDecoder.decode(connection.getResponseMessage(), params.getCharset());
        } else {
            return null;
        }
    }

    @Override
    public long getExpiration() {
        if (connection == null) return -1L;

        long expiration = -1L;

        // from max-age
        String cacheControl = connection.getHeaderField("Cache-Control");
        if (!TextUtils.isEmpty(cacheControl)) {
            StringTokenizer tok = new StringTokenizer(cacheControl, ",");
            while (tok.hasMoreTokens()) {
                String token = tok.nextToken().trim().toLowerCase();
                if (token.startsWith("max-age")) {
                    int eqIdx = token.indexOf('=');
                    if (eqIdx > 0) {
                        try {
                            String value = token.substring(eqIdx + 1).trim();
                            long seconds = Long.parseLong(value);
                            if (seconds > 0L) {
                                expiration = System.currentTimeMillis() + seconds * 1000L;
                            }
                        } catch (Throwable ex) {
                            LogUtil.e(ex.getMessage(), ex);
                        }
                    }
                    break;
                }
            }
        }

        // from expires
        if (expiration <= 0L) {
            expiration = connection.getExpiration();
        }

        if (expiration <= 0L && params.getCacheMaxAge() > 0L) {
            expiration = System.currentTimeMillis() + params.getCacheMaxAge();
        }

        if (expiration <= 0L) {
            expiration = Long.MAX_VALUE;
        }
        return expiration;
    }

    @Override
    public long getLastModified() {
        return getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
    }

    @Override
    public String getETag() {
        if (connection == null) return null;
        return connection.getHeaderField("ETag");
    }

    @Override
    public String getResponseHeader(String name) {
        if (connection == null) return null;
        return connection.getHeaderField(name);
    }

    @Override
    public Map<String, List<String>> getResponseHeaders() {
        if (connection == null) return null;
        return connection.getHeaderFields();
    }

    @Override
    public long getHeaderFieldDate(String name, long defaultValue) {
        if (connection == null) return defaultValue;
        return connection.getHeaderFieldDate(name, defaultValue);
    }

    private static String toGMTString(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat(
                "EEE, dd MMM y HH:mm:ss 'GMT'", Locale.US);
        TimeZone gmtZone = TimeZone.getTimeZone("GMT");
        sdf.setTimeZone(gmtZone);
        return sdf.format(date);
    }
}
