package com.nifty.cloud.ncs.http;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.ProtocolException;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.utils.URIUtils;
import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.client.EntityEnclosingRequestWrapper;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

import com.nifty.cloud.ncs.NiftyCloudClientException;
import com.nifty.cloud.ncs.NiftyCloudInputStreamEntity;

/**
 * HTTP通信を行うHTTPクライアントです。
 *
 */
public class HttpClient
{
	private static final Log log = LogFactory.getLog(HttpClient.class);

	/*
	 * Internal HTTP client
	 * 内部HTTPクライアント
	 */
	protected DefaultHttpClient httpClient;

	public static final String DEFAULT_ENCODING = "UTF-8";

	private static final long MAX_BACKOFF_TIME = 30 * 1000;

	protected ClientProperties properties;

    /**
     * Constructs a new NIFCLOUD Service client using the specified
     * client configuration options (ex: max retry attempts, proxy settings, etc).
     * 指定した{@link ClientProperties}を使用してHttpClientを構築します。
     *
     * @param properties
     *            Configuration options specifying how this client will
     *            communicate with NIFCLOUD Service
     *             (ex: proxy settings, retry count, etc.).
     *             HttpClientの設定情報()
     */
	public HttpClient(ClientProperties properties)
	{
		this.properties = properties;
		setProperties();
	}

	/**
	 * URIおよび要求パラメータを指定してPOSTメソッドのHTTP要求を生成します。
	 *
	 * @param uri
	 * 	接続先URI
	 * @param params
	 * 	要求パラメータ
	 * @return POSTメソッドのHTTP要求
	 */
	protected HttpRequestBase createPostMethod(URI uri, ArrayList<NameValuePair> params)
	{
		HttpPost post = new HttpPost(uri);
		if (params != null) {
			try {
				post.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
			} catch (UnsupportedEncodingException e) {
				throw (new NiftyCloudClientException("UnsupportedEncodingException", e));
			}
		}

		return (post);
	}

	/**
	 * URIおよび要求パラメータを指定してGETメソッドのHTTP要求を生成します。
	 *
	 * @param uri
	 * 	接続先URI
	 * @param params
	 * 	要求パラメータ
	 * @return GETメソッドのHTTP要求
	 */
	protected HttpRequestBase createGetMethod(URI uri, ArrayList<NameValuePair> params)
	{
		String schme = uri.getScheme();
		String host = uri.getHost();
		int port = uri.getPort();
		String path = uri.getRawPath();
		URI uriWithQuery = uri;

		if (params != null) {
			try {
				uriWithQuery = URIUtils.createURI(schme, host, port, path, URLEncodedUtils.format(params, "utf-8"), null);
			} catch (URISyntaxException e) {
				throw (new NiftyCloudClientException("URISyntaxException", e));
			}
		}
		HttpGet get = new HttpGet(uriWithQuery);

		return (get);
	}

	/**
	 * URI、要求パラメータ、および{@link HttpRequest}を指定してPUTメソッドのHTTP要求を生成します。
	 *
	 * @param uri
	 * 	接続先URI
	 * @param params
	 * 	要求パラメータ
	 * @param request
	 * 	HTTP要求の元となる{@link HttpRequest}
	 * @return PUTメソッドのHTTP要求
	 */
	protected HttpRequestBase createPutMethod(URI uri, ArrayList<NameValuePair> params, HttpRequest request)
	{
		String schme = uri.getScheme();
		String host = uri.getHost();
		int port = uri.getPort();
		String path = uri.getRawPath();
		URI uriWithQuery = uri;

		if (params != null) {
			try {
				uriWithQuery = URIUtils.createURI(schme, host, port, path, URLEncodedUtils.format(params, "utf-8"), null);
			} catch (URISyntaxException e) {
				throw (new NiftyCloudClientException("URISyntaxException", e));
			}
		}
		HttpPut put = new HttpPut(uriWithQuery);
		/*
		 * If you want disable 100-continue for PUT method, you should be set parameter.
		 */
		/*
		put.getParams().setBooleanParameter(CoreProtocolPNames.USE_EXPECT_CONTINUE, false);
		*/
		if (request.getContent() != null) {
			NiftyCloudInputStreamEntity entity = new NiftyCloudInputStreamEntity(request);
			put.setEntity(entity);
		}

		return (put);
	}

	/**
	 * URIおよび要求パラメータを指定してDELETEメソッドのHTTP要求を生成します。
	 *
	 * @param uri
	 * 	接続先URI
	 * @param params
	 * 	要求パラメータ
	 * @return DELETEメソッドのHTTP要求
	 */
	protected HttpRequestBase createDeleteMethod(URI uri, ArrayList<NameValuePair> params)
	{
		String schme = uri.getScheme();
		String host = uri.getHost();
		int port = uri.getPort();
		String path = uri.getRawPath();
		URI uriWithQuery = uri;

		if (params != null) {
			try {
				uriWithQuery = URIUtils.createURI(schme, host, port, path, URLEncodedUtils.format(params, "utf-8"), null);
			} catch (URISyntaxException e) {
				throw (new NiftyCloudClientException("URISyntaxException", e));
			}
		}
		HttpDelete delete = new HttpDelete(uriWithQuery);

		return (delete);
	}

	/**
	 * URIおよび要求パラメータを指定してHEADメソッドのHTTP要求を生成します。
	 *
	 * @param uri
	 * 	接続先URI
	 * @param params
	 * 	要求パラメータ
	 * @return HEADメソッドのHTTP要求
	 */
	protected HttpRequestBase createHeadMethod(URI uri, ArrayList<NameValuePair> params)
	{
		String schme = uri.getScheme();
		String host = uri.getHost();
		int port = uri.getPort();
		String path = uri.getRawPath();
		URI uriWithQuery = uri;

		if (params != null) {
			try {
				uriWithQuery = URIUtils.createURI(schme, host, port, path, URLEncodedUtils.format(params, "utf-8"), null);
			} catch (URISyntaxException e) {
				throw (new NiftyCloudClientException("URISyntaxException", e));
			}
		}
		HttpHead head = new HttpHead(uriWithQuery);

		return (head);
	}

	/**
	 * 要求が正常終了かどうかの判定を行います。
	 *
	 * @param response
	 * 	HTTP応答
	 * @return
	 * 	正常終了(ステータスコード=200)の場合<code>true</code>、そうでなければ<code>false</code>
	 */
	protected boolean isSuccess(org.apache.http.HttpResponse response)
	{
		int statusCode = response.getStatusLine().getStatusCode();

		if ((statusCode / 100) == (HttpStatus.SC_OK / 100)) {
			return (true);
		}

		return (false);
	}

	/**
	 * Create a HTTP response from apache's HTTP response object.
	 * apacheのHttpResponse、およびHTTPメソッドを指定してHTTP応答を生成します。
	 *
	 * @param httpResponse
	 *  Apache's HTTP response.
	 *  apacheのHttpResponse
	 * @param httpMethod
	 * 	HTTPメソッド
	 * @return
	 *  HTTP response.
	 *  HTTP応答
	 */
	protected HttpResponse createHttpResponse(org.apache.http.HttpResponse httpResponse, String httpMethod) throws IllegalStateException, IOException
	{
		HttpResponse response = new HttpResponse();

		response.setStatusCode(httpResponse.getStatusLine().getStatusCode());
		if (httpResponse.getEntity() != null) {
			response.setContent(httpResponse.getEntity().getContent());
		}
		log.debug("response: " + httpResponse.getStatusLine().toString());

		for (Header header : httpResponse.getAllHeaders()) {
			response.addHeader(header.getName(), header.getValue());
		}

		response.setHttpMethod(httpMethod);

		return (response);
	}

	/**
	 * Sleep on retry.
	 * リトライ時、指定したリトライ回数に応じて待機します。
	 *
	 * @param retry
	 *  Current retry count.
	 *  リトライ回数
	 */
	protected void exponentiallyWait(int retry)
	{
		long waitTime = (long)(Math.pow(3, retry) * 100L);
		waitTime = Math.min(waitTime, MAX_BACKOFF_TIME);
		log.debug("Error detected, will retry in " + waitTime + " msec, retries: " + retry);

		try {
			Thread.sleep(waitTime);
		} catch (InterruptedException e) {
			// NOP
		}
	}

    /**
     * Configure HttpClient with set of defaults as well as configuration.
     * Httpクライアントの設定を行います。
     */
	private void setProperties()
	{
		String userAgent = properties.getUserAgent();

		HttpParams httpParams = new BasicHttpParams();
		HttpProtocolParams.setUserAgent(httpParams, userAgent);
		HttpProtocolParams.setVersion(httpParams, HttpVersion.HTTP_1_1);
		SchemeRegistry schemeRegistry = new SchemeRegistry();

		TrustManager easyTrustManager = new X509TrustManager() {
		    @Override
		    public void checkClientTrusted(
		            X509Certificate[] chain,
		            String authType) throws CertificateException {
		    }

		    @Override
		    public void checkServerTrusted(
		            X509Certificate[] chain,
		            String authType) throws CertificateException {
		    }

		    @Override
		    public X509Certificate[] getAcceptedIssuers() {
		        return null;
		    }
		};

		SSLContext sslcontext;
		try {
			sslcontext = SSLContext.getInstance("SSL");
			sslcontext.init(null, new TrustManager[] { easyTrustManager }, null);
		} catch (Exception e) {
			throw (new NiftyCloudClientException(e.getMessage(), e));
		}
		SSLSocketFactory sf = new SSLSocketFactory(sslcontext, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
		schemeRegistry.register(new Scheme("https", 443, sf));
		schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory()));

		/* set timeout */
		HttpConnectionParams.setConnectionTimeout(httpParams, properties.getConnectTimeout());
		HttpConnectionParams.setSoTimeout(httpParams, properties.getSocketTimeout());

		HttpConnectionParams.setStaleCheckingEnabled(httpParams, true);
		HttpConnectionParams.setTcpNoDelay(httpParams, true);

		int socketBufferSizeHint = properties.getSocketBufferSizeHint();
		if (socketBufferSizeHint > 0) {
			HttpConnectionParams.setSocketBufferSize(httpParams, socketBufferSizeHint);
		}

		ThreadSafeClientConnManager manager = new ThreadSafeClientConnManager(schemeRegistry);
		manager.setMaxTotal(properties.getMaxConnections());
		manager.setDefaultMaxPerRoute(properties.getMaxConnections());
		httpClient = new DefaultHttpClient(manager, httpParams);

		/* redirect follow */
		httpClient.setRedirectStrategy(new DefaultRedirectStrategy(){
			@Override
			public boolean isRedirected(
					org.apache.http.HttpRequest request,
					org.apache.http.HttpResponse response,
					org.apache.http.protocol.HttpContext context) throws ProtocolException {
				boolean isRedirected = super.isRedirected(request, response, context);
				if (!isRedirected) {
					int statusCode = response.getStatusLine().getStatusCode();
					if (statusCode == 301 || statusCode == 302 || statusCode == 307) {
						return true;
					}
				}
				return isRedirected;
			}
			@Override
			public org.apache.http.client.methods.HttpUriRequest getRedirect(
					org.apache.http.HttpRequest request,
					org.apache.http.HttpResponse response,
					org.apache.http.protocol.HttpContext context) throws ProtocolException {

				org.apache.http.client.methods.HttpUriRequest uriRequest = null;

                try {
                	URI uri = getLocationURI(request, response, context);
					String method = request.getRequestLine().getMethod();
					if (method.equalsIgnoreCase(HttpHead.METHOD_NAME)) {
						uriRequest = new HttpHead(uri);
					} else if (method.equalsIgnoreCase(HttpGet.METHOD_NAME)) {
						uriRequest = new HttpGet(uri);
					} else if (method.equalsIgnoreCase(HttpPut.METHOD_NAME)) {
						HttpPut putUriRequest = new HttpPut(uri);
						putUriRequest.setEntity(((EntityEnclosingRequestWrapper)request).getEntity());
						uriRequest = putUriRequest;
					} else if (method.equalsIgnoreCase(HttpPost.METHOD_NAME)) {
						HttpPost postUriRequest = new HttpPost(uri);
						postUriRequest.setEntity(((EntityEnclosingRequestWrapper)request).getEntity());
						postUriRequest.getEntity().getContent().reset();
	                    uriRequest = postUriRequest;
					} else if (method.equalsIgnoreCase(HttpDelete.METHOD_NAME)) {
						uriRequest = new HttpDelete(uri);
					} else {
						uriRequest = new HttpGet(uri);
					}
					if (uriRequest.getURI() != null &&
							((org.apache.http.impl.client.RequestWrapper)request).getOriginal() != null) {
						String host = uriRequest.getURI().getHost();
						((org.apache.http.impl.client.RequestWrapper)request).getOriginal().setHeader("Host", host);
					}
                } catch (IllegalStateException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
				return uriRequest;
			}
		});

		/* Set proxy */
		String proxyHost = properties.getProxyHost();
		int proxyPort = properties.getProxyPort();

		if ((proxyHost != null) && (proxyPort > 0)) {
			log.info("Proxy configuration.  Host: " + proxyHost + " port: " + proxyPort);
			HttpHost proxy = new HttpHost(proxyHost, proxyPort);
			httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
			String proxyUser = properties.getProxyUserName();
			String proxyPassword = properties.getProxyPassword();
			if ((proxyUser != null) && (proxyPassword != null)) {
				AuthScope authScope = new AuthScope(proxyHost, proxyPort);
				UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(proxyUser, proxyPassword);
				httpClient.getCredentialsProvider().setCredentials(authScope, credentials);
				httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
			}
		}
	}

	/**
	 * Shutdown this HTTP client.
	 * HTTPクライアントを終了します。
	 *
	 */
	public void shutdown()
	{
		ClientConnectionManager connectionManager = httpClient.getConnectionManager();
		connectionManager.shutdown();
	}

	@Override
	protected void finalize() throws Throwable
	{
		this.shutdown();
		super.finalize();
	}
}
