/*
 *  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.wsdl;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.NTCredentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.params.AuthPolicy;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.support.CompressionSupport;
import com.eviware.soapui.impl.wsdl.support.PathUtils;
import com.eviware.soapui.impl.wsdl.support.http.HttpClientSupport;
import com.eviware.soapui.impl.wsdl.support.http.ProxyUtils;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.model.propertyexpansion.DefaultPropertyExpansionContext;
import com.eviware.soapui.model.settings.Settings;
import com.eviware.soapui.settings.HttpSettings;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.UISupport;
import com.eviware.soapui.support.swing.SwingWorker;
import com.eviware.soapui.support.types.StringToStringMap;
import com.eviware.x.form.XForm;
import com.eviware.x.form.XFormDialog;
import com.eviware.x.form.XFormDialogBuilder;
import com.eviware.x.form.XFormFactory;

/**
 * WsdlLoader for URLs
 * 
 * @author ole.matzura
 */

public class UrlWsdlLoader extends WsdlLoader
{
	private HttpContext state;
	protected HttpGet getMethod;
	private boolean aborted;
	protected Map<String, byte[]> urlCache = new HashMap<String, byte[]>();
	protected boolean finished;
	private boolean useWorker;
	private ModelItem contextModelItem;
	private org.apache.http.HttpResponse httpResponse;

	public UrlWsdlLoader( String url )
	{
		this( url, null );
	}

	public UrlWsdlLoader( String url, ModelItem contextModelItem )
	{
		super( url );
		this.contextModelItem = contextModelItem;
		state = new BasicHttpContext();
	}

	public boolean isUseWorker()
	{
		return useWorker;
	}

	public void setUseWorker( boolean useWorker )
	{
		this.useWorker = useWorker;
	}

	public InputStream load() throws Exception
	{
		return load( getBaseURI() );
	}

	public synchronized InputStream load( String url ) throws Exception
	{
		if( !PathUtils.isHttpPath( url ) )
		{
			try
			{
				File file = new File( url.replace( '/', File.separatorChar ) );
				if( file.exists() )
					url = file.toURI().toURL().toString();
			}
			catch( Exception e )
			{
			}
		}

		if( urlCache.containsKey( url ) )
		{
			setNewBaseURI( url );
			return new ByteArrayInputStream( urlCache.get( url ) );
		}

		if( url.startsWith( "file:" ) )
		{
			return handleFile( url );
		}

		log.debug( "Getting wsdl component from [" + url + "]" );

		createGetMethod( url );

		if( aborted )
			return null;

		LoaderWorker worker = new LoaderWorker();
		if( useWorker )
			worker.start();
		else
			worker.construct();

		while( !aborted && !finished )
		{
			Thread.sleep( 200 );
		}

		// wait for method to catch up - required in unit tests..
		// limited looping to 10 loops because of eclipse plugin which entered 
		// endless loop without it
		int counter = 0;
		byte[] content = null;

		if( httpResponse != null && httpResponse.getEntity() != null )
		{
			content = EntityUtils.toByteArray( new BufferedHttpEntity( httpResponse.getEntity() ) );
		}

		while( !aborted && content == null && counter < 10 )
		{
			Thread.sleep( 200 );
			counter++ ;
		}

		if( aborted )
		{
			throw new Exception( "Load of url [" + url + "] was aborted" );
		}
		else
		{
			if( content != null )
			{
				String compressionAlg = HttpClientSupport.getResponseCompressionType( httpResponse );
				if( compressionAlg != null )
					content = CompressionSupport.decompress( compressionAlg, content );

				urlCache.put( url, content );
				String newUrl = getMethod.getURI().toString();
				if( !url.equals( newUrl ) )
					log.info( "BaseURI was redirected to [" + newUrl + "]" );
				setNewBaseURI( newUrl );
				urlCache.put( newUrl, content );
				return new ByteArrayInputStream( content );
			}
			else
			{
				throw new Exception( "Failed to load url; " + url + ", "
						+ ( httpResponse != null ? httpResponse.getStatusLine().getStatusCode() : 0 ) + " - "
						+ ( httpResponse != null ? httpResponse.getStatusLine().getReasonPhrase() : "" ) );
			}
		}
	}

	protected InputStream handleFile( String url ) throws Exception
	{
		setNewBaseURI( url );
		return new URL( url ).openStream();
	}

	protected void createGetMethod( String url )
	{
		getMethod = new HttpGet( url );
		getMethod.getParams().setParameter( ClientPNames.HANDLE_REDIRECTS, true );
		state.setAttribute( ClientContext.CREDS_PROVIDER, new WsdlCredentialsProvider() );

		if( SoapUI.getSettings().getBoolean( HttpSettings.AUTHENTICATE_PREEMPTIVELY ) )
		{
			if( !StringUtils.isNullOrEmpty( getUsername() ) && !StringUtils.isNullOrEmpty( getPassword() ) )
			{
				UsernamePasswordCredentials creds = new UsernamePasswordCredentials( getUsername(), getPassword() );
				getMethod.addHeader( BasicScheme.authenticate( creds, "utf-8", false ) );
			}
		}
	}

	public final class LoaderWorker extends SwingWorker
	{
		public Object construct()
		{
			HttpClientSupport.SoapUIHttpClient httpClient = HttpClientSupport.getHttpClient();
			try
			{
				Settings soapuiSettings = SoapUI.getSettings();

				HttpClientSupport.applyHttpSettings( getMethod, soapuiSettings );
				ProxyUtils.initProxySettings( soapuiSettings, getMethod, state, getMethod.getURI().toString(),
						contextModelItem == null ? null : new DefaultPropertyExpansionContext( contextModelItem ) );

				httpResponse = httpClient.execute( getMethod, state );
			}
			catch( Exception e )
			{
				return e;
			}
			finally
			{
				finished = true;
			}

			return null;
		}
	}

	public boolean abort()
	{
		if( getMethod != null )
			getMethod.abort();

		aborted = true;

		return true;
	}

	public boolean isAborted()
	{
		return aborted;
	}

	/**
	 * CredentialsProvider for providing login information during WSDL loading
	 * 
	 * @author ole.matzura
	 */

	private static Map<AuthScope, Credentials> cache = new HashMap<AuthScope, Credentials>();

	public final class WsdlCredentialsProvider implements CredentialsProvider
	{
		private XFormDialog basicDialog;
		private XFormDialog ntDialog;

		public WsdlCredentialsProvider()
		{
		}

		public Credentials getCredentials( final AuthScope authScope )
		{
			if( authScope == null )
			{
				throw new IllegalArgumentException( "Authentication scope may not be null" );
			}

			//	if( cache.containsKey( authScope ) )
			//	{
			//	return cache.get( authScope );
			//	}

			String pw = getPassword();
			if( pw == null )
				pw = "";

			if( AuthPolicy.NTLM.equalsIgnoreCase( authScope.getScheme() )
					|| AuthPolicy.SPNEGO.equalsIgnoreCase( authScope.getScheme() ) )
			{
				String workstation = "";
				try
				{
					workstation = InetAddress.getLocalHost().getHostName();
				}
				catch( UnknownHostException e )
				{
				}

				if( hasCredentials() )
				{
					log.info( "Returning url credentials" );
					return new NTCredentials( getUsername(), pw, workstation, null );
				}

				log.info( authScope.getHost() + ":" + authScope.getPort() + " requires Windows authentication" );
				if( ntDialog == null )
				{
					buildNtDialog();
				}

				StringToStringMap values = new StringToStringMap();
				values.put( "Info", "Authentication required for [" + authScope.getHost() + ":" + authScope.getPort() + "]" );
				ntDialog.setValues( values );

				if( ntDialog.show() )
				{
					values = ntDialog.getValues();
					NTCredentials credentials = new NTCredentials( values.get( "Username" ), values.get( "Password" ),
							workstation, values.get( "Domain" ) );

					cache.put( authScope, credentials );
					return credentials;
				}
			}
			else if( AuthPolicy.BASIC.equalsIgnoreCase( authScope.getScheme() )
					|| AuthPolicy.DIGEST.equalsIgnoreCase( authScope.getScheme() ) )
			{
				if( hasCredentials() )
				{
					log.info( "Returning url credentials" );
					UsernamePasswordCredentials credentials = new UsernamePasswordCredentials( getUsername(), pw );
					cache.put( authScope, credentials );
					return credentials;
				}

				log.info( authScope.getHost() + ":" + authScope.getPort() + " requires authentication with the realm '"
						+ authScope.getRealm() + "'" );
				ShowDialog showDialog = new ShowDialog();
				showDialog.values.put( "Info",
						"Authentication required for [" + authScope.getHost() + ":" + authScope.getPort() + "]" );

				UISupport.getUIUtils().runInUIThreadIfSWT( showDialog );
				if( showDialog.result )
				{
					UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(
							showDialog.values.get( "Username" ), showDialog.values.get( "Password" ) );
					cache.put( authScope, credentials );
					return credentials;
				}
			}

			return null;
		}

		private void buildBasicDialog()
		{
			XFormDialogBuilder builder = XFormFactory.createDialogBuilder( "Basic Authentication" );
			XForm mainForm = builder.createForm( "Basic" );
			mainForm.addLabel( "Info", "" );
			mainForm.addTextField( "Username", "Username for authentication", XForm.FieldType.TEXT );
			mainForm.addTextField( "Password", "Password for authentication", XForm.FieldType.PASSWORD );

			basicDialog = builder.buildDialog( builder.buildOkCancelActions(), "Specify Basic Authentication Credentials",
					UISupport.OPTIONS_ICON );
		}

		private void buildNtDialog()
		{
			XFormDialogBuilder builder = XFormFactory.createDialogBuilder( "NT Authentication" );
			XForm mainForm = builder.createForm( "Basic" );
			mainForm.addLabel( "Info", "" );
			mainForm.addTextField( "Username", "Username for authentication", XForm.FieldType.TEXT );
			mainForm.addTextField( "Password", "Password for authentication", XForm.FieldType.PASSWORD );
			mainForm.addTextField( "Domain", "NT Domain for authentication", XForm.FieldType.TEXT );

			ntDialog = builder.buildDialog( builder.buildOkCancelActions(), "Specify NT Authentication Credentials",
					UISupport.OPTIONS_ICON );
		}

		private class ShowDialog implements Runnable
		{
			StringToStringMap values = new StringToStringMap();
			boolean result;

			public void run()
			{
				if( basicDialog == null )
					buildBasicDialog();

				basicDialog.setValues( values );

				result = basicDialog.show();
				if( result )
				{
					values = basicDialog.getValues();
				}
			}
		}

		public void clear()
		{
			cache.clear();
		}

		public void setCredentials( AuthScope arg0, Credentials arg1 )
		{
		}
	}

	public void close()
	{
	}
}
