/**
 * A utility class to handle HTTP request/response.
 *
 * @author Yusuke Yamamoto - yusuke at mac.com
 */
public class HttpClient implements java.io.Serializable {
  private final int OK = 200; // OK: Success!
  private final int NOT_MODIFIED = 304; // Not Modified: There was no new data to return.
  private 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 final int NOT_AUTHORIZED =
      401; // Not Authorized: Authentication credentials were missing or incorrect.
  private final int FORBIDDEN =
      403; // Forbidden: The request is understood, but it has been refused.  An accompanying error
  // message will explain why.
  private final int NOT_FOUND =
      404; // Not Found: The URI requested is invalid or the resource requested, such as a user,
  // does not exists.
  private final int NOT_ACCEPTABLE =
      406; // Not Acceptable: Returned by the Search API when an invalid format is specified in the
  // request.
  private final int INTERNAL_SERVER_ERROR =
      500; // Internal Server Error: Something is broken.  Please post to the group so the Twitter
  // team can investigate.
  private final int BAD_GATEWAY = 502; // Bad Gateway: Twitter is down or being upgraded.
  private final int SERVICE_UNAVAILABLE =
      503; // Service Unavailable: The Twitter 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 static final boolean DEBUG = Configuration.getDebug();

  private String basic;
  private int retryCount = Configuration.getRetryCount();
  private int retryIntervalMillis = Configuration.getRetryIntervalSecs() * 1000;
  private String userId = Configuration.getUser();
  private String password = Configuration.getPassword();
  private String proxyHost = Configuration.getProxyHost();
  private int proxyPort = Configuration.getProxyPort();
  private String proxyAuthUser = Configuration.getProxyUser();
  private String proxyAuthPassword = Configuration.getProxyPassword();
  private int connectionTimeout = Configuration.getConnectionTimeout();
  private int readTimeout = Configuration.getReadTimeout();
  private static final long serialVersionUID = 808018030183407996L;
  private boolean isJDK14orEarlier = false;
  private Map<String, String> requestHeaders = new HashMap<String, String>();
  private OAuth oauth = null;
  private String requestTokenURL = "http://twitter.com/oauth/request_token";
  private String authorizationURL = "http://twitter.com/oauth/authorize";
  private String accessTokenURL = "http://twitter.com/oauth/access_token";
  private OAuthToken oauthToken = null;

  public HttpClient(String userId, String password) {
    this();
    setUserId(userId);
    setPassword(password);
  }

  public HttpClient() {
    this.basic = null;
    setUserAgent(null);
    setOAuthConsumer(null, null);
    setRequestHeader("Accept-Encoding", "gzip");

    String versionStr = System.getProperty("java.specification.version");
    if (null != versionStr) {
      isJDK14orEarlier = 1.5d > Double.parseDouble(versionStr);
    }
  }

  public void setUserId(String userId) {
    this.userId = userId;
    encodeBasicAuthenticationString();
  }

  public void setPassword(String password) {
    this.password = password;
    encodeBasicAuthenticationString();
  }

  public String getUserId() {
    return userId;
  }

  public String getPassword() {
    return password;
  }

  public boolean isAuthenticationEnabled() {
    return null != basic || null != oauth;
  }

  /**
   * Sets the consumer key and consumer secret.<br>
   * System property -Dtwitter4j.oauth.consumerKey and -Dhttp.oauth.consumerSecret override this
   * attribute.
   *
   * @param consumerKey Consumer Key
   * @param consumerSecret Consumer Secret
   * @since Twitter4J 2.0.0
   * @see <a href="http://twitter.com/oauth_clients">Applications Using Twitter</a>
   */
  public void setOAuthConsumer(String consumerKey, String consumerSecret) {
    consumerKey = Configuration.getOAuthConsumerKey(consumerKey);
    consumerSecret = Configuration.getOAuthConsumerSecret(consumerSecret);
    if (null != consumerKey
        && null != consumerSecret
        && 0 != consumerKey.length()
        && 0 != consumerSecret.length()) {
      this.oauth = new OAuth(consumerKey, consumerSecret);
    }
  }

  /**
   * @return request token
   * @throws TwitterException tw
   * @since Twitter4J 2.0.0
   */
  public RequestToken getOAuthRequestToken() throws TwitterException {
    this.oauthToken =
        new RequestToken(httpRequest(requestTokenURL, new PostParameter[0], true), this);
    return (RequestToken) this.oauthToken;
  }

  /**
   * @param token request token
   * @return access token
   * @throws TwitterException
   * @since Twitter4J 2.0.0
   */
  public AccessToken getOAuthAccessToken(RequestToken token) throws TwitterException {
    try {
      this.oauthToken = token;
      this.oauthToken = new AccessToken(httpRequest(accessTokenURL, new PostParameter[0], true));
    } catch (TwitterException te) {
      throw new TwitterException(
          "The user has not given access to the account.", te, te.getStatusCode());
    }
    return (AccessToken) this.oauthToken;
  }

  /**
   * @param token request token
   * @return access token
   * @throws TwitterException
   * @since Twitter4J 2.0.8
   */
  public AccessToken getOAuthAccessToken(RequestToken token, String pin) throws TwitterException {
    try {
      this.oauthToken = token;
      this.oauthToken =
          new AccessToken(
              httpRequest(
                  accessTokenURL,
                  new PostParameter[] {new PostParameter("oauth_verifier", pin)},
                  true));
    } catch (TwitterException te) {
      throw new TwitterException(
          "The user has not given access to the account.", te, te.getStatusCode());
    }
    return (AccessToken) this.oauthToken;
  }

  /**
   * @param token request token
   * @param tokenSecret request token secret
   * @return access token
   * @throws TwitterException
   * @since Twitter4J 2.0.1
   */
  public AccessToken getOAuthAccessToken(String token, String tokenSecret) throws TwitterException {
    try {
      this.oauthToken = new OAuthToken(token, tokenSecret) {};
      this.oauthToken = new AccessToken(httpRequest(accessTokenURL, new PostParameter[0], true));
    } catch (TwitterException te) {
      throw new TwitterException(
          "The user has not given access to the account.", te, te.getStatusCode());
    }
    return (AccessToken) this.oauthToken;
  }

  /**
   * @param token request token
   * @param tokenSecret request token secret
   * @param pin pin
   * @return access token
   * @throws TwitterException
   * @since Twitter4J 2.0.8
   */
  public AccessToken getOAuthAccessToken(String token, String tokenSecret, String pin)
      throws TwitterException {
    try {
      this.oauthToken = new OAuthToken(token, tokenSecret) {};
      this.oauthToken =
          new AccessToken(
              httpRequest(
                  accessTokenURL,
                  new PostParameter[] {new PostParameter("oauth_verifier", pin)},
                  true));
    } catch (TwitterException te) {
      throw new TwitterException(
          "The user has not given access to the account.", te, te.getStatusCode());
    }
    return (AccessToken) this.oauthToken;
  }

  /**
   * Sets the authorized access token
   *
   * @param token authorized access token
   * @since Twitter4J 2.0.0
   */
  public void setOAuthAccessToken(AccessToken token) {
    this.oauthToken = token;
  }

  public void setRequestTokenURL(String requestTokenURL) {
    this.requestTokenURL = requestTokenURL;
  }

  public String getRequestTokenURL() {
    return requestTokenURL;
  }

  public void setAuthorizationURL(String authorizationURL) {
    this.authorizationURL = authorizationURL;
  }

  public String getAuthorizationURL() {
    return authorizationURL;
  }

  public void setAccessTokenURL(String accessTokenURL) {
    this.accessTokenURL = accessTokenURL;
  }

  public String getAccessTokenURL() {
    return accessTokenURL;
  }

  public String getProxyHost() {
    return proxyHost;
  }

  /**
   * Sets proxy host. System property -Dtwitter4j.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 -Dtwitter4j.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 -Dtwitter4j.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 -Dtwitter4j.http.proxyPassword overrides
   * this attribute.
   *
   * @param proxyAuthPassword
   */
  public void setProxyAuthPassword(String proxyAuthPassword) {
    this.proxyAuthPassword = Configuration.getProxyPassword(proxyAuthPassword);
  }

  public int getConnectionTimeout() {
    return connectionTimeout;
  }

  /**
   * Sets a specified timeout value, in milliseconds, to be used when opening a communications link
   * to the resource referenced by this URLConnection. System property
   * -Dtwitter4j.http.connectionTimeout overrides this attribute.
   *
   * @param connectionTimeout - an int that specifies the connect timeout value in milliseconds
   */
  public void setConnectionTimeout(int connectionTimeout) {
    this.connectionTimeout = Configuration.getConnectionTimeout(connectionTimeout);
  }

  public int getReadTimeout() {
    return readTimeout;
  }

  /**
   * Sets the read timeout to a specified timeout, in milliseconds. System property
   * -Dtwitter4j.http.readTimeout overrides this attribute.
   *
   * @param readTimeout - an int that specifies the timeout value to be used in milliseconds
   */
  public void setReadTimeout(int readTimeout) {
    this.readTimeout = Configuration.getReadTimeout(readTimeout);
  }

  private void encodeBasicAuthenticationString() {
    if (null != userId && null != password) {
      this.basic =
          "Basic " + new String(new BASE64Encoder().encode((userId + ":" + password).getBytes()));
    }
  }

  public void setRetryCount(int retryCount) {
    if (retryCount >= 0) {
      this.retryCount = Configuration.getRetryCount(retryCount);
    } else {
      throw new IllegalArgumentException("RetryCount cannot be negative.");
    }
  }

  public void setUserAgent(String ua) {
    setRequestHeader("User-Agent", Configuration.getUserAgent(ua));
  }

  public String getUserAgent() {
    return getRequestHeader("User-Agent");
  }

  public void setRetryIntervalSecs(int retryIntervalSecs) {
    if (retryIntervalSecs >= 0) {
      this.retryIntervalMillis = Configuration.getRetryIntervalSecs(retryIntervalSecs) * 1000;
    } else {
      throw new IllegalArgumentException("RetryInterval cannot be negative.");
    }
  }

  public Response post(String url, PostParameter[] postParameters, boolean authenticated)
      throws TwitterException {
    return httpRequest(url, postParameters, authenticated);
  }

  public Response post(String url, boolean authenticated) throws TwitterException {
    return httpRequest(url, new PostParameter[0], authenticated);
  }

  public Response post(String url, PostParameter[] PostParameters) throws TwitterException {
    return httpRequest(url, PostParameters, false);
  }

  public Response post(String url) throws TwitterException {
    return httpRequest(url, new PostParameter[0], false);
  }

  public Response get(String url, boolean authenticated) throws TwitterException {
    return httpRequest(url, null, authenticated);
  }

  public Response get(String url) throws TwitterException {
    return httpRequest(url, null, false);
  }

  protected Response httpRequest(String url, PostParameter[] postParams, boolean authenticated)
      throws TwitterException {
    int retriedCount;
    int retry = retryCount + 1;
    Response res = null;
    for (retriedCount = 0; retriedCount < retry; retriedCount++) {
      int responseCode = -1;
      try {
        HttpURLConnection con = null;
        OutputStream osw = null;
        try {
          con = getConnection(url);
          con.setDoInput(true);
          setHeaders(url, postParams, con, authenticated);
          if (null != postParams) {
            con.setRequestMethod("POST");
            con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
            con.setDoOutput(true);
            String postParam = encodeParameters(postParams);
            log("Post Params: ", postParam);
            byte[] bytes = postParam.getBytes("UTF-8");

            con.setRequestProperty("Content-Length", Integer.toString(bytes.length));
            osw = con.getOutputStream();
            osw.write(bytes);
            osw.flush();
            osw.close();
          } else {
            con.setRequestMethod("GET");
          }
          res = new Response(con);
          responseCode = con.getResponseCode();
          if (DEBUG) {
            log("Response: ");
            Map<String, List<String>> responseHeaders = con.getHeaderFields();
            for (String key : responseHeaders.keySet()) {
              List<String> values = responseHeaders.get(key);
              for (String value : values) {
                if (null != key) {
                  log(key + ": " + value);
                } else {
                  log(value);
                }
              }
            }
          }
          if (responseCode != OK) {
            if (responseCode < INTERNAL_SERVER_ERROR || retriedCount == retryCount) {
              throw new TwitterException(
                  getCause(responseCode) + "\n" + res.asString(), responseCode);
            }
            // will retry if the status code is INTERNAL_SERVER_ERROR
          } else {
            break;
          }
        } finally {
          try {
            osw.close();
          } catch (Exception ignore) {
          }
        }
      } catch (IOException ioe) {
        // connection timeout or read timeout
        if (retriedCount == retryCount) {
          throw new TwitterException(ioe.getMessage(), ioe, responseCode);
        }
      }
      try {
        if (DEBUG) {
          res.asString();
        }
        log("Sleeping " + retryIntervalMillis + " millisecs for next retry.");
        Thread.sleep(retryIntervalMillis);
      } catch (InterruptedException ignore) {
        // nothing to do
      }
    }
    return res;
  }

  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].name, "UTF-8"))
            .append("=")
            .append(URLEncoder.encode(postParams[j].value, "UTF-8"));
      } catch (java.io.UnsupportedEncodingException neverHappen) {
      }
    }
    return buf.toString();
  }

  /**
   * sets HTTP headers
   *
   * @param connection HttpURLConnection
   * @param authenticated boolean
   */
  private void setHeaders(
      String url, PostParameter[] params, HttpURLConnection connection, boolean authenticated) {
    log("Request: ");
    if (null != params) {
      log("POST ", url);
    } else {
      log("GET ", url);
    }

    if (authenticated) {
      if (basic == null && oauth == null) {}
      String authorization = null;
      if (null != oauth) {
        // use OAuth
        authorization =
            oauth.generateAuthorizationHeader(
                params != null ? "POST" : "GET", url, params, oauthToken);
      } else if (null != basic) {
        // use Basic Auth
        authorization = this.basic;
      } else {
        throw new IllegalStateException(
            "Neither user ID/password combination nor OAuth consumer key/secret combination supplied");
      }
      connection.addRequestProperty("Authorization", authorization);
      log("Authorization: " + authorization);
    }
    for (String key : requestHeaders.keySet()) {
      connection.addRequestProperty(key, requestHeaders.get(key));
      log(key + ": " + requestHeaders.get(key));
    }
  }

  public void setRequestHeader(String name, String value) {
    requestHeaders.put(name, value);
  }

  public String getRequestHeader(String name) {
    return requestHeaders.get(name);
  }

  private HttpURLConnection getConnection(String url) throws IOException {
    HttpURLConnection con = null;
    if (proxyHost != null && !proxyHost.equals("")) {
      if (proxyAuthUser != null && !proxyAuthUser.equals("")) {
        log("Proxy AuthUser: "******"Proxy AuthPassword: "******"Opening proxied connection(" + proxyHost + ":" + proxyPort + ")");
      }
      con = (HttpURLConnection) new URL(url).openConnection(proxy);
    } else {
      con = (HttpURLConnection) new URL(url).openConnection();
    }
    if (connectionTimeout > 0 && !isJDK14orEarlier) {
      con.setConnectTimeout(connectionTimeout);
    }
    if (readTimeout > 0 && !isJDK14orEarlier) {
      con.setReadTimeout(readTimeout);
    }
    return con;
  }

  @Override
  public int hashCode() {
    int result = OK;
    result = 31 * result + (DEBUG ? 1 : 0);
    result = 31 * result + (basic != null ? basic.hashCode() : 0);
    result = 31 * result + retryCount;
    result = 31 * result + retryIntervalMillis;
    result = 31 * result + (userId != null ? userId.hashCode() : 0);
    result = 31 * result + (password != null ? password.hashCode() : 0);
    result = 31 * result + (proxyHost != null ? proxyHost.hashCode() : 0);
    result = 31 * result + proxyPort;
    result = 31 * result + (proxyAuthUser != null ? proxyAuthUser.hashCode() : 0);
    result = 31 * result + (proxyAuthPassword != null ? proxyAuthPassword.hashCode() : 0);
    result = 31 * result + connectionTimeout;
    result = 31 * result + readTimeout;
    result = 31 * result + (isJDK14orEarlier ? 1 : 0);
    result = 31 * result + (requestHeaders != null ? requestHeaders.hashCode() : 0);
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (null == obj) {
      return false;
    }
    if (this == obj) {
      return true;
    }
    if (obj instanceof HttpClient) {
      HttpClient that = (HttpClient) obj;
      return this.retryIntervalMillis == that.retryIntervalMillis
          && this.basic.equals(that.basic)
          && this.requestHeaders.equals(that.requestHeaders);
    }
    return false;
  }

  private void log(String message) {
    if (DEBUG) {
      System.out.println("[" + new java.util.Date() + "]" + message);
    }
  }

  private void log(String message, String message2) {
    if (DEBUG) {
      log(message + message2);
    }
  }

  private String getCause(int statusCode) {
    String cause = null;
    // http://apiwiki.twitter.com/HTTP-Response-Codes-and-Errors
    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 Twitter team can investigate.";
        break;
      case BAD_GATEWAY:
        cause = "Twitter is down or being upgraded.";
        break;
      case SERVICE_UNAVAILABLE:
        cause =
            "Service Unavailable: The Twitter 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;
  }
}
 /**
  * Sets proxy authentication password. System property -Dtwitter4j.http.proxyPassword overrides
  * this attribute.
  *
  * @param proxyAuthPassword
  */
 public void setProxyAuthPassword(String proxyAuthPassword) {
   this.proxyAuthPassword = Configuration.getProxyPassword(proxyAuthPassword);
 }