/*
 *  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.panels.teststeps.amf;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.wsdl.submit.transports.http.support.methods.ExtendedPostMethod;
import com.eviware.soapui.impl.wsdl.support.http.HttpClientSupport;
import com.eviware.soapui.impl.wsdl.support.http.ProxyUtils;
import com.eviware.soapui.model.propertyexpansion.PropertyExpansionContext;

import flex.messaging.io.ClassAliasRegistry;
import flex.messaging.io.MessageDeserializer;
import flex.messaging.io.MessageIOConstants;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.ActionContext;
import flex.messaging.io.amf.ActionMessage;
import flex.messaging.io.amf.AmfMessageDeserializer;
import flex.messaging.io.amf.AmfMessageSerializer;
import flex.messaging.io.amf.MessageBody;
import flex.messaging.io.amf.MessageHeader;
import flex.messaging.io.amf.client.AMFHeaderProcessor;
import flex.messaging.io.amf.client.exceptions.ClientStatusException;
import flex.messaging.io.amf.client.exceptions.ServerStatusException;
import flex.messaging.io.amf.client.exceptions.ServerStatusException.HttpResponseInfo;

/**
 * AMFConnection derivate using HttpClient instead of UrlConnection
 * 
 * @author Ole
 */

public class SoapUIAMFConnection
{
	private static int DEFAULT_OBJECT_ENCODING = MessageIOConstants.AMF3;

	/**
	 * Creates a default AMF connection instance.
	 */
	public SoapUIAMFConnection()
	{
	}

	private ActionContext actionContext;
	private boolean connected;
	private int objectEncoding;
	private boolean objectEncodingSet = false;
	private SerializationContext serializationContext;
	private String url;

	private List<MessageHeader> amfHeaders;
	private AMFHeaderProcessor amfHeaderProcessor;
	private Map<String, String> httpRequestHeaders;
	private int responseCounter;

	private ExtendedPostMethod postMethod;
	private HttpContext httpState = new BasicHttpContext();
	private PropertyExpansionContext context;

	public int getObjectEncoding()
	{
		if( !objectEncodingSet )
			return DEFAULT_OBJECT_ENCODING;

		return objectEncoding;
	}

	public void setObjectEncoding( int objectEncoding )
	{
		this.objectEncoding = objectEncoding;
		objectEncodingSet = true;
	}

	public String getUrl()
	{
		return url;
	}

	/**
	 * Adds an AMF packet-level header which is sent with every request for the
	 * life of this AMF connection.
	 * 
	 * @param name
	 *           The name of the header.
	 * @param mustUnderstand
	 *           Whether the header must be processed or not.
	 * @param data
	 *           The value of the header.
	 */
	public void addAmfHeader( String name, boolean mustUnderstand, Object data )
	{
		if( amfHeaders == null )
			amfHeaders = new ArrayList<MessageHeader>();

		MessageHeader header = new MessageHeader( name, mustUnderstand, data );
		amfHeaders.add( header );
	}

	/**
	 * Add an AMF packet-level header with mustUnderstand=false, which is sent
	 * with every request for the life of this AMF connection.
	 * 
	 * @param name
	 *           The name of the header.
	 * @param data
	 *           The value of the header.
	 */
	public void addAmfHeader( String name, Object data )
	{
		addAmfHeader( name, false, data );
	}

	/**
	 * Removes any AMF headers found with the name given.
	 * 
	 * @param name
	 *           The name of the header(s) to remove.
	 * 
	 * @return true if a header existed with the given name.
	 */
	public boolean removeAmfHeader( String name )
	{
		boolean exists = false;
		if( amfHeaders != null )
		{
			for( Iterator<MessageHeader> iterator = amfHeaders.iterator(); iterator.hasNext(); )
			{
				MessageHeader header = iterator.next();
				if( name.equals( header.getName() ) )
				{
					iterator.remove();
					exists = true;
				}
			}
		}
		return exists;
	}

	/**
	 * Removes all AMF headers.
	 */
	public void removeAllAmfHeaders()
	{
		if( amfHeaders != null )
			amfHeaders = null;
	}

	/**
	 * Adds a Http request header to the underlying connection.
	 * 
	 * @param name
	 *           The name of the Http header.
	 * @param value
	 *           The value of the Http header.
	 */
	public void addHttpRequestHeader( String name, String value )
	{
		if( httpRequestHeaders == null )
			httpRequestHeaders = new HashMap<String, String>();

		httpRequestHeaders.put( name, value );
	}

	/**
	 * Removes the Http header found with the name given.
	 * 
	 * @param name
	 *           The name of the Http header.
	 * 
	 * @return true if a header existed with the given name.
	 */
	public boolean removeHttpRequestHeader( String name )
	{
		boolean exists = false;
		if( httpRequestHeaders != null )
		{
			Object previousValue = httpRequestHeaders.remove( name );
			exists = ( previousValue != null );
		}
		return exists;
	}

	/**
	 * Removes all Http request headers.
	 */
	public void removeAllHttpRequestHeaders()
	{
		if( httpRequestHeaders != null )
			httpRequestHeaders = null;
	}

	/**
	 * Makes an AMF request to the server. A connection must have been made prior
	 * to making a call.
	 * 
	 * @param command
	 *           The method to call on the server.
	 * @param arguments
	 *           Arguments for the method.
	 * 
	 * @return The result of the call.
	 * 
	 * @throws ClientStatusException
	 *            If there is a client side exception.
	 * @throws ServerStatusException
	 *            If there is a server side exception.
	 */

	public Object call( PropertyExpansionContext context, String command, Object... arguments )
			throws ClientStatusException, ServerStatusException
	{
		this.context = context;

		if( !connected )
		{
			String message = "AMF connection is not connected";
			ClientStatusException cse = new ClientStatusException( message, ClientStatusException.AMF_CALL_FAILED_CODE );
			throw cse;
		}

		String responseURI = getResponseURI();

		ActionMessage requestMessage = new ActionMessage( getObjectEncoding() );

		if( amfHeaders != null )
		{
			for( MessageHeader header : amfHeaders )
				requestMessage.addHeader( header );
		}

		MessageBody amfMessage = new MessageBody( command, responseURI, arguments );
		requestMessage.addBody( amfMessage );

		// Setup for AMF message serializer
		actionContext.setRequestMessage( requestMessage );
		ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
		AmfMessageSerializer amfMessageSerializer = new AmfMessageSerializer();
		amfMessageSerializer.initialize( serializationContext, outBuffer, null/* debugTrace */);

		try
		{
			amfMessageSerializer.writeMessage( requestMessage );
			Object result = send( outBuffer );
			return result;
		}
		catch( Exception e )
		{
			if( e instanceof ClientStatusException )
				throw ( ClientStatusException )e;
			else if( e instanceof ServerStatusException )
				throw ( ServerStatusException )e;
			// Otherwise, wrap into a ClientStatusException.
			ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CALL_FAILED_CODE );
			throw exception;
		}
		finally
		{
			try
			{
				outBuffer.close();
			}
			catch( IOException ignore )
			{
			}
		}
	}

	/**
	 * Closes the underlying URL connection, sets the url to null, and clears the
	 * cookies.
	 */
	public void close()
	{
		// Clear the URL connection and URL.
		postMethod = null;
		url = null;

		serializationContext = null;
		connected = false;
	}

	/**
	 * Connects to the URL provided. Any previous connections are closed.
	 * 
	 * @param url
	 *           The url to connect to.
	 * 
	 * @throws ClientStatusException
	 *            If there is a client side exception.
	 */
	public void connect( String url ) throws ClientStatusException
	{
		if( connected )
			close();

		this.url = url;
		try
		{
			serializationContext = new SerializationContext();
			serializationContext.createASObjectForMissingType = true;
			internalConnect();
		}
		catch( IOException e )
		{
			ClientStatusException exception = new ClientStatusException( e, ClientStatusException.AMF_CONNECT_FAILED_CODE );
			throw exception;
		}
	}

	// --------------------------------------------------------------------------
	//
	// Protected Methods
	//
	// --------------------------------------------------------------------------

	/**
	 * Generates the HTTP response info for the server status exception.
	 * 
	 * @return The HTTP response info for the server status exception.
	 */
	protected HttpResponseInfo generateHttpResponseInfo()
	{
		HttpResponseInfo httpResponseInfo = null;
		try
		{
			int responseCode = 0;
			if( postMethod.hasHttpResponse() )
			{
				responseCode = postMethod.getHttpResponse().getStatusLine().getStatusCode();
			}
			String responseMessage = postMethod.getResponseBodyAsString();
			httpResponseInfo = new HttpResponseInfo( responseCode, responseMessage );
		}
		catch( IOException ignore )
		{
		}
		return httpResponseInfo;
	}

	/**
	 * Generates and returns the response URI.
	 * 
	 * @return The response URI.
	 */
	protected String getResponseURI()
	{
		String responseURI = "/" + responseCounter;
		responseCounter++ ;
		return responseURI;
	}

	/**
	 * An internal method that sets up the underlying URL connection.
	 * 
	 * @throws IOException
	 *            If an exception is encountered during URL connection setup.
	 */
	protected void internalConnect() throws IOException
	{
		serializationContext.instantiateTypes = false;
		postMethod = new ExtendedPostMethod( url );
		setHttpRequestHeaders();
		actionContext = new ActionContext();
		connected = true;
	}

	/**
	 * Processes the HTTP response headers and body.
	 */
	protected Object processHttpResponse( InputStream inputStream ) throws ClassNotFoundException, IOException,
			ClientStatusException, ServerStatusException
	{
		return processHttpResponseBody( inputStream );
	}

	/**
	 * Processes the HTTP response body.
	 */
	protected Object processHttpResponseBody( InputStream inputStream ) throws ClassNotFoundException, IOException,
			ClientStatusException, ServerStatusException
	{
		DataInputStream din = new DataInputStream( inputStream );
		ActionMessage message = new ActionMessage();
		actionContext.setRequestMessage( message );
		MessageDeserializer deserializer = new AmfMessageDeserializer();
		deserializer.initialize( serializationContext, din, null/* trace */);
		deserializer.readMessage( message, actionContext );
		din.close();
		context.setProperty( AMFResponse.AMF_RESPONSE_ACTION_MESSAGE, message );
		return processAmfPacket( message );
	}

	/**
	 * Processes the AMF packet.
	 */
	@SuppressWarnings( "unchecked" )
	protected Object processAmfPacket( ActionMessage packet ) throws ClientStatusException, ServerStatusException
	{
		processAmfHeaders( packet.getHeaders() );
		return processAmfBody( packet.getBodies() );
	}

	/**
	 * Processes the AMF headers by dispatching them to an AMF header processor,
	 * if one exists.
	 */
	protected void processAmfHeaders( ArrayList<MessageHeader> headers ) throws ClientStatusException
	{
		// No need to process headers if there's no AMF header processor.
		if( amfHeaderProcessor == null )
			return;

		for( MessageHeader header : headers )
			amfHeaderProcessor.processHeader( header );
	}

	/**
	 * Processes the AMF body. Note that this method won't work if batching of
	 * AMF messages is supported at some point but for now we are guaranteed to
	 * have a single message.
	 */
	protected Object processAmfBody( ArrayList<MessageBody> messages ) throws ServerStatusException
	{
		for( MessageBody message : messages )
		{
			String targetURI = message.getTargetURI();

			if( targetURI.endsWith( MessageIOConstants.RESULT_METHOD ) )
			{
				return message.getData();
			}
			else if( targetURI.endsWith( MessageIOConstants.STATUS_METHOD ) )
			{
				// String exMessage = "Server error";
				// HttpResponseInfo responseInfo = generateHttpResponseInfo();
				// ServerStatusException exception = new ServerStatusException(
				// exMessage, message.getData(), responseInfo );

				return message.getData();
				// throw exception;
			}
		}
		return null; // Should not happen.
	}

	/**
	 * Writes the output buffer and processes the HTTP response.
	 */
	protected Object send( ByteArrayOutputStream outBuffer ) throws ClassNotFoundException, IOException,
			ClientStatusException, ServerStatusException
	{
		// internalConnect.
		internalConnect();

		postMethod.setEntity( new ByteArrayEntity( outBuffer.toByteArray() ) );

		ProxyUtils.initProxySettings( context.getModelItem() == null ? SoapUI.getSettings() : context.getModelItem()
				.getSettings(), postMethod, httpState, url, context );

		HttpClientSupport.execute( postMethod, httpState );
		context.setProperty( AMFResponse.AMF_POST_METHOD, postMethod );

		return processHttpResponse( responseBodyInputStream() );
	}

	private ByteArrayInputStream responseBodyInputStream() throws IOException
	{
		byte[] responseBody = postMethod.getResponseBody();
		ByteArrayInputStream bais = new ByteArrayInputStream( responseBody );
		context.setProperty( AMFResponse.AMF_RAW_RESPONSE_BODY, responseBody );
		return bais;
	}

	/**
	 * Sets the Http request headers, including the cookie headers.
	 */
	protected void setHttpRequestHeaders()
	{
		if( httpRequestHeaders != null )
		{
			for( Map.Entry<String, String> element : httpRequestHeaders.entrySet() )
			{
				String key = element.getKey();
				String value = element.getValue();
				postMethod.setHeader( key, value );
			}
		}
	}

	/**
	 * Registers a custom alias for a class name bidirectionally.
	 * 
	 * @param alias
	 *           The alias for the class name.
	 * @param className
	 *           The concrete class name.
	 */
	public static void registerAlias( String alias, String className )
	{
		ClassAliasRegistry registry = ClassAliasRegistry.getRegistry();
		registry.registerAlias( alias, className );
		registry.registerAlias( className, alias );
	}
}
