
package de.mpg.mpiz_koeln.featureClient;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
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 java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;
import org.biomoby.client.MobyRequest;
import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobySecondaryData;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.data.MobyContentInstance;
import org.biomoby.shared.data.MobyDataComposite;
import org.biomoby.shared.data.MobyDataInstance;
import org.biomoby.shared.data.MobyDataJob;
import org.biomoby.shared.data.MobyDataObject;
import org.biomoby.shared.data.MobyDataObjectSet;
import org.biomoby.shared.data.MobyDataSecondaryInstance;

/**
 * Factory class to handle the service calls.<br>
 * It queries a moby central to find service, filters them based on given definitions and on the moby script to
 * determine valid services. It also controls the service calls and the multithreading environment to handle concurrent
 * service calls.
 * 
 * @author <A HREF="mailto:andreas.groscurth@gmail.com">Andreas Groscurth</A>
 */
class ServiceCallFactory {
	private static final Set< MobyService > CACHED_SERVICE_BY_NAME = new HashSet< MobyService >();

	private static final Logger LOGGER = Logger.getLogger( ServiceCallFactory.class );

	private ServiceCallFactory() {
	}

	static Collection< FeatureClientResult > start(FeatureClient client) throws FeatureClientException {
		return new ServiceCallFactory().new ServiceWorker( client ).startCall();
	}

	static Collection< FeatureClientResult > start(Collection< MobyService > mobyServices, FeatureClient client) {
		return new ServiceCallFactory().new ServiceWorker( client ).startCall( mobyServices );
	}

	private class ServiceWorker {
		private FeatureClient client;

		ServiceWorker( FeatureClient client ) {
			this.client = client;
		}

		/**
		 * Calls a MOBY central to find services defined by the client.
		 * 
		 * @return a list of services
		 * @throws ParserConfigurationException
		 */
		private Collection< MobyService > findMobyService() throws ParserConfigurationException {
			Collection< MobyService > services2Call = new ArrayList< MobyService >();

			// get the output definition to search for
			Map< String, Collection< String >> mapDatatypes = client.getOutputDefinition();
			// if definitions were given try to find services based on them
			if ( mapDatatypes != null ) {
				// if there is one - try to find service by that pattern
				services2Call.addAll( findServiceByDatatypes( mapDatatypes ) );
			}

			Set< MobyService > servicesByNames = client.getServicesByName();
			if ( servicesByNames != null && !servicesByNames.isEmpty() ) {
				// copy the cache list
				Set< MobyService > cachedServices = new HashSet< MobyService >( CACHED_SERVICE_BY_NAME );
				// keep only the services which are cached
				cachedServices.retainAll( servicesByNames );
				// add the cached services
				services2Call.addAll( cachedServices );
				// remove all services which are already cached
				servicesByNames.removeAll( CACHED_SERVICE_BY_NAME );
				// find all services which are not cached yet
				Collection< MobyService > collection = findServiceByNames( servicesByNames );
				services2Call.addAll( collection );
				// cache the currently found services
				CACHED_SERVICE_BY_NAME.addAll( collection );
			}

			// if the client shall validate the services it found.
			if ( client.isValidateService() ) {
				Map< String, MobyService > map = new HashMap< String, MobyService >();
				for ( MobyService mobyService : services2Call ) {
					map.put( mobyService.getName().toLowerCase(), mobyService );
				}
				// filter services which are marked as invalid by the cgi script of moby.ucalgary.ca
				services2Call = filterDeadServices( map );
			}
			// filter service which the user does not want
			services2Call = filterServices( services2Call, client.getService2Filter() );
			return services2Call;
		}

		/**
		 * Filters the found service list. All services which are mentioned in the service2Filter list of the client are
		 * removed.
		 * 
		 * @param list the service list found by querying the central
		 * @param service2Filter the names of the services one wants to remove
		 */
		private Collection< MobyService > filterServices(Collection< MobyService > list,
				Collection< String > service2Filter) {
			// if we dont have anything to filter return
			if ( service2Filter == null || service2Filter.isEmpty() ) {
				return list;
			}

			List< MobyService > finalList = new ArrayList< MobyService >();
			for ( MobyService mobyService : list ) {
				// if the service name, unique name or authority is on the filter list it is remove from the basic list.
				if ( !service2Filter.contains( mobyService.getName() )
						&& !service2Filter.contains( mobyService.getUniqueName() )
						&& !service2Filter.contains( mobyService.getAuthority() ) ) {
					finalList.add( mobyService );
				}
			}
			return finalList;
		}

		/**
		 * Calls a list of BioMOBY web services.
		 * 
		 * @param mobyServices the services
		 * @return the results of the calls
		 * @throws FeatureClientException
		 */
		Collection< FeatureClientResult > startCall() throws FeatureClientException {
			try {
				Collection< MobyService > list = findMobyService();
				return startCall( list );
			}
			catch ( ParserConfigurationException e ) {
				throw new FeatureClientException( e );
			}

		}

		/**
		 * Calls a list of BioMOBY web services.
		 * 
		 * @param mobyServices the services
		 * @return the results of the calls
		 */
		Collection< FeatureClientResult > startCall(Collection< MobyService > mobyServices) {
			// do we have at least one entry remaining ?
			if ( mobyServices.isEmpty() ) {
				LOGGER.warn( "No services left after filtering !" );
				return Collections.emptyList();
			}
			// do the actual calling and get back a Collection of MobyServiceCalls.
			// this class is basically only there to memorize which service was used
			// to retrieve what result
			Collection< MobyServiceCall > Collection = doCalls( mobyServices );

			// if no results are available
			if ( Collection.isEmpty() ) {
				return Collections.emptyList();
			}

			Collection< FeatureClientResult > results = new ArrayList< FeatureClientResult >();

			// loop only for transformation of the service result into one or more
			// MobyDataObjects
			for ( MobyServiceCall mobyServiceCall : Collection ) {
				// the service call result
				MobyContentInstance dataInstance = mobyServiceCall.getServiceCallResult();
				// the service which was called
				MobyService mobyService = mobyServiceCall.getMobyService();
				// no result available
				if ( dataInstance == null ) {
					continue;
				}
				LOGGER.info( "Results are available for " + mobyService.getUniqueName() );
				results.add( new FeatureClientResult( mobyService, dataInstance ) );
			}
			return results;
		}

		/**
		 * Filters services which are marked as dead services by the moby validation script at
		 * http://moby.ucalgary.ca/moby/ValidateService. The script is called and parsed to identify the services which
		 * are unavailable. The remaining services are returned.
		 * 
		 * @param Collection the Collection of services found by the finding process
		 * @return the Collection with working services
		 */
		private Collection< MobyService > filterDeadServices(Map< String, MobyService > services) {
			try {
				// open the stream to the validate script
				InputStream inputStream = new URL( "http://moby.ucalgary.ca/moby/ValidateService" ).openStream();
				BufferedReader reader = new BufferedReader( new InputStreamReader( inputStream ) );
				String line;

				while ( ( line = reader.readLine() ) != null) {
					// first entry is the service name, second a boolean indicating if the service is working
					String[] split = line.split( "," );

					if ( split.length == 2 ) {
						// if false -> service is considered as dead
						if ( !Boolean.parseBoolean( split[ 1 ] ) ) {
							if ( services.containsKey( split[ 0 ].toLowerCase() ) ) {
								services.remove( split[ 0 ].toLowerCase() );
							}
						}
					}
				}

				reader.close();
			}
			// if an exception is thrown a warning is logged, but the input Collection is return to provide a working
			// system.
			catch ( IOException e ) {
				LOGGER.warn( "Could not retrieve dead services !", e );
			}

			return services.values();
		}

		/**
		 * Returns a Collection of BioMOBY web services found by their name at a given central.
		 * 
		 * @param map the map which stores the name and the authority of the services to search for
		 * @return a Collection of found BioMOBY services matching to the given names or an empty Collection if no
		 *         services could have been found
		 */
		private Collection< MobyService > findServiceByNames(Set< MobyService > services2Find) {
			Collection< MobyService > Collection = new ArrayList< MobyService >();
			MobyService[] services = null;
			// for each service in the map find it in the central
			for ( MobyService mobyService : services2Find ) {
				LOGGER.info( "Searching for service: " + mobyService.getUniqueName() );

				try {
					// find the corresponding Bio<ol></ol>MOBY service
					services = client.getCentral().findService( mobyService );
				}
				catch ( MobyException e ) {
					LOGGER.error( "Failed to find services for " + mobyService.getUniqueName(), e );
				}

				// no service could be found
				if ( services == null || services.length == 0 ) {
					LOGGER.warn( "No services found for " + mobyService.getUniqueName() );
					continue;
				}

				// because we are searching for an exact match there should only be
				// one entry found
				Collection.add( services[ 0 ] );
			}
			return Collection;
		}

		/**
		 * Returns a Collection of BioMOBY web services found by their in- and output together with namespaces. So
		 * services are searched which match a specific input and a specific output. The map maps for each data type a
		 * Collection of namespaces. <br>
		 * <br>
		 * E.g. Searching for services which consumes an AGI_LocusCode and return a Collection of Publications (data
		 * type = Object, namespaces = PMID,PMCID).
		 * 
		 * @param map the map defining the output of the services to search for
		 * @return a Collection of services which have been found or an empty Collection otherwise
		 * @throws ParserConfigurationException
		 */
		private Collection< MobyService > findServiceByDatatypes(Map< String, Collection< String >> map)
				throws ParserConfigurationException {
			Collection< MobyService > Collection = new ArrayList< MobyService >();
			MobyService mobyService = null;
			MobyService[] mobyServices = null;
			// what is the input the client is using
			MobyContentInstance input = client.getInput();

			// for each entry search for services
			for ( Map.Entry< String, Collection< String >> service : map.entrySet() ) {
				String datatype = service.getKey();
				LOGGER.info( "Searching for datatype: " + datatype + " and namespaces " + service.getValue() );

				mobyService = new MobyService();
				mobyService.setCategory( "" );
				MobyDataJob job = input.get( input.keySet().iterator().next() );
				MobyDataInstance[] inputs = job.getPrimaryData();
				// add each input given by the request as input of the service
				for ( MobyDataInstance dataInstance : inputs ) {
					mobyService.addInput( ( MobyData ) dataInstance );
				}

				// if the datatype is 'Object' create a simple Moby object, otherwise a composite
				MobyDataObject outputObject = datatype.equalsIgnoreCase( "Object" )
						? new MobyDataObject( "" )
						: new MobyDataComposite( datatype.trim() );

				// get all namespaces for the current datatype
				Collection< String > namespaces = service.getValue();

				// if we have namespaces they are also considered
				if ( namespaces != null && !namespaces.isEmpty() ) {
					for ( String string : namespaces ) {
						MobyNamespace mobyNamespace = MobyNamespace.getNamespace( string.trim() );
						outputObject.addNamespace( mobyNamespace );
					}
				}
				mobyService.addOutput( outputObject );

				// find out all services which return single output
				try {
					mobyServices = client.getCentral().findService( mobyService );
				}
				catch ( MobyException e ) {
					LOGGER.error( "Failed to find service for " + service.getKey(), e );
				}

				// add all found services
				if ( mobyServices != null && mobyServices.length > 0 ) {
					Collection.addAll( Arrays.asList( mobyServices ) );
				}

				// remove the current object to avoid a mess in the next call
				mobyService.removeOutput( outputObject );

				// create a new outputObject to get all services which return
				// collections
				MobyDataObjectSet dataObjectSet = new MobyDataObjectSet( "" );
				dataObjectSet.add( outputObject );
				mobyService.addOutput( dataObjectSet );

				mobyServices = null;
				try {
					mobyServices = client.getCentral().findService( mobyService );
				}
				catch ( MobyException e ) {
					LOGGER.error( "Failed to find service for " + service.getKey(), e );
				}
				// add all found services, if some have been found, to the Collection
				if ( mobyServices != null && mobyServices.length > 0 ) {
					Collection.addAll( Arrays.asList( mobyServices ) );
				}
			}
			return Collection;
		}

		/**
		 * The hard work. It calls each service individually in a separate thread with a given timeout defined by the
		 * request. After that time or after the services returns, all available results are collected.
		 * 
		 * @param services the services to call
		 * @return a Collection of <tt>MobyServiceCall</tt> objects which contain the service results
		 */
		private Collection< MobyServiceCall > doCalls(Collection< MobyService > services) {
			// re-create the service Collection into a Collection of Callables - this is
			// mandatory for the used thread mechanism
			Collection< Callable< MobyServiceCall >> callable = new ArrayList< Callable< MobyServiceCall >>(
					services.size() );

			// the MobyServiceCall class implements the Callable interface...
			for ( MobyService mobyService : services ) {
				callable.add( new MobyServiceCall( mobyService ) );
			}
			// the executor to execute the services....
			ExecutorService executorService = Executors.newCachedThreadPool();
			try {
				// invoke the services with the given timeout
				// a multiplier is used as calling the same service more than once is faster than calling it
				// independantly one by one
				double multiplier = client.getInput().size() == 1 ? 1 : client.getInput().size() * 0.5;
				double timeout = client.getTimeout() * multiplier;

				Collection< Future< MobyServiceCall >> results = executorService.invokeAll( callable );
				// Collection to collect all service results
				Collection< MobyServiceCall > resultCollection = new ArrayList< MobyServiceCall >( callable.size() );

				for ( Future< MobyServiceCall > result : results ) {
					// if there are any results available we store them. A service
					// which was terminated because of the timeout will throw an exception here... so nothing to do
					// when an exception occurs.
					try {
						resultCollection.add( result.get( ( long ) timeout, TimeUnit.SECONDS ) );
					}
					// any of the next exception might occur due to the interruption based on the timeout, so we dont
					// need
					// any handling of that
					catch ( CancellationException e ) {
					}
					catch ( InterruptedException e ) {
					}
					catch ( ExecutionException e ) {
					}
					// at this point an exception happened which should not... so lets get informed about it
					catch ( Exception e ) {
						LOGGER.error( "Error in invoking service ", e );
					}
				}
				return resultCollection;
			}
			catch ( InterruptedException e ) {
				LOGGER.warn( "Interrupted somehow.... ", e );
			}
			return Collections.emptyList();
		}

		/**
		 * The <tt>MobyServiceCall<tt> represent the calling of a BioMOBY web service.<br>
		 * It is the interaction point with the JMoby API and therefore used to actually call a web service.<br>
		 * It implements the <tt>Callable<tt> interface so that the machinery which handles all service calls (@see ServiceCallFactory}
		 * can call each service in a different thread with a specific timeout until the service call terminates.
		 */
		private class MobyServiceCall implements Callable< MobyServiceCall > {
			private MobyService mobyService; // the service to call
			private MobyContentInstance result; // the service call result

			/**
			 * Creates a new <tt>MobyServiceCall</tt>
			 * 
			 * @param mobyService the service to be called
			 */
			MobyServiceCall( MobyService mobyService ) {
				this.mobyService = mobyService;
			}

			/**
			 * Returns the service call result.
			 * 
			 * @return MobyDataInstance, the service result
			 */
			MobyContentInstance getServiceCallResult() {
				return result;
			}

			/**
			 * Returns the service which was called
			 * 
			 * @return the moby service
			 */
			MobyService getMobyService() {
				return mobyService;
			}

			/**
			 * The actual call. The service is prepared with the inputs and parameters and called via the MobyRequest
			 * API.
			 * 
			 * @see Callable#call()
			 */
			public MobyServiceCall call() {
				try {
					LOGGER.info( "Setting up " + mobyService.getName() + " for calling " );
					// the calling is done via the MobyRequest class
					MobyRequest mobyRequest = new MobyRequest( client.getCentral() );
					mobyRequest.setInput( client.getInput() );
					// if no central is available - we have a problem - should actually not be a problem at all...
					if ( mobyRequest.getCentralImpl() == null ) {
						throw new NullPointerException( "Central is not initialised" );
					}

					// set the service which is called
					mobyRequest.setService( mobyService );
					// if the caller also has secondaries, we set them to the request
					MobyDataSecondaryInstance[] secondaries = client.getServiceSecondaryInputs();
					if ( secondaries != null && secondaries.length > 0 ) {
						mobyRequest.setSecondaryInput( secondaries );
					}
					else {
						// assuming the user does not provide any secondaries, but
						// the service requires them, we have to initialize them with the default ones from the registry
						// otherwise mobyrequest will complain and stop working (at least at the time i tested this)
						MobySecondaryData[] sec = mobyService.getSecondaryInputs();
						if ( sec != null || sec.length > 0 ) {
							MobyDataSecondaryInstance[] secondaryInstances = new MobyDataSecondaryInstance[ sec.length ];
							for ( int i = 0; i < secondaryInstances.length; i++ ) {
								secondaryInstances[ i ] = new MobyDataSecondaryInstance( sec[ i ],
										sec[ i ].getDefaultValue() );
							}
							mobyRequest.setSecondaryInput( secondaryInstances );
						}
					}
					LOGGER.info( "Start to call " + mobyService.getName() );
					// set the authentification if it is available. this is needed in case a service requires
					// authorification
					if ( client.getUser() != null && client.getUser().length() > 0 ) {
						if ( client.getPassword() != null && client.getPassword().length() > 0 ) {
							mobyRequest.setAuthentication( client.getUser(), client.getPassword() );
						}
					}

					// the service is invoked
					MobyContentInstance contentInstance = mobyRequest.invokeService();
					result = ( contentInstance == null || contentInstance.isEmpty() ) ? null : contentInstance;
					LOGGER.info( "Calling " + mobyService.getName() + " finished !" );
				}
				catch ( Exception e ) {
					LOGGER.warn( "Service of " + mobyService.getUniqueName() + " failed !", e );
				}
				return this;
			}
		}
	}
}