package weibo4j.http;

import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.multipart.*;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.log4j.Logger;
import weibo4j.model.*;
import weibo4j.org.json.JSONException;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

/**
 * @author sinaWeibo
 */
public class HttpClient implements java.io.Serializable {

    private static final long serialVersionUID = -176092625883595547L;
    private static final int OK = 200;// OK: Success!
    private static final int NOT_MODIFIED = 304;// Not Modified: There was no new data to return.
    private static final int BAD_REQUEST = 400;// Bad Request: The request was invalid.  An accompanying error message will explain why. This is the status code will be returned during rate limiting.
    private static final int NOT_AUTHORIZED = 401;// Not Authorized: Authentication credentials were missing or incorrect.
    private static final int FORBIDDEN = 403;// Forbidden: The request is understood, but it has been refused.  An accompanying error message will explain why.
    private static final int NOT_FOUND = 404;// Not Found: The URI requested is invalid or the resource requested, such as a user, does not exists.
    private static final int NOT_ACCEPTABLE = 406;// Not Acceptable: Returned by the Search API when an invalid format is specified in the request.
    private static final int INTERNAL_SERVER_ERROR = 500;// Internal Server Error: Something is broken.  Please post to the group so the Weibo team can investigate.
    private static final int BAD_GATEWAY = 502;// Bad Gateway: Weibo is down or being upgraded.
    private static final int SERVICE_UNAVAILABLE = 503;// Service Unavailable: The Weibo servers are up, but overloaded with requests. Try again later. The search and trend methods use this to indicate when you are being rate limited.

    private String proxyHost = Configuration.getProxyHost();
    private int proxyPort = Configuration.getProxyPort();
    private String proxyAuthUser = Configuration.getProxyUser();
    private String proxyAuthPassword = Configuration.getProxyPassword();

    public String getProxyHost() {
        return proxyHost;
    }

    /**
     * Sets proxy host. System property -Dsinat4j.http.proxyHost or
     * http.proxyHost overrides this attribute.
     *
     * @param proxyHost
     */
    public void setProxyHost(String proxyHost) {
        this.proxyHost = Configuration.getProxyHost(proxyHost);
    }

    public int getProxyPort() {
        return proxyPort;
    }

    /**
     * Sets proxy port. System property -Dsinat4j.http.proxyPort or
     * -Dhttp.proxyPort overrides this attribute.
     *
     * @param proxyPort
     */
    public void setProxyPort(int proxyPort) {
        this.proxyPort = Configuration.getProxyPort(proxyPort);
    }

    public String getProxyAuthUser() {
        return proxyAuthUser;
    }

    /**
     * Sets proxy authentication user. System property -Dsinat4j.http.proxyUser
     * overrides this attribute.
     *
     * @param proxyAuthUser
     */
    public void setProxyAuthUser(String proxyAuthUser) {
        this.proxyAuthUser = Configuration.getProxyUser(proxyAuthUser);
    }

    public String getProxyAuthPassword() {
        return proxyAuthPassword;
    }

    /**
     * Sets proxy authentication password. System property
     * -Dsinat4j.http.proxyPassword overrides this attribute.
     *
     * @param proxyAuthPassword
     */
    public void setProxyAuthPassword(String proxyAuthPassword) {
        this.proxyAuthPassword = Configuration.getProxyPassword(proxyAuthPassword);
    }

    private final static boolean DEBUG = Configuration.getDebug();
    static Logger log = Logger.getLogger(HttpClient.class.getName());
    org.apache.commons.httpclient.HttpClient client = null;

    private MultiThreadedHttpConnectionManager connectionManager;
    private int maxSize;

    public HttpClient() {
        this(150, 30000, 30000, 1024 * 1024);
    }

    public HttpClient(int maxConPerHost, int conTimeOutMs, int soTimeOutMs, int maxSize) {
        connectionManager = new MultiThreadedHttpConnectionManager();
        HttpConnectionManagerParams params = connectionManager.getParams();
        params.setDefaultMaxConnectionsPerHost(maxConPerHost);
        params.setConnectionTimeout(conTimeOutMs);
        params.setSoTimeout(soTimeOutMs);

        HttpClientParams clientParams = new HttpClientParams();
        // 忽略cookie 避免 Cookie rejected 警告
        clientParams.setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
        client = new org.apache.commons.httpclient.HttpClient(clientParams, connectionManager);
        Protocol myhttps = new Protocol("https", new MySSLSocketFactory(), 443);
        Protocol.registerProtocol("https", myhttps);
        this.maxSize = maxSize;
        // 支持proxy
        if (proxyHost != null && !proxyHost.equals("")) {
            client.getHostConfiguration().setProxy(proxyHost, proxyPort);
            client.getParams().setAuthenticationPreemptive(true);
            if (proxyAuthUser != null && !proxyAuthUser.equals("")) {
                client.getState().setProxyCredentials(AuthScope.ANY,
                        new UsernamePasswordCredentials(proxyAuthUser, proxyAuthPassword));
                log("Proxy AuthUser: " + proxyAuthUser);
                log("Proxy AuthPassword: " + proxyAuthPassword);
            }
        }
    }

    /**
     * log调试
     */
    private static void log(String message) {
        if (DEBUG) {
            log.debug(message);
        }
    }

    /**
     * 处理http getmethod 请求
     */

    public Response get(String url, String token) throws WeiboException {

        return get(url, new PostParameter[0], token);

    }

    public Response get(String url, PostParameter[] params, String token) throws WeiboException {
        log("Request:");
        log("GET:" + url);
        if (null != params && params.length > 0) {
            String encodedParams = HttpClient.encodeParameters(params);
            if (-1 == url.indexOf("?")) {
                url += "?" + encodedParams;
            }
            else {
                url += "&" + encodedParams;
            }
        }
        GetMethod getmethod = new GetMethod(url);
        return httpRequest(getmethod, token);

    }

    public Response get(String url, PostParameter[] params, Paging paging, String token) throws WeiboException {
        if (null != paging) {
            List<PostParameter> pagingParams = new ArrayList<PostParameter>(4);
            if (-1 != paging.getMaxId()) {
                pagingParams.add(new PostParameter("max_id", String.valueOf(paging.getMaxId())));
            }
            if (-1 != paging.getSinceId()) {
                pagingParams.add(new PostParameter("since_id", String.valueOf(paging.getSinceId())));
            }
            if (-1 != paging.getPage()) {
                pagingParams.add(new PostParameter("page", String.valueOf(paging.getPage())));
            }
            if (-1 != paging.getCount()) {
                if (-1 != url.indexOf("search")) {
                    // search api takes "rpp"
                    pagingParams.add(new PostParameter("rpp", String.valueOf(paging.getCount())));
                }
                else {
                    pagingParams.add(new PostParameter("count", String.valueOf(paging.getCount())));
                }
            }
            PostParameter[] newparams = null;
            PostParameter[] arrayPagingParams = pagingParams.toArray(new PostParameter[pagingParams.size()]);
            if (null != params) {
                newparams = new PostParameter[params.length + pagingParams.size()];
                System.arraycopy(params, 0, newparams, 0, params.length);
                System.arraycopy(arrayPagingParams, 0, newparams, params.length, pagingParams.size());
            }
            else {
                if (0 != arrayPagingParams.length) {
                    String encodedParams = HttpClient.encodeParameters(arrayPagingParams);
                    if (-1 != url.indexOf("?")) {
                        url += "&" + encodedParams;
                    }
                    else {
                        url += "?" + encodedParams;
                    }
                }
            }
            return get(url, newparams, token);
        }
        else {
            return get(url, params, token);
        }
    }

    /**
     * 处理http deletemethod请求
     */

    public Response delete(String url, PostParameter[] params, String token) throws WeiboException {
        if (0 != params.length) {
            String encodedParams = HttpClient.encodeParameters(params);
            if (-1 == url.indexOf("?")) {
                url += "?" + encodedParams;
            }
            else {
                url += "&" + encodedParams;
            }
        }
        DeleteMethod deleteMethod = new DeleteMethod(url);
        return httpRequest(deleteMethod, token);

    }

    /**
     * 处理http post请求
     */

    public Response post(String url, PostParameter[] params, String token) throws WeiboException {
        return post(url, params, true, token);

    }

    public Response post(String url, PostParameter[] params, Boolean WithTokenHeader, String token)
            throws WeiboException {
        log("Request:");
        log("POST" + url);
        PostMethod postMethod = new PostMethod(url);
        for (int i = 0; i < params.length; i++) {
            postMethod.addParameter(params[i].getName(), params[i].getValue());
        }
        HttpMethodParams param = postMethod.getParams();
        param.setContentCharset("UTF-8");
        return httpRequest(postMethod, WithTokenHeader, token);
    }

    /**
     * 支持multipart方式上传图片
     */
    public Response multPartURL(String url, PostParameter[] params, ImageItem item, String token)
            throws WeiboException {
        PostMethod postMethod = new PostMethod(url);
        try {
            Part[] parts = null;
            if (params == null) {
                parts = new Part[1];
            }
            else {
                parts = new Part[params.length + 1];
            }
            if (params != null) {
                int i = 0;
                for (PostParameter entry : params) {
                    parts[i++] = new StringPart(entry.getName(), (String) entry.getValue());
                }
                parts[parts.length - 1] = new ByteArrayPart(item.getContent(), item.getName(), item.getContentType());
            }
            postMethod.setRequestEntity(new MultipartRequestEntity(parts, postMethod.getParams()));

            return httpRequest(postMethod, token);

        }
        catch (Exception ex) {
            throw new WeiboException(ex.getMessage(), ex, -1);
        }
    }

    public Response multPartURL(String fileParamName, String url, PostParameter[] params, File file,
                                boolean authenticated, String token) throws WeiboException {
        PostMethod postMethod = new PostMethod(url);
        try {
            Part[] parts = null;
            if (params == null) {
                parts = new Part[1];
            }
            else {
                parts = new Part[params.length + 1];
            }
            if (params != null) {
                int i = 0;
                for (PostParameter entry : params) {
                    parts[i++] = new StringPart(entry.getName(), (String) entry.getValue());
                }
            }
            FilePart filePart = new FilePart(fileParamName, file.getName(), file,
                    new MimetypesFileTypeMap().getContentType(file), "UTF-8");
            filePart.setTransferEncoding("binary");
            parts[parts.length - 1] = filePart;

            postMethod.setRequestEntity(new MultipartRequestEntity(parts, postMethod.getParams()));
            return httpRequest(postMethod, token);
        }
        catch (Exception ex) {
            throw new WeiboException(ex.getMessage(), ex, -1);
        }
    }

    public Response httpRequest(HttpMethod method, String token) throws WeiboException {
        return httpRequest(method, true, token);
    }

    public Response httpRequest(HttpMethod method, Boolean WithTokenHeader, String token) throws WeiboException {
        InetAddress ipaddr;
        int responseCode = -1;
        try {
            ipaddr = InetAddress.getLocalHost();
            List<Header> headers = new ArrayList<Header>();
            if (WithTokenHeader) {
                if (token == null) {
                    throw new IllegalStateException("Oauth2 token is not set!");
                }
                headers.add(new Header("Authorization", "OAuth2 " + token));
                headers.add(new Header("API-RemoteIP", ipaddr.getHostAddress()));
                client.getHostConfiguration().getParams().setParameter("http.default-headers", headers);
                for (Header hd : headers) {
                    log(hd.getName() + ": " + hd.getValue());
                }
            }

            method.getParams()
                    .setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler(3, false));
            client.executeMethod(method);
            Header[] resHeader = method.getResponseHeaders();
            responseCode = method.getStatusCode();
            log("Response:");
            log("https StatusCode:" + String.valueOf(responseCode));

            for (Header header : resHeader) {
                log(header.getName() + ":" + header.getValue());
            }
            Response response = new Response();
            response.setResponseAsString(method.getResponseBodyAsString());
            log(response.toString() + "\n");

            if (responseCode != OK) {
                try {
                    throw new WeiboException(getCause(responseCode), response.asJSONObject(), method.getStatusCode());
                }
                catch (JSONException e) {
                    e.printStackTrace();
                }
            }
            return response;

        }
        catch (IOException ioe) {
            throw new WeiboException(ioe.getMessage(), ioe, responseCode);
        }
        finally {
            method.releaseConnection();
        }

    }

    /*
     * 对parameters进行encode处理
     */
    public static String encodeParameters(PostParameter[] postParams) {
        StringBuffer buf = new StringBuffer();
        for (int j = 0; j < postParams.length; j++) {
            if (j != 0) {
                buf.append("&");
            }
            try {
                buf.append(URLEncoder.encode(postParams[j].getName(), "UTF-8")).append("=")
                        .append(URLEncoder.encode(postParams[j].getValue(), "UTF-8"));
            }
            catch (java.io.UnsupportedEncodingException neverHappen) {
            }
        }
        return buf.toString();
    }

    private static class ByteArrayPart extends PartBase {
        private byte[] mData;
        private String mName;

        public ByteArrayPart(byte[] data, String name, String type) throws IOException {
            super(name, type, "UTF-8", "binary");
            mName = name;
            mData = data;
        }

        protected void sendData(OutputStream out) throws IOException {
            out.write(mData);
        }

        protected long lengthOfData() throws IOException {
            return mData.length;
        }

        protected void sendDispositionHeader(OutputStream out) throws IOException {
            super.sendDispositionHeader(out);
            StringBuilder buf = new StringBuilder();
            buf.append("; filename=\"").append(mName).append("\"");
            out.write(buf.toString().getBytes());
        }
    }

    private static String getCause(int statusCode) {
        String cause = null;
        switch (statusCode) {
            case NOT_MODIFIED:
                break;
            case BAD_REQUEST:
                cause = "The request was invalid.  An accompanying error message will explain why. This is the status code will be returned during rate limiting.";
                break;
            case NOT_AUTHORIZED:
                cause = "Authentication credentials were missing or incorrect.";
                break;
            case FORBIDDEN:
                cause = "The request is understood, but it has been refused.  An accompanying error message will explain why.";
                break;
            case NOT_FOUND:
                cause = "The URI requested is invalid or the resource requested, such as a user, does not exists.";
                break;
            case NOT_ACCEPTABLE:
                cause = "Returned by the Search API when an invalid format is specified in the request.";
                break;
            case INTERNAL_SERVER_ERROR:
                cause = "Something is broken.  Please post to the group so the Weibo team can investigate.";
                break;
            case BAD_GATEWAY:
                cause = "Weibo is down or being upgraded.";
                break;
            case SERVICE_UNAVAILABLE:
                cause = "Service Unavailable: The Weibo servers are up, but overloaded with requests. Try again later. The search and trend methods use this to indicate when you are being rate limited.";
                break;
            default:
                cause = "";
        }
        return statusCode + ":" + cause;
    }


}
