/*
 *  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.submit.transports.http.support.attachments;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import javax.activation.DataHandler;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMultipart;
import javax.mail.internet.PreencodedMimeBodyPart;
import javax.wsdl.Input;
import javax.wsdl.Output;
import javax.xml.namespace.QName;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.log4j.Logger;
import org.apache.xmlbeans.SchemaType;
import org.apache.xmlbeans.XmlBase64Binary;
import org.apache.xmlbeans.XmlCursor;
import org.apache.xmlbeans.XmlHexBinary;
import org.apache.xmlbeans.XmlObject;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.config.PartsConfig;
import com.eviware.soapui.config.PartsConfig.Part;
import com.eviware.soapui.impl.wsdl.AttachmentContainer;
import com.eviware.soapui.impl.wsdl.HttpAttachmentPart;
import com.eviware.soapui.impl.wsdl.WsdlAttachmentContainer;
import com.eviware.soapui.impl.wsdl.WsdlOperation;
import com.eviware.soapui.impl.wsdl.support.MessageXmlPart;
import com.eviware.soapui.impl.wsdl.support.PathUtils;
import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlContext;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlValidator;
import com.eviware.soapui.impl.wsdl.support.xsd.SchemaUtils;
import com.eviware.soapui.model.iface.Attachment;
import com.eviware.soapui.model.iface.Attachment.AttachmentEncoding;
import com.eviware.soapui.support.StringUtils;
import com.eviware.soapui.support.Tools;
import com.eviware.soapui.support.editor.inspectors.attachments.ContentTypeHandler;
import com.eviware.soapui.support.types.StringToStringMap;
import com.eviware.soapui.support.xml.XmlObjectTreeModel;
import com.eviware.soapui.support.xml.XmlObjectTreeModel.XmlTreeNode;
import com.eviware.soapui.support.xml.XmlUtils;

/**
 * Attachment-related utility classes
 * 
 * @author ole.matzura
 */

public class AttachmentUtils
{
	private final static Logger log = Logger.getLogger( AttachmentUtils.class );
	private static final QName XMLMIME_CONTENTTYPE_200505 = new QName( "http://www.w3.org/2005/05/xmlmime",
			"contentType" );
	private static final QName XMLMIME_CONTENTTYPE_200411 = new QName( "http://www.w3.org/2004/11/xmlmime",
			"contentType" );
	private static final QName SWAREF_QNAME = new QName( "http://ws-i.org/profiles/basic/1.1/xsd", "swaRef" );
	public static final QName XOP_HREF_QNAME = new QName( "href" );
	private static final QName XOP_INCLUDE_QNAME = new QName( "http://www.w3.org/2004/08/xop/include", "Include" );
	public static final String ROOTPART_SOAPUI_ORG = "<rootpart@soapui.org>";
	public static final long MAX_SIZE_IN_MEMORY_ATTACHMENT = 500 * 1024;

	public static boolean prepareMessagePart( WsdlAttachmentContainer container, MimeMultipart mp,
			MessageXmlPart messagePart, StringToStringMap contentIds ) throws Exception, MessagingException
	{
		boolean isXop = false;

		XmlObjectTreeModel treeModel = null;
		XmlCursor cursor = messagePart.newCursor();
		XmlObject rootXmlObject = cursor.getObject();

		try
		{
			while( !cursor.isEnddoc() )
			{
				if( cursor.isContainer() )
				{
					// could be an attachment part (as of "old" SwA specs which
					// specify a content
					// element referring to the attachment)
					if( messagePart.isAttachmentPart() )
					{
						String href = cursor.getAttributeText( XOP_HREF_QNAME );
						if( href != null && href.length() > 0 )
						{
							contentIds.put( messagePart.getPart().getName(), href );
						}

						break;
					}

					XmlObject xmlObj = cursor.getObject();
					SchemaType schemaType = xmlObj.schemaType();
					if( schemaType.isNoType() )
					{
						if( treeModel == null )
						{
							treeModel = new XmlObjectTreeModel( messagePart.getSchemaType().getTypeSystem(), rootXmlObject );
						}

						XmlTreeNode tn = treeModel.getXmlTreeNode( xmlObj );
						if( tn != null )
							schemaType = tn.getSchemaType();
					}

					if( AttachmentUtils.isSwaRefType( schemaType ) )
					{
						String textContent = XmlUtils.getNodeValue( cursor.getDomNode() );
						if( StringUtils.hasContent( textContent ) && textContent.startsWith( "cid:" ) )
						{
							textContent = textContent.substring( 4 );

							try
							{
								// is the textcontent already a URI?
								new URI( textContent );
								contentIds.put( textContent, textContent );
							}
							catch( RuntimeException e )
							{
								// not a URI.. try to create one..
								String contentId = textContent + "@soapui.org";
								cursor.setTextValue( "cid:" + contentId );
								contentIds.put( textContent, contentId );
							}
						}
					}
					else if( AttachmentUtils.isXopInclude( schemaType ) )
					{
						String contentId = cursor.getAttributeText( new QName( "href" ) );
						if( contentId != null && contentId.length() > 0 )
						{
							contentIds.put( contentId, contentId );
							isXop = true;

							Attachment[] attachments = container.getAttachmentsForPart( contentId );
							if( attachments.length == 1 )
							{
								XmlCursor cur = cursor.newCursor();
								if( cur.toParent() )
								{
									String contentType = getXmlMimeContentType( cur );
									if( contentType != null && contentType.length() > 0 )
										attachments[0].setContentType( contentType );
								}

								cur.dispose();
							}
						}
					}
					else
					{
						// extract contentId
						String textContent = XmlUtils.getNodeValue( cursor.getDomNode() );
						if( StringUtils.hasContent( textContent ) )
						{
							Attachment attachment = null;
							boolean isXopAttachment = false;

							// is content a reference to a file?
							if( container.isInlineFilesEnabled() && textContent.startsWith( "file:" ) )
							{
								String filename = textContent.substring( 5 );
								if( container.isMtomEnabled() )
								{
									MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );
									String xmimeContentType = getXmlMimeContentType( cursor );

									if( StringUtils.isNullOrEmpty( xmimeContentType ) )
										xmimeContentType = ContentTypeHandler.getContentTypeFromFilename( filename );

									part.setDataHandler( new DataHandler( new XOPPartDataSource( new File( filename ),
											xmimeContentType, schemaType ) ) );
									part.setContentID( "<" + filename + ">" );
									mp.addBodyPart( part );

									isXopAttachment = true;
								}
								else
								{
									if( new File( filename ).exists() )
									{
										inlineData( cursor, schemaType, new FileInputStream( filename ) );
									}
									else
									{
										Attachment att = getAttachmentForFilename( container, filename );
										if( att != null )
											inlineData( cursor, schemaType, att.getInputStream() );
									}
								}
							}
							else
							{
								Attachment[] attachmentsForPart = container.getAttachmentsForPart( textContent );
								if( textContent.startsWith( "cid:" ) )
								{
									textContent = textContent.substring( 4 );
									attachmentsForPart = container.getAttachmentsForPart( textContent );

									Attachment[] attachments = attachmentsForPart;
									if( attachments.length == 1 )
									{
										attachment = attachments[0];
									}
									else if( attachments.length > 1 )
									{
										attachment = buildMulitpartAttachment( attachments );
									}

									isXopAttachment = container.isMtomEnabled();
									contentIds.put( textContent, textContent );
								}
								// content should be binary data; is this an XOP element
								// which should be serialized with MTOM?
								else if( container.isMtomEnabled()
										&& ( SchemaUtils.isBinaryType( schemaType ) || SchemaUtils.isAnyType( schemaType ) ) )
								{
									if( "true".equals( System.getProperty( "soapui.mtom.strict" ) ) )
									{
										if( SchemaUtils.isAnyType( schemaType ) )
										{
											textContent = null;
										}
										else
										{
											for( int c = 0; c < textContent.length(); c++ )
											{
												if( Character.isWhitespace( textContent.charAt( c ) ) )
												{
													textContent = null;
													break;
												}
											}
										}
									}

									if( textContent != null )
									{
										MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );
										String xmimeContentType = getXmlMimeContentType( cursor );

										part.setDataHandler( new DataHandler( new XOPPartDataSource( textContent,
												xmimeContentType, schemaType ) ) );

										textContent = "http://www.soapui.org/" + System.nanoTime();

										part.setContentID( "<" + textContent + ">" );
										mp.addBodyPart( part );

										isXopAttachment = true;
									}
								}
								else if( container.isInlineFilesEnabled() && attachmentsForPart != null
										&& attachmentsForPart.length > 0 )
								{
									attachment = attachmentsForPart[0];
								}
							}

							// add XOP include?
							if( isXopAttachment && container.isMtomEnabled() )
							{
								buildXopInclude( cursor, textContent );
								isXop = true;
							}
							// inline?
							else if( attachment != null )
							{
								inlineAttachment( cursor, schemaType, attachment );
							}
						}
					}
				}

				cursor.toNextToken();
			}
		}
		finally
		{
			cursor.dispose();
		}

		return isXop;
	}

	private static Attachment getAttachmentForFilename( WsdlAttachmentContainer container, String filename )
	{
		for( Attachment attachment : container.getAttachments() )
		{
			if( filename.equals( attachment.getName() ) )
			{
				return attachment;
			}
		}

		return null;
	}

	private static void inlineAttachment( XmlCursor cursor, SchemaType schemaType, Attachment attachment )
			throws Exception
	{
		inlineData( cursor, schemaType, attachment.getInputStream() );
	}

	private static void inlineData( XmlCursor cursor, SchemaType schemaType, InputStream in ) throws IOException
	{
		String content = null;
		byte[] data = Tools.readAll( in, -1 ).toByteArray();

		if( SchemaUtils.isInstanceOf( schemaType, XmlHexBinary.type ) )
		{
			content = new String( Hex.encodeHex( data ) );
		}
		else if( SchemaUtils.isInstanceOf( schemaType, XmlBase64Binary.type ) )
		{
			content = new String( Base64.encodeBase64( data ) );
		}
		else
		{
			content = new String( data );
		}

		XmlCursor c = cursor.newCursor();
		c.setTextValue( content );
		c.dispose();
	}

	private static void buildXopInclude( XmlCursor cursor, String contentId )
	{
		// build xop:Include
		XmlCursor c = cursor.newCursor();
		c.removeXmlContents();
		c.toFirstContentToken();
		c.beginElement( XOP_INCLUDE_QNAME );
		c.insertAttributeWithValue( XOP_HREF_QNAME, "cid:" + contentId );
		c.toNextSibling();
		c.removeXml();
		c.dispose();
	}

	private static Attachment buildMulitpartAttachment( Attachment[] attachments )
	{
		System.out.println( "buildMulitpartAttachment(Attachment[] attachments) not implemented!" );
		return null;
	}

	public static String buildRootPartContentType( String action, SoapVersion soapVersion )
	{
		String contentType = "application/xop+xml; charset=UTF-8; type=\"" + soapVersion.getContentType();
		if( soapVersion == SoapVersion.Soap12 )
			contentType += "; action=\\\"" + action + "\\\"";

		return contentType + "\"";
	}

	public static String buildMTOMContentType( String header, String action, SoapVersion soapVersion )
	{
		int ix = header.indexOf( "boundary" );
		String contentType = "multipart/related; type=\"application/xop+xml\"; start=\"" + ROOTPART_SOAPUI_ORG + "\"; "
				+ "start-info=\"" + soapVersion.getContentType();

		if( soapVersion == SoapVersion.Soap12 && action != null )
			contentType += "\"; action=\"" + action;

		// nested or not? see
		// http://www.eviware.com/forums/index.php?topic=2866.new#new
		// contentType += "; action=\\\"" + action + "\\\"\"; action=\"" + action;

		return contentType + "\"; " + header.substring( ix );
	}

	public static boolean isSwaRefType( SchemaType schemaType )
	{
		return schemaType != null && schemaType.getName() != null && schemaType.getName().equals( SWAREF_QNAME );
	}

	public static String getXmlMimeContentType( XmlCursor cursor )
	{
		String attributeText = cursor.getAttributeText( XMLMIME_CONTENTTYPE_200411 );
		if( attributeText == null )
			attributeText = cursor.getAttributeText( XMLMIME_CONTENTTYPE_200505 );
		return attributeText;
	}

	public static AttachmentEncoding getAttachmentEncoding( WsdlOperation operation,
			HttpAttachmentPart httpAttachmentPart, boolean isResponse )
	{
		if( httpAttachmentPart.getSchemaType() != null )
		{
			if( SchemaUtils.isInstanceOf( httpAttachmentPart.getSchemaType(), XmlBase64Binary.type ) )
				return AttachmentEncoding.BASE64;
			else if( SchemaUtils.isInstanceOf( httpAttachmentPart.getSchemaType(), XmlHexBinary.type ) )
				return AttachmentEncoding.HEX;
		}

		return getAttachmentEncoding( operation, httpAttachmentPart.getName(), isResponse );
	}

	public static AttachmentEncoding getAttachmentEncoding( WsdlOperation operation, String partName, boolean isResponse )
	{
		// make sure we have access
		if( operation == null || operation.getBindingOperation() == null
				|| operation.getBindingOperation().getOperation() == null )
		{
			return AttachmentEncoding.NONE;
		}

		javax.wsdl.Part part = null;

		if( isResponse )
		{
			Output output = operation.getBindingOperation().getOperation().getOutput();
			if( output == null || output.getMessage() == null )
			{
				return AttachmentEncoding.NONE;
			}
			else
			{
				part = output.getMessage().getPart( partName );
			}
		}
		else
		{
			Input input = operation.getBindingOperation().getOperation().getInput();
			if( input == null || input.getMessage() == null )
			{
				return AttachmentEncoding.NONE;
			}
			else
			{
				part = input.getMessage().getPart( partName );
			}
		}

		if( part != null )
		{
			QName typeName = part.getTypeName();
			if( typeName.getNamespaceURI().equals( "http://www.w3.org/2001/XMLSchema" ) )
			{
				if( typeName.getLocalPart().equals( "base64Binary" ) )
				{
					return AttachmentEncoding.BASE64;
				}
				else if( typeName.getLocalPart().equals( "hexBinary" ) )
				{
					return AttachmentEncoding.HEX;
				}
			}
		}

		return AttachmentEncoding.NONE;
	}

	public static boolean isXopInclude( SchemaType schemaType )
	{
		return XOP_INCLUDE_QNAME.equals( schemaType.getName() );
	}

	public static List<HttpAttachmentPart> extractAttachmentParts( WsdlOperation operation, String messageContent,
			boolean addAnonymous, boolean isResponse, boolean forceMtom )
	{
		List<HttpAttachmentPart> result = new ArrayList<HttpAttachmentPart>();

		PartsConfig messageParts = isResponse ? operation.getConfig().getResponseParts() : operation.getConfig()
				.getRequestParts();
		if( messageParts != null )
		{
			for( Part part : messageParts.getPartList() )
			{
				HttpAttachmentPart attachmentPart = new HttpAttachmentPart( part.getName(), part.getContentTypeList() );
				attachmentPart.setType( Attachment.AttachmentType.MIME );
				result.add( attachmentPart );
			}
		}

		if( messageContent.length() > 0 )
		{
			WsdlContext wsdlContext = operation.getInterface().getWsdlContext();
			WsdlValidator validator = new WsdlValidator( wsdlContext );
			try
			{
				XmlObject[] requestDocuments = validator.getMessageParts( messageContent, operation.getName(), isResponse );

				for( XmlObject partDoc : requestDocuments )
				{
					XmlCursor cursor = partDoc.newCursor();
					while( !cursor.isEnddoc() )
					{
						if( cursor.isContainer() )
						{
							SchemaType schemaType = cursor.getObject().schemaType();
							if( schemaType != null )
							{
								String attributeText = AttachmentUtils.getXmlMimeContentType( cursor );

								// xop?
								if( SchemaUtils.isBinaryType( schemaType ) || SchemaUtils.isAnyType( schemaType ) )
								{
									String contentId = cursor.getTextValue();
									if( contentId.startsWith( "cid:" ) )
									{
										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId.substring( 4 ),
												attributeText );
										attachmentPart
												.setType( attributeText == null && !forceMtom ? Attachment.AttachmentType.CONTENT
														: Attachment.AttachmentType.XOP );
										result.add( attachmentPart );
									}
								}
								else if( AttachmentUtils.isXopInclude( schemaType ) )
								{
									String contentId = cursor.getAttributeText( new QName( "href" ) );
									if( contentId != null && contentId.length() > 0 )
									{
										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId, attributeText );
										attachmentPart.setType( Attachment.AttachmentType.XOP );
										result.add( attachmentPart );
									}
								}
								// swaref?
								else if( AttachmentUtils.isSwaRefType( schemaType ) )
								{
									String contentId = cursor.getTextValue();
									if( contentId.startsWith( "cid:" ) )
									{
										HttpAttachmentPart attachmentPart = new HttpAttachmentPart( contentId.substring( 4 ),
												attributeText );
										attachmentPart.setType( Attachment.AttachmentType.SWAREF );
										result.add( attachmentPart );
									}
								}
							}
						}

						cursor.toNextToken();
					}
				}
			}
			catch( Exception e )
			{
				if( e instanceof NullPointerException )
					SoapUI.logError( e );
				log.warn( e.getMessage() );
			}
		}

		if( addAnonymous )
			result.add( new HttpAttachmentPart() );

		return result;
	}

	/**
	 * Adds defined attachments as mimeparts
	 */

	public static void addMimeParts( AttachmentContainer container, List<Attachment> attachments, MimeMultipart mp,
			StringToStringMap contentIds ) throws MessagingException
	{
		// no multipart handling?
		if( !container.isMultipartEnabled() )
		{
			for( int c = 0; c < attachments.size(); c++ )
			{
				Attachment att = attachments.get( c );
				if( att.getAttachmentType() != Attachment.AttachmentType.CONTENT )
				{
					addSingleAttachment( mp, contentIds, att );
				}
			}
		}
		else
		{
			// first identify if any part has more than one attachments
			Map<String, List<Attachment>> attachmentsMap = new HashMap<String, List<Attachment>>();
			for( int c = 0; c < attachments.size(); c++ )
			{
				Attachment att = attachments.get( c );
				if( att.getAttachmentType() == Attachment.AttachmentType.CONTENT )
					continue;

				String partName = att.getPart();

				if( !attachmentsMap.containsKey( partName ) )
				{
					attachmentsMap.put( partName, new ArrayList<Attachment>() );
				}

				attachmentsMap.get( partName ).add( att );
			}

			// add attachments
			for( Iterator<String> i = attachmentsMap.keySet().iterator(); i.hasNext(); )
			{
				attachments = attachmentsMap.get( i.next() );
				if( attachments.size() == 1 )
				{
					Attachment att = attachments.get( 0 );
					addSingleAttachment( mp, contentIds, att );
				}
				// more than one attachment with the same part -> create multipart
				// attachment
				else if( attachments.size() > 1 )
				{
					addMultipartAttachment( mp, contentIds, attachments );
				}
			}
		}
	}

	/**
	 * Adds a mulitpart MimeBodyPart from an array of attachments
	 */

	public static void addMultipartAttachment( MimeMultipart mp, StringToStringMap contentIds,
			List<Attachment> attachments ) throws MessagingException
	{
		MimeMultipart multipart = new MimeMultipart( "mixed" );
		long totalSize = 0;

		for( int c = 0; c < attachments.size(); c++ )
		{
			Attachment att = attachments.get( c );
			String contentType = att.getContentType();
			totalSize += att.getSize();

			MimeBodyPart part = contentType.startsWith( "text/" ) ? new MimeBodyPart() : new PreencodedMimeBodyPart(
					"binary" );

			part.setDataHandler( new DataHandler( new AttachmentDataSource( att ) ) );
			initPartContentId( contentIds, part, att, false );
			multipart.addBodyPart( part );
		}

		MimeBodyPart part = new PreencodedMimeBodyPart( "binary" );

		if( totalSize > MAX_SIZE_IN_MEMORY_ATTACHMENT )
		{
			part.setDataHandler( new DataHandler( new MultipartAttachmentFileDataSource( multipart ) ) );
		}
		else
		{
			part.setDataHandler( new DataHandler( new MultipartAttachmentDataSource( multipart ) ) );
		}

		Attachment attachment = attachments.get( 0 );
		initPartContentId( contentIds, part, attachment, true );

		mp.addBodyPart( part );
	}

	public static void initPartContentId( StringToStringMap contentIds, MimeBodyPart part, Attachment attachment,
			boolean isMultipart ) throws MessagingException
	{
		String partName = attachment.getPart();

		String contentID = attachment.getContentID();
		if( StringUtils.hasContent( contentID ) )
		{
			contentID = contentID.trim();
			int ix = contentID.indexOf( ' ' );
			if( ix != -1 )
				part.setContentID( "<" + ( isMultipart ? contentID.substring( ix + 1 ) : contentID.substring( 0, ix ) )
						+ ">" );
			else
			{
				if( !contentID.startsWith( "<" ) )
					contentID = "<" + contentID;

				if( !contentID.endsWith( ">" ) )
					contentID = contentID + ">";

				part.setContentID( contentID );
			}
		}
		else if( partName != null && !partName.equals( HttpAttachmentPart.ANONYMOUS_NAME ) )
		{
			if( contentIds.containsKey( partName ) )
			{
				part.setContentID( "<" + contentIds.get( partName ) + ">" );
			}
			else
			{
				part.setContentID( "<" + partName + "=" + System.nanoTime() + "@soapui.org>" );
			}
		}

		// set content-disposition
		String name = attachment.getName();
		String file = attachment.getUrl();
		if( PathUtils.isFilePath( file ) )
		{
			int ix = file.lastIndexOf( File.separatorChar );
			if( ix == -1 )
				ix = file.lastIndexOf( '/' );

			if( ix > 0 && ix < file.length() - 1 )
				file = file.substring( ix + 1 );

			part.setDisposition( "attachment; name=\"" + name + "\"; filename=\"" + file + "\"" );
		}
		else
		{
			part.setDisposition( "attachment; name=\"" + name + "\"" );
		}
	}

	/**
	 * Adds a simple MimeBodyPart from an attachment
	 */

	public static void addSingleAttachment( MimeMultipart mp, StringToStringMap contentIds, Attachment att )
			throws MessagingException
	{
		String contentType = att.getContentType();
		MimeBodyPart part = contentType.startsWith( "text/" ) ? new MimeBodyPart()
				: new PreencodedMimeBodyPart( "binary" );

		part.setDataHandler( new DataHandler( new AttachmentDataSource( att ) ) );
		initPartContentId( contentIds, part, att, false );

		mp.addBodyPart( part );
	}

	public static final Session JAVAMAIL_SESSION = Session.getDefaultInstance( new Properties() );
}
