/*
 *  soapUI, copyright (C) 2004-2011 smartbear.com 
 *
 *  soapUI is free software; you can redistribute it and/or modify it under the 
 *  terms of version 2.1 of the GNU Lesser General Public License as published by 
 *  the Free Software Foundation.
 *
 *  soapUI is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 
 *  even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
 *  See the GNU Lesser General Public License for more details at gnu.org.
 */

package com.eviware.soapui.impl.wsdl.support.http;

import java.io.File;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import org.apache.commons.ssl.KeyMaterial;
import org.apache.http.Header;
import org.apache.http.HttpClientConnection;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.CookieStore;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
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.impl.auth.NTLMSchemeFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpProcessor;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.log4j.Logger;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.SoapUISystemProperties;
import com.eviware.soapui.impl.wsdl.submit.transports.http.ExtendedHttpMethod;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.metrics.SoapUIMetrics;
import com.eviware.soapui.impl.wsdl.support.CompressionSupport;
import com.eviware.soapui.model.settings.Settings;
import com.eviware.soapui.model.settings.SettingsListener;
import com.eviware.soapui.settings.HttpSettings;
import com.eviware.soapui.settings.SSLSettings;

/**
 * HttpClient related tools
 * 
 * @author Ole.Matzura
 */

public class HttpClientSupport
{
	private final static Helper helper = new Helper();

	/**
	 * Internal helper to ensure synchronized access..
	 */

	public static class SoapUIHttpClient extends DefaultHttpClient
	{
		public SoapUIHttpClient( final ClientConnectionManager conman )
		{
			super( conman, null );
		}

		@Override
		protected HttpRequestExecutor createRequestExecutor()
		{
			return new SoapUIHttpRequestExecutor();
		}

	}

	public static class SoapUIHttpRequestExecutor extends HttpRequestExecutor
	{

		@Override
		public void preProcess( final HttpRequest request, final HttpProcessor processor, final HttpContext context )
				throws HttpException, IOException
		{
			HttpRequest original = request;

			if( original instanceof RequestWrapper )
			{
				RequestWrapper w = ( RequestWrapper )request;
				original = w.getOriginal();
			}

			if( original instanceof ExtendedHttpMethod )
			{
				SoapUIMetrics metrics = ( ( ExtendedHttpMethod )original ).getMetrics();
				metrics.getConnectTimer().stop();
				metrics.getTimeToFirstByteTimer().start();
			}
			super.preProcess( request, processor, context );
		}

		@Override
		protected HttpResponse doSendRequest( HttpRequest request, HttpClientConnection conn, HttpContext context )
				throws IOException, HttpException
		{
			HttpResponse response = super.doSendRequest( request, conn, context );
			return response;
		}

		@Override
		protected HttpResponse doReceiveResponse( final HttpRequest request, final HttpClientConnection conn,
				final HttpContext context ) throws HttpException, IOException
		{
			if( request == null )
			{
				throw new IllegalArgumentException( "HTTP request may not be null" );
			}
			if( conn == null )
			{
				throw new IllegalArgumentException( "HTTP connection may not be null" );
			}
			if( context == null )
			{
				throw new IllegalArgumentException( "HTTP context may not be null" );
			}

			HttpResponse response = null;
			int statuscode = 0;

			HttpRequest original = request;

			if( original instanceof RequestWrapper )
			{
				RequestWrapper w = ( RequestWrapper )request;
				original = w.getOriginal();
			}

			while( response == null || statuscode < HttpStatus.SC_OK )
			{
				response = conn.receiveResponseHeader();

				SoapUIMetrics metrics = null;
				if( original instanceof ExtendedHttpMethod )
				{
					metrics = ( ( ExtendedHttpMethod )original ).getMetrics();
					metrics.getTimeToFirstByteTimer().stop();
					metrics.getReadTimer().start();
				}

				if( canResponseHaveBody( request, response ) )
				{
					conn.receiveResponseEntity( response );
					//	if( metrics != null ) {
					//	metrics.getReadTimer().stop();
					// }
				}

				statuscode = response.getStatusLine().getStatusCode();

				if( conn.getMetrics() instanceof SoapUIMetrics )
				{
					SoapUIMetrics connectionMetrics = ( SoapUIMetrics )conn.getMetrics();

					if( metrics != null && connectionMetrics != null && !connectionMetrics.isDone() )
					{
						metrics.getDNSTimer().set( connectionMetrics.getDNSTimer().getStart(),
								connectionMetrics.getDNSTimer().getStop() );
						// reset connection-level metrics
						connectionMetrics.reset();
					}
				}

			} // while intermediate response

			if(original instanceof ExtendedHttpMethod ) {
				ExtendedHttpMethod extendedHttpMethod = (ExtendedHttpMethod)original;
				extendedHttpMethod.afterReadResponse(((SoapUIMultiThreadedHttpConnectionManager.SoapUIBasicPooledConnAdapter ) conn).getSSLSession());
			}

			return response;
		}
	}

	private static class Helper
	{
		private final SoapUIHttpClient httpClient;
		private final static Logger log = Logger.getLogger( HttpClientSupport.Helper.class );
		private final SoapUIMultiThreadedHttpConnectionManager connectionManager;
		private final SchemeRegistry registry;

		public Helper()
		{
			Settings settings = SoapUI.getSettings();
			registry = new SchemeRegistry();
			registry.register( new Scheme( "http", 80, PlainSocketFactory.getSocketFactory() ) );

			try
			{
				SoapUISSLSocketFactory socketFactory = initSocketFactory();
				registry.register( new Scheme( "https", 443, socketFactory ) );
			}
			catch( Throwable e )
			{
				SoapUI.logError( e );
			}

			connectionManager = new SoapUIMultiThreadedHttpConnectionManager( registry );
			connectionManager.setMaxTotal( ( int )settings.getLong( HttpSettings.MAX_TOTAL_CONNECTIONS, 2000 ) );
			connectionManager
					.setDefaultMaxPerRoute( ( int )settings.getLong( HttpSettings.MAX_CONNECTIONS_PER_HOST, 500 ) );

			httpClient = new SoapUIHttpClient( connectionManager );

			// this interceptor needs to be last one added and executed.
			httpClient.addRequestInterceptor( new HeaderRequestInterceptor(), httpClient.getRequestInterceptorCount() );
			httpClient.getAuthSchemes().register( AuthPolicy.NTLM, new NTLMSchemeFactory() );
			httpClient.getAuthSchemes().register( AuthPolicy.SPNEGO, new NTLMSchemeFactory() );

			settings.addSettingsListener( new SSLSettingsListener() );
		}

		public SoapUIHttpClient getHttpClient()
		{
			return httpClient;
		}

		public HttpResponse execute( ExtendedHttpMethod method, HttpContext httpContext ) throws ClientProtocolException,
				IOException
		{
			method.afterWriteRequest();
			if( method.getMetrics() != null )
			{
				method.getMetrics().getConnectTimer().start();
			}
			HttpResponse httpResponse = httpClient.execute( method, httpContext );
			method.setHttpResponse( httpResponse );

			return httpResponse;
		}

		public HttpResponse execute( ExtendedHttpMethod method ) throws ClientProtocolException, IOException
		{
			method.afterWriteRequest();
			if( method.getMetrics() != null )
			{
				method.getMetrics().getConnectTimer().start();
			}
			HttpResponse httpResponse = httpClient.execute( method );
			method.setHttpResponse( httpResponse );
			return httpResponse;
		}

		public final class SSLSettingsListener implements SettingsListener
		{
			@Override
			public void settingChanged( String name, String newValue, String oldValue )
			{

				if( name.equals( SSLSettings.KEYSTORE ) || name.equals( SSLSettings.KEYSTORE_PASSWORD ) )
				{
					try
					{
						log.info( "Updating keyStore.." );
						registry.register( new Scheme( "https", 443, initSocketFactory() ) );
					}
					catch( Throwable e )
					{
						SoapUI.logError( e );
					}
				}
				else if( name.equals( HttpSettings.MAX_CONNECTIONS_PER_HOST ) )
				{
					log.info( "Updating max connections per host to " + newValue );
					connectionManager.setDefaultMaxPerRoute( Integer.parseInt( newValue ) );
				}
				else if( name.equals( HttpSettings.MAX_TOTAL_CONNECTIONS ) )
				{
					log.info( "Updating max total connections host to " + newValue );
					connectionManager.setMaxTotal( Integer.parseInt( newValue ) );
				}
			}

			@Override
			public void settingsReloaded()
			{
				try
				{
					log.info( "Updating keyStore.." );
					registry.register( new Scheme( "https", 443, initSocketFactory() ) );
				}
				catch( Throwable e )
				{
					SoapUI.logError( e );
				}
			}
		}

		public SoapUISSLSocketFactory initSocketFactory() throws KeyStoreException, NoSuchAlgorithmException,
				CertificateException, IOException, UnrecoverableKeyException, KeyManagementException
		{
			KeyStore keyStore = null;
			Settings settings = SoapUI.getSettings();

			String keyStoreUrl = System.getProperty( SoapUISystemProperties.SOAPUI_SSL_KEYSTORE_LOCATION,
					settings.getString( SSLSettings.KEYSTORE, null ) );

			keyStoreUrl = keyStoreUrl != null ? keyStoreUrl.trim() : "";

			String pass = System.getProperty( SoapUISystemProperties.SOAPUI_SSL_KEYSTORE_PASSWORD,
					settings.getString( SSLSettings.KEYSTORE_PASSWORD, "" ) );

			char[] pwd = pass.toCharArray();

			if( keyStoreUrl.trim().length() > 0 )
			{
				File f = new File( keyStoreUrl );
				if( f.exists() )
				{
					log.info( "Initializing KeyStore" );

					try
					{
						KeyMaterial km = new KeyMaterial( f, pwd );
						keyStore = km.getKeyStore();
					}
					catch( Exception e )
					{
						SoapUI.logError( e );
					}
				}
			}

			return new SoapUISSLSocketFactory( keyStore, pass );
		}
	}

	public static SoapUIHttpClient getHttpClient()
	{
		return helper.getHttpClient();
	}

	public static HttpResponse execute( ExtendedHttpMethod method, HttpContext httpContext )
			throws ClientProtocolException, IOException
	{
		return helper.execute( method, httpContext );
	}

	public static HttpResponse execute( ExtendedHttpMethod method ) throws ClientProtocolException, IOException
	{
		return helper.execute( method );
	}

	public static void applyHttpSettings( HttpRequest httpMethod, Settings settings )
	{
		// user agent?
		String userAgent = settings.getString( HttpSettings.USER_AGENT, null );
		if( userAgent != null && userAgent.length() > 0 )
			httpMethod.setHeader( "User-Agent", userAgent );

		// timeout?
		long timeout = settings.getLong( HttpSettings.SOCKET_TIMEOUT, HttpSettings.DEFAULT_SOCKET_TIMEOUT );
		httpMethod.getParams().setParameter( CoreConnectionPNames.SO_TIMEOUT, ( int )timeout );
	}

	public static String getResponseCompressionType( HttpResponse httpResponse )
	{
		Header contentType = null;
		if( httpResponse.getEntity() != null )
		{
			contentType = httpResponse.getEntity().getContentType();
		}

		Header contentEncoding = null;
		if( httpResponse.getEntity() != null )
		{
			contentEncoding = httpResponse.getEntity().getContentEncoding();
		}

		return getCompressionType( contentType == null ? null : contentType.getValue(), contentEncoding == null ? null
				: contentEncoding.getValue() );
	}

	public static String getCompressionType( String contentType, String contentEncoding )
	{
		String compressionAlg = contentType == null ? null : CompressionSupport.getAvailableAlgorithm( contentType );
		if( compressionAlg != null )
			return compressionAlg;

		if( contentEncoding == null )
			return null;
		else
			return CompressionSupport.getAvailableAlgorithm( contentEncoding );
	}

	public static void addSSLListener( Settings settings )
	{
		settings.addSettingsListener( helper.new SSLSettingsListener() );
	}

	public static BasicHttpContext createEmptyContext()
	{
		BasicHttpContext httpContext = new BasicHttpContext();

		// always use local cookie store so we don't share cookies with other threads/executions/requests
		CookieStore cookieStore = new BasicCookieStore();
		httpContext.setAttribute( ClientContext.COOKIE_STORE, cookieStore );

		return httpContext;
	}

}
