/*
 *  soapUI, copyright (C) 2004-2012 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.IOException;
import java.net.ConnectException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.concurrent.TimeUnit;

import org.apache.http.HttpHost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.ClientConnectionOperator;
import org.apache.http.conn.ClientConnectionRequest;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.conn.OperatedClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.scheme.SchemeSocketFactory;
import org.apache.http.impl.HttpConnectionMetricsImpl;
import org.apache.http.impl.conn.AbstractPoolEntry;
import org.apache.http.impl.conn.DefaultClientConnection;
import org.apache.http.impl.conn.DefaultClientConnectionOperator;
import org.apache.http.impl.conn.tsccm.BasicPoolEntry;
import org.apache.http.impl.conn.tsccm.BasicPooledConnAdapter;
import org.apache.http.impl.conn.tsccm.PoolEntryRequest;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.io.HttpTransportMetrics;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.log4j.Logger;

import com.eviware.soapui.impl.wsdl.submit.transports.http.support.metrics.SoapUIMetrics;

/**
 * Manages a set of HttpConnections for various HostConfigurations. Modified to
 * keep different pools for different keystores.
 * 
 */
public class SoapUIMultiThreadedHttpConnectionManager extends ThreadSafeClientConnManager
{

	/**
	 * Log object for this class.
	 */
	private static final Logger log = Logger.getLogger( SoapUIMultiThreadedHttpConnectionManager.class );

	/**
	 * Connection eviction policy
	 */
	IdleConnectionMonitorThread idleConnectionHandler = new IdleConnectionMonitorThread( this );

	public SoapUIMultiThreadedHttpConnectionManager( SchemeRegistry registry )
	{
		super( registry );
		idleConnectionHandler.start();
	}

	/**
	 * Hook for creating the connection operator. It is called by the
	 * constructor. Derived classes can override this method to change the
	 * instantiation of the operator. The default implementation here
	 * instantiates {@link DefaultClientConnectionOperator
	 * DefaultClientConnectionOperator}.
	 * 
	 * @param schreg
	 *           the scheme registry.
	 * 
	 * @return the connection operator to use
	 */
	@Override
	protected ClientConnectionOperator createConnectionOperator( SchemeRegistry schreg )
	{

		return new SoapUIClientConnectionOperator( schreg );// @ThreadSafe
	}

	public static class IdleConnectionMonitorThread extends Thread
	{
		private final ClientConnectionManager connMgr;
		private volatile boolean shutdown;

		public IdleConnectionMonitorThread( ClientConnectionManager connMgr )
		{
			super();
			this.connMgr = connMgr;
		}

		@Override
		public void run()
		{
			try
			{
				while( !shutdown )
				{
					synchronized( this )
					{
						wait( 5000 );
						// Close expired connections
						connMgr.closeExpiredConnections();
						// Optionally, close connections
						// that have been idle longer than 30 sec
						connMgr.closeIdleConnections( 30, TimeUnit.SECONDS );
					}
				}
			}
			catch( InterruptedException ex )
			{
				// terminate
			}
		}

		public void shutdown()
		{
			shutdown = true;
			synchronized( this )
			{
				notifyAll();
			}
		}
	}

	public ClientConnectionRequest requestConnection( final HttpRoute route, final Object state )
	{

		final PoolEntryRequest poolRequest = pool.requestPoolEntry( route, state );

		return new ClientConnectionRequest()
		{

			public void abortRequest()
			{
				poolRequest.abortRequest();
			}

			public ManagedClientConnection getConnection( long timeout, TimeUnit tunit ) throws InterruptedException,
					ConnectionPoolTimeoutException
			{
				if( route == null )
				{
					throw new IllegalArgumentException( "Route may not be null." );
				}

				if( log.isDebugEnabled() )
				{
					log.debug( "Get connection: " + route + ", timeout = " + timeout );
				}

				BasicPoolEntry entry = poolRequest.getPoolEntry( timeout, tunit );
				SoapUIBasicPooledConnAdapter connAdapter = new SoapUIBasicPooledConnAdapter(
						SoapUIMultiThreadedHttpConnectionManager.this, entry );
				return connAdapter;
			}
		};

	}

	public void releaseConnection( ManagedClientConnection conn, long validDuration, TimeUnit timeUnit )
	{

		if( !( conn instanceof SoapUIBasicPooledConnAdapter ) )
		{
			throw new IllegalArgumentException( "Connection class mismatch, "
					+ "connection not obtained from this manager." );
		}
		SoapUIBasicPooledConnAdapter hca = ( SoapUIBasicPooledConnAdapter )conn;
		if( ( hca.getPoolEntry() != null ) && ( hca.getManager() != this ) )
		{
			throw new IllegalArgumentException( "Connection not obtained from this manager." );
		}
		synchronized( hca )
		{
			BasicPoolEntry entry = ( BasicPoolEntry )hca.getPoolEntry();
			if( entry == null )
			{
				return;
			}
			try
			{
				// make sure that the response has been read completely
				if( hca.isOpen() && !hca.isMarkedReusable() )
				{
					// In MTHCM, there would be a call to
					// SimpleHttpConnectionManager.finishLastResponse(conn);
					// Consuming the response is handled outside in 4.0.

					// make sure this connection will not be re-used
					// Shut down rather than close, we might have gotten here
					// because of a shutdown trigger.
					// Shutdown of the adapter also clears the tracked route.
					hca.shutdown();
				}
			}
			catch( IOException iox )
			{
				if( log.isDebugEnabled() )
					log.debug( "Exception shutting down released connection.", iox );
			}
			finally
			{
				boolean reusable = hca.isMarkedReusable();
				if( log.isDebugEnabled() )
				{
					if( reusable )
					{
						log.debug( "Released connection is reusable." );
					}
					else
					{
						log.debug( "Released connection is not reusable." );
					}
				}
				hca.detach();
				pool.freeEntry( entry, reusable, validDuration, timeUnit );
			}
		}
	}

	private class SoapUIClientConnectionOperator extends DefaultClientConnectionOperator
	{
		public SoapUIClientConnectionOperator( SchemeRegistry schemes )
		{
			super( schemes );
		}

		@Override
		public OperatedClientConnection createConnection()
		{
			SoapUIDefaultClientConnection connection = new SoapUIDefaultClientConnection();
			return connection;
		}

		@Override
		public void openConnection( final OperatedClientConnection conn, final HttpHost target, final InetAddress local,
				final HttpContext context, final HttpParams params ) throws IOException
		{
			if( conn == null )
			{
				throw new IllegalArgumentException( "Connection may not be null" );
			}
			if( target == null )
			{
				throw new IllegalArgumentException( "Target host may not be null" );
			}
			if( params == null )
			{
				throw new IllegalArgumentException( "Parameters may not be null" );
			}
			if( conn.isOpen() )
			{
				throw new IllegalStateException( "Connection must not be open" );
			}

			Scheme schm = schemeRegistry.getScheme( target.getSchemeName() );
			SchemeSocketFactory sf = schm.getSchemeSocketFactory();

			//long start = System.nanoTime();
			long start = System.currentTimeMillis();
			InetAddress[] addresses = resolveHostname( target.getHostName() );
			// long dnsEnd = System.nanoTime();
			long dnsEnd = System.currentTimeMillis();

			int port = schm.resolvePort( target.getPort() );
			for( int i = 0; i < addresses.length; i++ )
			{
				InetAddress address = addresses[i];
				boolean last = i == addresses.length - 1;

				Socket sock = sf.createSocket( params );
				conn.opening( sock, target );

				InetSocketAddress remoteAddress = new InetSocketAddress( address, port );
				InetSocketAddress localAddress = null;
				if( local != null )
				{
					localAddress = new InetSocketAddress( local, 0 );
				}
				if( log.isDebugEnabled() )
				{
					log.debug( "Connecting to " + remoteAddress );
				}
				try
				{
					Socket connsock = sf.connectSocket( sock, remoteAddress, localAddress, params );
					if( sock != connsock )
					{
						sock = connsock;
						conn.opening( sock, target );
					}
					prepareSocket( sock, context, params );
					conn.openCompleted( sf.isSecure( sock ), params );

					SoapUIMetrics metrics = ( SoapUIMetrics )conn.getMetrics();

					if( metrics != null )
					{
						metrics.getDNSTimer().set( start, dnsEnd );
					}

					return;
				}
				catch( ConnectException ex )
				{
					if( last )
					{
						throw new HttpHostConnectException( target, ex );
					}
				}
				catch( ConnectTimeoutException ex )
				{
					if( last )
					{
						throw ex;
					}
				}
				if( log.isDebugEnabled() )
				{
					log.debug( "Connect to " + remoteAddress + " timed out. "
							+ "Connection will be retried using another IP address" );
				}
			}
		}
	}

	private class SoapUIDefaultClientConnection extends DefaultClientConnection
	{

		public SoapUIDefaultClientConnection()
		{
			super();
		}

		@Override
		/**
		 * @since 4.1
		 */
		protected HttpConnectionMetricsImpl createConnectionMetrics( final HttpTransportMetrics inTransportMetric,
				final HttpTransportMetrics outTransportMetric )
		{
			return new SoapUIMetrics( inTransportMetric, outTransportMetric );
		}
	}

	static class SoapUIBasicPooledConnAdapter extends BasicPooledConnAdapter
	{

		protected SoapUIBasicPooledConnAdapter( ThreadSafeClientConnManager tsccm, AbstractPoolEntry entry )
		{
			super( tsccm, entry );
		}

		@Override
		protected ClientConnectionManager getManager()
		{
			// override needed only to make method visible in this package
			return super.getManager();
		}

		@Override
		protected AbstractPoolEntry getPoolEntry()
		{
			// override needed only to make method visible in this package
			return super.getPoolEntry();
		}

		@Override
		protected void detach()
		{
			// override needed only to make method visible in this package
			super.detach();
		}
	}

}
