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

import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextField;

import org.apache.ws.security.WSConstants;
import org.apache.ws.security.WSSecurityException;
import org.apache.ws.security.message.WSSecHeader;
import org.apache.ws.security.message.WSSecSAMLToken;
import org.apache.ws.security.saml.WSSecSignatureSAML;
import org.apache.ws.security.saml.ext.AssertionWrapper;
import org.apache.ws.security.saml.ext.SAMLParms;
import org.apache.xml.security.algorithms.MessageDigestAlgorithm;
import org.apache.xml.security.signature.XMLSignature;
import org.w3c.dom.Document;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.WSSEntryConfig;
import com.eviware.soapui.impl.wsdl.support.wss.OutgoingWss;
import com.eviware.soapui.impl.wsdl.support.wss.WssCrypto;
import com.eviware.soapui.impl.wsdl.support.wss.saml.callback.SAML1CallbackHandler;
import com.eviware.soapui.impl.wsdl.support.wss.saml.callback.SAML2CallbackHandler;
import com.eviware.soapui.impl.wsdl.support.wss.saml.callback.SAMLCallbackHandler;
import com.eviware.soapui.impl.wsdl.support.wss.support.KeystoresComboBoxModel;
import com.eviware.soapui.impl.wsdl.support.wss.support.SAMLAttributeValuesTable;
import com.eviware.soapui.model.propertyexpansion.PropertyExpansionContext;
import com.eviware.soapui.model.propertyexpansion.PropertyExpansionsResult;
import com.eviware.soapui.support.components.SimpleBindingForm;
import com.eviware.soapui.support.types.StringToStringMap;
import com.eviware.soapui.support.xml.XmlObjectConfigurationBuilder;
import com.eviware.soapui.support.xml.XmlObjectConfigurationReader;
import com.google.common.base.Strings;
import com.jgoodies.binding.PresentationModel;

/**
 * @author Erik R. Yverling
 *
 *         Used to generate a SAML assertion using various input components
 */
public class AutomaticSAMLEntry extends WssEntryBase
{
	public static final String TYPE = "SAML (Form)";

	public static final String SAML_VERSION_1 = "1.1";
	public static final String SAML_VERSION_2 = "2.0";

	public static final String AUTHENTICATION_ASSERTION_TYPE = "Authentication";
	public static final String ATTRIBUTE_ASSERTION_TYPE = "Attribute";
	public static final String AUTHORIZATION_ASSERTION_TYPE = "Authorization";

	public static final String ATTRIBUTE_VALUES_VALUE_COLUMN = "value";

	public static final String HOLDER_OF_KEY_CONFIRMATION_METHOD = "Holder-of-key";
	public static final String SENDER_VOUCHES_CONFIRMATION_METHOD = "Sender vouches";

	private static final String NOT_A_VALID_SAML_VERSION = "Not a valid SAML version" ;

	private KeyAliasComboBoxModel keyAliasComboBoxModel;
	private InternalWssContainerListener wssContainerListener;

	private String samlVersion;
	private String assertionType;
	private String confirmationMethod;
	private String crypto;
	private String issuer;
	private String subjectName;
	private String subjectQualifier;
	private String digestAlgorithm;
	private String signatureAlgorithm;
	private boolean signed;
	private String attributeName;
	private List<StringToStringMap> attributeValues;

	private SimpleBindingForm form;
	private JCheckBox signedCheckBox;
	private JComboBox confirmationMethodComboBox;
	private JComboBox cryptoComboBox;
	private JComboBox keyAliasComboBox;
	private JPasswordField passwordField;
	private JTextField attributeNameTextField;
	private SAMLAttributeValuesTable samlAttributeValuesTable;

	public void init( WSSEntryConfig config, OutgoingWss container )
	{
		super.init( config, container, TYPE );
	}

	// FIXME How can we make FindBugs that these fields will always be initialized and be able to add NonNull annotations?
	@Override
	protected void load( XmlObjectConfigurationReader reader )
	{
		samlVersion = reader.readString( "samlVersion", SAML_VERSION_1 );
		signed = reader.readBoolean( "signed", false );
		assertionType = reader.readString( "assertionType", AUTHENTICATION_ASSERTION_TYPE );
		confirmationMethod = reader.readString( "confirmationMethod", SENDER_VOUCHES_CONFIRMATION_METHOD );
		crypto = reader.readString( "crypto", null );
		issuer = reader.readString( "issuer", null );
		subjectName = reader.readString( "subjectName", null );
		subjectQualifier = reader.readString( "subjectQualifier", null );
		digestAlgorithm = reader.readString( "digestAlgorithm", MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA1 );
		signatureAlgorithm = reader.readString( "signatureAlgorithm", WSConstants.RSA );
		attributeName = reader.readString( "attributeName", null );
		attributeValues = readTableValues( reader, "attributeValues" );
	}

	@Override
	protected void save( XmlObjectConfigurationBuilder builder )
	{
		builder.add( "samlVersion", samlVersion );
		builder.add( "signed", signed );
		builder.add( "assertionType", assertionType );
		builder.add( "confirmationMethod", confirmationMethod );
		builder.add( "crypto", crypto );
		builder.add( "issuer", issuer );
		builder.add( "subjectName", subjectName );
		builder.add( "subjectQualifier", subjectQualifier );
		builder.add( "digestAlgorithm", digestAlgorithm );
		builder.add( "signatureAlgorithm", signatureAlgorithm );
		builder.add( "attributeName", attributeName );
		saveTableValues( builder, attributeValues, "attributeValues" );
	}

	@Override
	protected JComponent buildUI()
	{
		wssContainerListener = new InternalWssContainerListener();
		getWssContainer().addWssContainerListener( wssContainerListener );

		form = new SimpleBindingForm( new PresentationModel<SignatureEntry>( this ) );

		form.addSpace( 5 );

		form.appendComboBox( "samlVersion", "SAML version", new String[] { SAML_VERSION_1, SAML_VERSION_2 },
				"Choose the SAML version" );

		signedCheckBox = form.appendCheckBox( "signed", "Signed", null );
		signedCheckBox.addItemListener( new ItemListener()
		{
			@Override
			public void itemStateChanged( ItemEvent e )
			{
				checkSigned();
			}

		} );

		form.appendComboBox( "assertionType", "Assertion type",
				new String[] { AUTHENTICATION_ASSERTION_TYPE, ATTRIBUTE_ASSERTION_TYPE, AUTHORIZATION_ASSERTION_TYPE },
				"Choose the type of assertion" ).addItemListener( new ItemListener()
		{
			@Override
			public void itemStateChanged( ItemEvent e )
			{
				checkAssertionType();
			}

		} );

		confirmationMethodComboBox = form.appendComboBox( "confirmationMethod", "Confirmation method",
				new String[] { SENDER_VOUCHES_CONFIRMATION_METHOD }, "Choose the confirmation method" );

		cryptoComboBox = form.appendComboBox( "crypto", "Keystore", new KeystoresComboBoxModel( getWssContainer(),
				getWssContainer().getCryptoByName( crypto ), true ),
				"Selects the Keystore containing the key to use for signing the SAML message" );

		cryptoComboBox.addItemListener( new ItemListener()
		{
			@Override
			public void itemStateChanged( ItemEvent e )
			{
				// FIXME This cases the drop down to be blank when changing keystore
				keyAliasComboBoxModel.update( getWssContainer().getCryptoByName( crypto ) );
			}
		} );

		keyAliasComboBoxModel = new KeyAliasComboBoxModel( getWssContainer().getCryptoByName( crypto ) );
		keyAliasComboBox = form.appendComboBox( "username", "Alias", keyAliasComboBoxModel,
				"The alias for the key to use for encryption" );

		passwordField = form.appendPasswordField( "password", "Password", "The certificate password" );

		form.appendTextField( "issuer", "Issuer", "The issuer" );

		form.appendTextField( "subjectName", "Subject Name", "The subject qualifier" );

		form.appendTextField( "subjectQualifier", "Subject Qualifier", "The subject qualifier" );

		form.appendComboBox( "digestAlgorithm", "Digest Algorithm", new String[] {
				MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA1, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256,
				MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA384, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA512 },
				"Set the digest algorithm to use" );

		form.appendComboBox( "signatureAlgorithm", "Signature Algorithm", new String[] { WSConstants.RSA,
				WSConstants.DSA, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA384,
				XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA512, XMLSignature.ALGO_ID_MAC_HMAC_SHA1,
				XMLSignature.ALGO_ID_MAC_HMAC_SHA256, XMLSignature.ALGO_ID_MAC_HMAC_SHA384,
				XMLSignature.ALGO_ID_MAC_HMAC_SHA512 }, "Set the name of the signature encryption algorithm to use" );

		attributeNameTextField = form.appendTextField( "attributeName", "Attribute name", "The name of the attribute" );

		samlAttributeValuesTable = new SAMLAttributeValuesTable( attributeValues, this );
		form.append( "Attribute values", samlAttributeValuesTable );

		initComponentsEnabledState();

		return new JScrollPane( form.getPanel() );
	}

	private void initComponentsEnabledState()
	{
		checkSigned();
		checkAssertionType();
	}

	private void checkSigned()
	{
		if( !signed )
		{
			form.setComboBoxItems( "confirmationMethod", confirmationMethodComboBox,
					new String[] { SENDER_VOUCHES_CONFIRMATION_METHOD } );
			confirmationMethodComboBox.setSelectedIndex( 0 );
			cryptoComboBox.setEnabled( false );
			keyAliasComboBox.setEnabled( false );
			passwordField.setEnabled( false );
		}
		else
		{
			form.setComboBoxItems( "confirmationMethod", confirmationMethodComboBox, new String[] {
					SENDER_VOUCHES_CONFIRMATION_METHOD, HOLDER_OF_KEY_CONFIRMATION_METHOD } );
			cryptoComboBox.setEnabled( true );
			keyAliasComboBox.setEnabled( true );
			passwordField.setEnabled( true );
		}
	}

	private void checkAssertionType()
	{
		if( assertionType.equals( AUTHORIZATION_ASSERTION_TYPE ) )
		{
			signed = false;
			signedCheckBox.setSelected( false );
			signedCheckBox.setEnabled( false );
		}
		else
		{
			signedCheckBox.setEnabled( true );
		}

		if( assertionType.equals( ATTRIBUTE_ASSERTION_TYPE ) )
		{
			attributeNameTextField.setEnabled( true );
			samlAttributeValuesTable.setEnabled( true );
		}
		else
		{
			attributeNameTextField.setEnabled( false );
			samlAttributeValuesTable.setEnabled( false );
		}
	}

	public void process( WSSecHeader secHeader, Document doc, PropertyExpansionContext context ) {
		try
		{
			SAMLParms samlParms = new SAMLParms();
			SAMLCallbackHandler callbackHandler = null;

			if( !signed )
			{
				WSSecSAMLToken wsSecSAMLToken = new WSSecSAMLToken();

				if( samlVersion.equals( SAML_VERSION_1 ) )
				{
					callbackHandler = new SAML1CallbackHandler( assertionType, confirmationMethod );
				}
				else if( samlVersion.equals( SAML_VERSION_2 ) )
				{
					callbackHandler = new SAML2CallbackHandler( assertionType, confirmationMethod );
				}
				else {
					throw  new IllegalArgumentException(NOT_A_VALID_SAML_VERSION);
				}
				AssertionWrapper assertion = createAssertion( context, samlParms, callbackHandler );
				wsSecSAMLToken.build( doc, assertion, secHeader );
			}
			else
			{
				WSSecSignatureSAML wsSecSignatureSAML = new WSSecSignatureSAML();
				WssCrypto wssCrypto = getWssContainer().getCryptoByName( crypto, true );
				String alias = context.expand( getUsername());

				if( wssCrypto == null )
				{
					throw new RuntimeException( "Missing keystore [" + crypto + "] for signature entry" );
				}
				else if (Strings.isNullOrEmpty(alias))
				{
					throw new RuntimeException(" No alias was provided for the keystore '" + crypto + "'. Please check your SAML (Form) configurations");
				}

				if( samlVersion.equals( SAML_VERSION_1 ) )
				{
					callbackHandler = new SAML1CallbackHandler( wssCrypto.getCrypto(), alias,
							assertionType, confirmationMethod );
				}
				else if( samlVersion.equals( SAML_VERSION_2 ) )
				{
					callbackHandler = new SAML2CallbackHandler( wssCrypto.getCrypto(), alias ,
							assertionType, confirmationMethod );
				}
				else {
					throw  new IllegalArgumentException(NOT_A_VALID_SAML_VERSION);
				}

				AssertionWrapper assertion = createAssertion( context, samlParms, callbackHandler );

				assertion.signAssertion( context.expand( getUsername() ), context.expand( getPassword() ),
						wssCrypto.getCrypto(), false );

				wsSecSignatureSAML.setUserInfo( context.expand( getUsername() ), context.expand( getPassword() ) );

				if( confirmationMethod.equals( SENDER_VOUCHES_CONFIRMATION_METHOD ) )
				{
					wsSecSignatureSAML.setKeyIdentifierType( WSConstants.BST_DIRECT_REFERENCE );

					wsSecSignatureSAML.build( doc, null, assertion, wssCrypto.getCrypto(), context.expand( getUsername() ),
							context.expand( getPassword() ), secHeader );
				}
				else if( confirmationMethod.equals( HOLDER_OF_KEY_CONFIRMATION_METHOD ) )
				{
					wsSecSignatureSAML.setDigestAlgo( digestAlgorithm );

					if( assertionType.equals( AUTHENTICATION_ASSERTION_TYPE ) )
					{
						wsSecSignatureSAML.setKeyIdentifierType( WSConstants.BST_DIRECT_REFERENCE );
						wsSecSignatureSAML.setSignatureAlgorithm( signatureAlgorithm );
					}
					else if( assertionType.equals( ATTRIBUTE_ASSERTION_TYPE ) )
					{

						wsSecSignatureSAML.setKeyIdentifierType( WSConstants.X509_KEY_IDENTIFIER );
						wsSecSignatureSAML.setSignatureAlgorithm( signatureAlgorithm );

						byte[] ephemeralKey = callbackHandler.getEphemeralKey();
						wsSecSignatureSAML.setSecretKey( ephemeralKey );
					}

					wsSecSignatureSAML.build( doc, wssCrypto.getCrypto(), assertion, null, null, null, secHeader );
				}
			}

		}
		catch( Exception e )
		{
			SoapUI.logError( e );
		}
	}

	private AssertionWrapper createAssertion( PropertyExpansionContext context, SAMLParms samlParms,
											  SAMLCallbackHandler callbackHandler ) throws WSSecurityException
	{
		if( assertionType.equals( ATTRIBUTE_ASSERTION_TYPE ) )
		{
			callbackHandler.setCustomAttributeName( context.expand( attributeName ) );
			callbackHandler.setCustomAttributeValues( extractValueColumnValues( attributeValues, context ) );
		}

		callbackHandler.setIssuer( context.expand( issuer ) );
		callbackHandler.setSubjectName( context.expand( subjectName ) );
		callbackHandler.setSubjectQualifier( context.expand( subjectQualifier ) );

		samlParms.setCallbackHandler( callbackHandler );
		return new AssertionWrapper( samlParms );
	}

	// Since we only use one column for the attribute values
	private List<String> extractValueColumnValues( List<StringToStringMap> table, PropertyExpansionContext context )
	{
		List<String> firstColumnValues = new ArrayList<String>();
		for( StringToStringMap row : table )
		{
			String columnValue = row.get( ATTRIBUTE_VALUES_VALUE_COLUMN );
			// TODO Add property expansion to each value
			firstColumnValues.add( columnValue );
		}
		return firstColumnValues;
	}

	public void relase()
	{
		if( wssContainerListener != null )
		{
			getWssContainer().removeWssContainerListener( wssContainerListener );
		}
	}

	@Override
	protected void addPropertyExpansions( PropertyExpansionsResult result )
	{
		super.addPropertyExpansions( result );
		result.extractAndAddAll( this, "issuer" );
		result.extractAndAddAll( this, "subjectName" );
		result.extractAndAddAll( this, "subjectQualifier" );
		result.extractAndAddAll( this, "attributeName" );
		// TODO Add property expansion refactoring for attributesValues, as with HttpTestRequestStep
	}

	public String getSamlVersion()
	{
		return samlVersion;
	}

	public void setSamlVersion( String samlVersion )
	{
		this.samlVersion = samlVersion;
		saveConfig();
	}

	public String getAssertionType()
	{
		return assertionType;
	}

	public void setAssertionType( String assertionType )
	{
		this.assertionType = assertionType;
		saveConfig();
	}

	public String getConfirmationMethod()
	{
		return confirmationMethod;
	}

	public void setConfirmationMethod( String confirmationMethod )
	{
		this.confirmationMethod = confirmationMethod;
		saveConfig();
	}

	public String getIssuer()
	{
		return issuer;
	}

	public void setIssuer( String issuer )
	{
		this.issuer = issuer;
		saveConfig();
	}

	public String getCrypto()
	{
		return crypto;
	}

	public void setCrypto( String crypto )
	{
		this.crypto = crypto;
		saveConfig();
	}

	public String getSubjectName()
	{
		return subjectName;
	}

	public void setSubjectName( String subjectName )
	{
		this.subjectName = subjectName;
		saveConfig();
	}

	public String getSubjectQualifier()
	{
		return subjectQualifier;
	}

	public void setSubjectQualifier( String subjectQualifier )
	{
		this.subjectQualifier = subjectQualifier;
		saveConfig();
	}

	public String getDigestAlgorithm()
	{
		return digestAlgorithm;
	}

	public void setDigestAlgorithm( String digestAlgorithm )
	{
		this.digestAlgorithm = digestAlgorithm;
		saveConfig();
	}

	public String getSignatureAlgorithm()
	{
		return signatureAlgorithm;
	}

	public void setSignatureAlgorithm( String signatureAlgorithm )
	{
		this.signatureAlgorithm = signatureAlgorithm;
		saveConfig();
	}

	public boolean isSigned()
	{
		return signed;
	}

	public void setSigned( boolean signed )
	{
		this.signed = signed;
		saveConfig();
	}

	public String getAttributeName()
	{
		return attributeName;
	}

	public void setAttributeName( String attributeName )
	{
		this.attributeName = attributeName;
		saveConfig();
	}

	public List<StringToStringMap> getAttributeValues()
	{
		return attributeValues;
	}

	public void setAttributeValues( List<StringToStringMap> attributeValues )
	{
		this.attributeValues = attributeValues;
		saveConfig();
	}

	private final class InternalWssContainerListener extends WssContainerListenerAdapter
	{
		@Override
		public void cryptoUpdated( WssCrypto crypto )
		{
			if( crypto.getLabel().equals( getCrypto() ) )
				keyAliasComboBoxModel.update( crypto );
		}
	}
}