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

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringReader;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.wsdl.Definition;
import javax.wsdl.Import;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLWriter;

import org.apache.commons.collections.list.TreeList;
import org.apache.log4j.Logger;
import org.xml.sax.InputSource;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.impl.WsdlInterfaceFactory;
import com.eviware.soapui.impl.support.definition.export.WsdlDefinitionExporter;
import com.eviware.soapui.impl.wsdl.WsdlInterface;
import com.eviware.soapui.impl.wsdl.WsdlOperation;
import com.eviware.soapui.impl.wsdl.support.soap.SoapUtils;
import com.eviware.soapui.impl.wsdl.support.soap.SoapVersion;
import com.eviware.soapui.impl.wsdl.support.wsdl.WsdlUtils;
import com.eviware.soapui.impl.wsdl.testcase.WsdlTestRunContext;
import com.eviware.soapui.model.iface.Interface;
import com.eviware.soapui.model.mock.MockResult;
import com.eviware.soapui.model.mock.MockRunListener;
import com.eviware.soapui.model.propertyexpansion.PropertyExpander;
import com.eviware.soapui.model.support.AbstractMockRunner;
import com.eviware.soapui.model.support.ModelSupport;
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.XmlUtils;

/**
 * MockRunner that dispatches Http Requests to their designated
 * WsdlMockOperation if possible
 * 
 * @author ole.matzura
 */

@SuppressWarnings( "unchecked" )
public class WsdlMockRunner extends AbstractMockRunner
{
	private WsdlMockService mockService;
	private final List<WsdlMockResult> mockResults = Collections.synchronizedList( new TreeList() );
	private long maxResults = 100;
	private int removed = 0;
	private final WsdlMockRunContext mockContext;
	private final Map<String, StringToStringMap> wsdlCache = new HashMap<String, StringToStringMap>();
	private boolean running;
	private boolean logEnabled = true;
	private final static Logger log = Logger.getLogger( WsdlMockRunner.class );

	public WsdlMockRunner( WsdlMockService mockService, WsdlTestRunContext context ) throws Exception
	{
		this.mockService = mockService;

		Set<WsdlInterface> interfaces = new HashSet<WsdlInterface>();

		for( int i = 0; i < mockService.getMockOperationCount(); i++ )
		{
			WsdlOperation operation = mockService.getMockOperationAt( i ).getOperation();
			if( operation != null )
				interfaces.add( operation.getInterface() );
		}

		for( WsdlInterface iface : interfaces )
			iface.getWsdlContext().loadIfNecessary();

		initWsdlCache();

		mockContext = new WsdlMockRunContext( mockService, context );

		start();
	}

	private void initWsdlCache()
	{
		for( Interface iface : mockService.getMockedInterfaces() )
		{
			if( !iface.getInterfaceType().equals( WsdlInterfaceFactory.WSDL_TYPE ) )
				continue;

			try
			{
				WsdlDefinitionExporter exporter = new WsdlDefinitionExporter( ( WsdlInterface )iface );

				String wsdlPrefix = getInterfacePrefix( iface ).substring( 1 );
				StringToStringMap parts = exporter.createFilesForExport( "/" + wsdlPrefix + "&part=" );

				for( String key : parts.keySet() )
				{
					if( key.toLowerCase().endsWith( ".wsdl" ) )
					{
						InputSource inputSource = new InputSource( new StringReader( parts.get( key ) ) );
						String content = WsdlUtils.replacePortEndpoint( ( WsdlInterface )iface, inputSource,
								mockService.getLocalMockServiceEndpoint() );

						if( content != null )
							parts.put( key, content );
					}
				}

				wsdlCache.put( iface.getName(), parts );

				log.info( "Mounted WSDL for interface [" + iface.getName() + "] at [" + getOverviewUrl() + "]" );
			}
			catch( Exception e )
			{
				SoapUI.logError( e );
			}
		}
	}

	public String getInterfacePrefix( Interface iface )
	{
		String wsdlPrefix = getOverviewUrl() + "&interface=" + iface.getName();
		return wsdlPrefix;
	}

	public WsdlMockRunContext getMockContext()
	{
		return mockContext;
	}

	public synchronized void addMockResult( WsdlMockResult mockResult )
	{
		if( maxResults > 0 && logEnabled )
			mockResults.add( mockResult );

		while( mockResults.size() > maxResults )
		{
			mockResults.remove( 0 );
			removed++ ;
		}
	}

	public boolean isRunning()
	{
		return running;
	}

	public void stop()
	{
		if( !isRunning() )
			return;

		SoapUI.getMockEngine().stopMockService( this );

		MockRunListener[] mockRunListeners = mockService.getMockRunListeners();

		for( MockRunListener listener : mockRunListeners )
		{
			listener.onMockRunnerStop( this );
		}

		try
		{
			mockService.runStopScript( mockContext, this );
			running = false;
		}
		catch( Exception e )
		{
			SoapUI.logError( e );
		}
	}

	public WsdlMockService getMockService()
	{
		return mockService;
	}

	public long getMaxResults()
	{
		return maxResults;
	}

	public synchronized void setMaxResults( long l )
	{
		this.maxResults = l;

		while( mockResults.size() > l )
		{
			mockResults.remove( 0 );
			removed++ ;
		}
	}

	@Override
	public MockResult dispatchHeadRequest( HttpServletRequest request, HttpServletResponse response )
			throws DispatchException
	{
		response.setStatus( HttpServletResponse.SC_OK );
		return null;
	}

	public WsdlMockResult dispatchPostRequest( WsdlMockRequest mockRequest ) throws Exception
	{
		WsdlMockResult result = null;

		try
		{
			long timestamp = System.currentTimeMillis();

			SoapVersion soapVersion = mockRequest.getSoapVersion();
			if( soapVersion == null )
				throw new DispatchException( "Unrecognized SOAP Version" );

			String soapAction = mockRequest.getSoapAction();
			WsdlOperation operation = null;

			if( SoapUtils.isSoapFault( mockRequest.getRequestContent(), soapVersion ) )
			{
				// we should inspect fault detail and try to find matching operation
				// but not for now..
				WsdlMockOperation faultMockOperation = mockService.getFaultMockOperation();
				if( faultMockOperation != null )
					operation = faultMockOperation.getOperation();
			}
			else
			{
				try
				{
					operation = SoapUtils.findOperationForRequest( soapVersion, soapAction,
							mockRequest.getRequestXmlObject(), mockService.getMockedOperations(),
							mockService.isRequireSoapVersion(), mockService.isRequireSoapAction(),
							mockRequest.getRequestAttachments() );
				}
				catch( Exception e )
				{
					if( mockService.isDispatchResponseMessages() )
					{
						try
						{
							operation = SoapUtils.findOperationForResponse( soapVersion, soapAction,
									mockRequest.getRequestXmlObject(), mockService.getMockedOperations(),
									mockService.isRequireSoapVersion(), mockService.isRequireSoapAction() );

							if( operation != null )
							{
								mockRequest.setResponseMessage( true );
							}
						}
						catch( Exception e2 )
						{
							throw e;
						}
					}
					else
					{
						throw e;
					}
				}
			}

			if( operation != null )
			{
				WsdlMockOperation mockOperation = mockService.getMockOperation( operation );
				if( mockOperation != null )
				{
					long startTime = System.nanoTime();
					// try
					// {
					result = mockOperation.dispatchRequest( mockRequest );
					// }
					// catch( DispatchException e )
					// {
					// result = new WsdlMockResult( mockRequest );
					//
					// String fault = SoapMessageBuilder.buildFault( "Server",
					// e.getMessage(), mockRequest.getSoapVersion() );
					// result.setResponseContent( fault );
					// result.setMockOperation( mockOperation );
					//
					// mockRequest.getHttpResponse().getWriter().write( fault );
					// }

					if( mockRequest.getHttpRequest() instanceof org.mortbay.jetty.Request )
						( ( org.mortbay.jetty.Request )mockRequest.getHttpRequest() ).setHandled( true );

					result.setTimeTaken( ( System.nanoTime() - startTime ) / 1000000 );
					result.setTimestamp( timestamp );
					addMockResult( result );
					return result;
				}
				else
				{
					throw new DispatchException( "Failed to find matching operation for request" );
				}
			}

			throw new DispatchException( "Missing operation for soapAction [" + soapAction + "] and body element ["
					+ XmlUtils.getQName( mockRequest.getContentElement() ) + "] with SOAP Version ["
					+ mockRequest.getSoapVersion() + "]" );
		}
		catch( Exception e )
		{
			if( e instanceof DispatchException )
				throw ( DispatchException )e;

			throw new DispatchException( e );
		}
	}

	public MockResult getMockResultAt( int index )
	{
		return index <= removed ? null : mockResults.get( index - removed );
	}

	public int getMockResultCount()
	{
		return mockResults.size() + removed;
	}

	public synchronized void clearResults()
	{
		mockResults.clear();
	}

	public void release()
	{
		clearResults();
		mockService = null;
		mockContext.clear();
	}

	@Override
	public MockResult dispatchRequest( HttpServletRequest request, HttpServletResponse response )
			throws DispatchException
	{
		Object result = null;

		try
		{
			for( MockRunListener listener : mockService.getMockRunListeners() )
			{
				result = listener.onMockRequest( this, request, response );
				if( result instanceof MockResult )
					return ( MockResult )result;
			}

			WsdlMockRequest mockRequest = new WsdlMockRequest( request, response, mockContext );
			result = mockService.runOnRequestScript( mockContext, this, mockRequest );
			if( !( result instanceof MockResult ) )
			{
				String method = mockRequest.getMethod();

				if( method.equals( "POST" ) )
					result = dispatchPostRequest( mockRequest );
				else
					result = super.dispatchRequest( request, response );
			}

			mockService.runAfterRequestScript( mockContext, this, ( MockResult )result );
			return ( MockResult )result;
		}
		catch( Throwable e )
		{
			if( e instanceof DispatchException )
				throw ( DispatchException )e;
			else
				throw new DispatchException( e );
		}
		finally
		{
			if( result instanceof MockResult )
			{
				for( MockRunListener listener : mockService.getMockRunListeners() )
				{
					listener.onMockResult( ( MockResult )result );
				}
			}
		}
	}

	public MockResult dispatchGetRequest( HttpServletRequest request, HttpServletResponse response )
			throws DispatchException
	{
		try
		{
			String qs = request.getQueryString();
			if( qs != null && qs.toUpperCase().startsWith( "WSDL" ) )
			{
				dispatchWsdlRequest( request, response );
			}
			else
			{
				if( qs != null && qs.startsWith( "cmd=" ) )
				{
					dispatchCommand( request.getParameter( "cmd" ), request, response );
				}
				else
				{
					String docroot = PropertyExpander.expandProperties( mockContext, getMockService().getDocroot() );
					if( StringUtils.hasContent( docroot ) )
					{
						try
						{
							String pathInfo = request.getPathInfo();
							if( pathInfo == null )
								pathInfo = "";

							if( mockService.getPath().length() > 1 && pathInfo.startsWith( mockService.getPath() ) )
								pathInfo = pathInfo.substring( mockService.getPath().length() );

							String filename = docroot + pathInfo.replace( '/', File.separatorChar );
							File file = new File( filename );
							if( file.exists() )
							{
								returnFile( response, file );
							}
						}
						catch( Throwable e )
						{
							throw new DispatchException( e );
						}
					}
				}
			}

			return null;
		}
		catch( Exception e )
		{
			throw new DispatchException( e );
		}
	}

	public void returnFile( HttpServletResponse response, File file ) throws FileNotFoundException, IOException
	{
		FileInputStream in = new FileInputStream( file );
		response.setStatus( HttpServletResponse.SC_OK );
		long length = file.length();
		response.setContentLength( ( int )length );
		response.setContentType( ContentTypeHandler.getContentTypeFromFilename( file.getName() ) );
		Tools.readAndWrite( in, length, response.getOutputStream() );
		in.close();
	}

	private void dispatchCommand( String cmd, HttpServletRequest request, HttpServletResponse response )
			throws IOException
	{
		if( "stop".equals( cmd ) )
		{
			response.setStatus( HttpServletResponse.SC_OK );
			response.flushBuffer();

			SoapUI.getThreadPool().execute( new Runnable()
			{

				public void run()
				{
					try
					{
						Thread.sleep( 500 );
					}
					catch( InterruptedException e )
					{
						e.printStackTrace();
					}
					stop();
				}
			} );
		}
		else if( "restart".equals( cmd ) )
		{
			response.setStatus( HttpServletResponse.SC_OK );
			response.flushBuffer();

			SoapUI.getThreadPool().execute( new Runnable()
			{

				public void run()
				{
					try
					{
						Thread.sleep( 500 );
					}
					catch( InterruptedException e )
					{
						e.printStackTrace();
					}

					stop();
					//
					// try
					// {
					// Thread.sleep( 500 );
					// }
					// catch( InterruptedException e )
					// {
					// e.printStackTrace();
					// }

					try
					{
						mockService.start();
					}
					catch( Exception e )
					{
						e.printStackTrace();
					}

				}
			} );
		}
	}

	protected void dispatchWsdlRequest( HttpServletRequest request, HttpServletResponse response ) throws IOException
	{
		if( request.getQueryString().equalsIgnoreCase( "WSDL" ) )
		{
			printWsdl( response );
			return;
		}

		String ifaceName = request.getParameter( "interface" );
		WsdlInterface iface = ( WsdlInterface )mockService.getProject().getInterfaceByName( ifaceName );
		if( iface == null )
		{
			printInterfaceList( response );
			return;
		}

		StringToStringMap parts = wsdlCache.get( iface.getName() );
		String part = request.getParameter( "part" );
		String content = StringUtils.isNullOrEmpty( part ) ? null : parts.get( part );

		if( content == null )
		{
			printPartList( iface, parts, response );
			return;
		}

		if( content != null )
		{
			printOkXmlResult( response, content );
		}
	}

	public void printOkXmlResult( HttpServletResponse response, String content ) throws IOException
	{
		response.setStatus( HttpServletResponse.SC_OK );
		response.setContentType( "text/xml" );
		response.setCharacterEncoding( "UTF-8" );
		response.getWriter().print( content );
	}

	public void printWsdl( HttpServletResponse response ) throws IOException
	{
		WsdlInterface[] mockedInterfaces = mockService.getMockedInterfaces();
		if( mockedInterfaces.length == 1 )
		{
			StringToStringMap parts = wsdlCache.get( mockedInterfaces[0].getName() );
			printOkXmlResult( response, parts.get( parts.get( "#root#" ) ) );
		}
		else
		{
			try
			{
				WSDLFactory wsdlFactory = WSDLFactory.newInstance();
				Definition def = wsdlFactory.newDefinition();
				for( WsdlInterface iface : mockedInterfaces )
				{
					StringToStringMap parts = wsdlCache.get( iface.getName() );
					Import wsdlImport = def.createImport();
					wsdlImport.setLocationURI( getInterfacePrefix( iface ) + "&part=" + parts.get( "#root#" ) );
					wsdlImport.setNamespaceURI( WsdlUtils.getTargetNamespace( iface.getWsdlContext().getDefinition() ) );

					def.addImport( wsdlImport );
				}

				response.setStatus( HttpServletResponse.SC_OK );
				response.setContentType( "text/xml" );
				response.setCharacterEncoding( "UTF-8" );

				WSDLWriter writer = wsdlFactory.newWSDLWriter();
				writer.writeWSDL( def, response.getWriter() );
			}
			catch( Exception e )
			{
				SoapUI.logError( e );
				throw new IOException( "Failed to create combined WSDL" );
			}
		}
	}

	public void printPartList( WsdlInterface iface, StringToStringMap parts, HttpServletResponse response )
			throws IOException
	{
		response.setStatus( HttpServletResponse.SC_OK );
		response.setContentType( "text/html" );

		PrintWriter out = response.getWriter();
		out.print( "<html><body><p>Parts in interface [" + iface.getName() + "]</p><ul>" );

		for( String key : parts.keySet() )
		{
			if( key.equals( "#root#" ) )
				continue;

			out.print( "<li><a href=\"" );
			out.print( getInterfacePrefix( iface ) + "&part=" + key );
			out.print( "\">" + key + "</a></li>" );
		}

		out.print( "</ul></p></body></html>" );
	}

	public void printInterfaceList( HttpServletResponse response ) throws IOException
	{
		response.setStatus( HttpServletResponse.SC_OK );
		response.setContentType( "text/html" );

		PrintWriter out = response.getWriter();
		out.print( "<html><body><p>Mocked Interfaces in project [" + mockService.getProject().getName() + "]</p><ul>" );

		for( Interface iface : ModelSupport.getChildren( mockService.getProject(), WsdlInterface.class ) )
		{
			out.print( "<li><a href=\"" );
			out.print( getInterfacePrefix( iface ) );
			out.print( "\">" + iface.getName() + "</a></li>" );
		}

		out.print( "</ul></p></body></html>" );
	}

	public String getOverviewUrl()
	{
		return mockService.getPath() + "?WSDL";
	}

	public void setLogEnabled( boolean logEnabled )
	{
		this.logEnabled = logEnabled;
	}

	public void start() throws Exception
	{
		if( running )
			return;

		mockContext.reset();
		mockService.runStartScript( mockContext, this );

		SoapUI.getMockEngine().startMockService( this );
		running = true;

		MockRunListener[] mockRunListeners = mockService.getMockRunListeners();

		for( MockRunListener listener : mockRunListeners )
		{
			listener.onMockRunnerStart( this );
		}
	}
}
