
package de.mpg.mpiz_koeln.featureClient;
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 org.biomoby.client.CentralDigestCachedImpl;
import org.biomoby.shared.Central;
import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobySecondaryData;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.data.MobyContentInstance;
import org.biomoby.shared.data.MobyDataJob;
import org.biomoby.shared.data.MobyDataObject;
import org.biomoby.shared.data.MobyDataSecondaryInstance;
import org.biomoby.shared.datatypes.MobyObject;

/**
 * 
 * <p>
 * It features several things:
 * <ul>
 * <li>Calling services with the concrete objects (like AminoAcidSequence)</li>
 * <li>Service discovery based on service name or input/output definition (like return all services which consume an
 * AGI locus code and return PMIDs)</li>
 * <li>Parallelization for multiple service calls</li>
 * <li>Using a timeout if the service does not return in that time</li>
 * <li>Calling a service once with possible different inputs</li>
 * </ul>
 * 
 * How to use the this client can be learned by looking a the examples found in the
 * de.mpg.mpiz_koeln.featureClient.test.
 * 
 * @author <A HREF="mailto:andreas.groscurth@gmail.com">Andreas Groscurth</A>
 */
public class FeatureClient {
	private static final int DEFAULT_TIMEOUT = 20; // default timeout for one call - stop after 20secs
	// a set to store services the user wants to call and provides them via name
	private Set< MobyService > servicesByName;
	// map storing the output definition for a group of services
	private Map< String, Collection< String >> outputMap;
	// the secondaries
	private Collection< MobyDataSecondaryInstance > serviceSecondaryInputs;
	// the timeout
	private long timeout;

	// which services shall be filtered
	private List< String > filterList;
	// moby intern storage of input objects - its a global variable as the user can add inputs for multiple service
	// calls separately
	private MobyContentInstance mobyContent;
	private String user; // if the service requires authentification
	private String password; // if the service requires authentification
	private Central central; // the central to use
	private boolean validateService; // shall the services be validated?

	/**
	 * Creates a new <tt>FeatureClient</tt> instance with the given moby central and the default timeout of 20s.
	 * 
	 * @param central the central to be used
	 */
	public FeatureClient( Central central ) {
		this( central, DEFAULT_TIMEOUT );
	}

	/**
	 * Creates a new <tt>FeatureClient</tt> instance default the central in Canada. It uses the default timeout of
	 * 20s.
	 * 
	 * @throws MobyException
	 */
	public FeatureClient() throws MobyException {
		this( new CentralDigestCachedImpl(), DEFAULT_TIMEOUT );
	}

	/**
	 * Creates a new <tt>FeatureClient</tt> instance with the given moby central and a given timeout.
	 * 
	 * @param central the central to use
	 * @param timeout the timeout how long a service call is valid
	 */
	public FeatureClient( Central central, long timeout ) {
		this.central = central;
		setTimeout( timeout );
		mobyContent = new MobyContentInstance();
		validateService = true;
	}

	/**
	 * Sets whether the services which will be called, shall be validated if they are working. This is done via a script
	 * at the official moby central repository.
	 * 
	 * @param validateServices validate ?
	 */
	public void setValidateServices(boolean validateServices) {
		this.validateService = validateServices;
	}

	/**
	 * Returns whether the services shall be validated.
	 * 
	 * @return validate ?
	 */
	boolean isValidateService() {
		return validateService;
	}

	/**
	 * Sets the central to be used. The central is identified by its URL and its URI
	 * 
	 * @param url the endpoint of the central
	 * @param uri the namespace of the central
	 * @throws MobyException
	 */
	public void setCentral(String url, String uri) throws MobyException {
		this.central = new CentralDigestCachedImpl( url, uri );
	}

	/**
	 * Calls all given services and returns their result.
	 * 
	 * @param services list of services to be called
	 * @return list of <tt>FeatureClientResult</tt>
	 */
	public Collection< FeatureClientResult > call(Collection< MobyService > services) {
		Collection< FeatureClientResult > result = ServiceCallFactory.start( services, this );
		mobyContent = new MobyContentInstance();
		return result;
	}

	/**
	 * Calls a service and returns the one result the service returns. This is used if one wants to call one service
	 * which returns exactly one result.
	 * 
	 * @param <T> the datatype which the service returns
	 * @return the one service result
	 * @throws FeatureClientException something happened
	 */
	public <T> T callWithSingleResult() throws FeatureClientException {
		// use the normal calling procedure
		Collection< FeatureClientResult > collection = call();
		if ( collection.isEmpty() ) {
			return null;
		}
		// fetch the one result and get the datatype from this results
		Collection< T > collection2 = collection.iterator().next().getSingleCallResult();
		return collection2.isEmpty() ? null : collection2.iterator().next();
	}

	/**
	 * Calls a service and returns the results of the service. This is used if one wants to call one service which
	 * returns a collection of results.
	 * 
	 * @param <T> the datatype which the service returns
	 * @return the collection of results
	 * @throws FeatureClientException
	 */
	public <T> Collection< T > callWithMultipleResult() throws FeatureClientException {
		Collection< FeatureClientResult > collection = call();
		if ( collection.isEmpty() ) {
			return null;
		}
		return collection.iterator().next().getSingleCallResult();
	}

	/**
	 * Calls a service multiple times and returns it results. This is used if one wants to call one service more than
	 * once.
	 * 
	 * @param <T> the datatype which the service returns
	 * @return the results of the multiple calls.
	 * @throws FeatureClientException
	 */
	public <T> Map< String, Collection< T > > callServiceMultipleTimes() throws FeatureClientException {
		Collection< FeatureClientResult > collection = call();
		if ( collection.isEmpty() ) {
			return null;
		}
		return collection.iterator().next().getMultipleCallResult();
	}

	/**
	 * Calls one or several services (which are found by the find-service-procedure) and returns their results.
	 * 
	 * @return list of service call results
	 * @throws FeatureClientException
	 */
	public Collection< FeatureClientResult > call() throws FeatureClientException {
		Collection< FeatureClientResult > result = ServiceCallFactory.start( this );
		// clear the input for further calls
		mobyContent = new MobyContentInstance();
		return result;
	}

	/**
	 * Returns the central which is used
	 * 
	 * @return the central
	 */
	Central getCentral() {
		return central;
	}

	/**
	 * Returns the input of the services
	 * 
	 * @return the input
	 */
	MobyContentInstance getInput() {
		return mobyContent;
	}

	/**
	 * Calls the moby service and returns its result.
	 * 
	 * @param mobyService the service to be called
	 * @return the result list of the service call
	 */
	public Collection< FeatureClientResult > call(MobyService mobyService) {
		return call( Collections.singletonList( mobyService ) );
	}

	/**
	 * Sets the input for a single service call. The input is a simple input and can not be used to set complex data
	 * types. The simple object input is identified via its namespace and its id.
	 * 
	 * @param namespace the namespace of the object
	 * @param id the identifier of the object
	 * @throws FeatureClientException
	 */
	public void setSingleCallInput(String namespace, String id) throws FeatureClientException {
		setSingleCallInput( namespace, id, "" );
	}

	/**
	 * Sets the input for a single service call. The input is a simple input and can not be used to set complex data
	 * types. It also sets the name of the object if the service requires explicit naming of the input parameter.
	 * 
	 * @param namespace the namespace of the object
	 * @param id the identifier of the object
	 * @param name the article name of the object
	 * @throws FeatureClientException
	 */
	public void setSingleCallInput(String namespace, String id, String name) throws FeatureClientException {
		MobyObject mobyObject = new MobyObject( namespace, id );
		mobyObject.setName( name );
		setSingleCallInput( mobyObject );
	}

	/**
	 * Sets the inputs for a single service call. The inputs can either be a simple object type or a complex type. This
	 * method is only to be used if a service requires more than one input. If one wishes to call a service multiple
	 * times see {@link #addMultipleCallInput(String, MobyObject...)}
	 * 
	 * @param inputs the inputs for the service call
	 * @throws FeatureClientException
	 */
	public void setSingleCallInput(MobyObject... inputs) throws FeatureClientException {
		try {
			// create a new moby internal structure to add the given inputs
			mobyContent = new MobyContentInstance();
			// as its a single call we have one job to do
			MobyDataJob mobyDataJob = new MobyDataJob();
			for ( MobyObject mobyObject : inputs ) {
				// if we have several inputs, all inputs have to be identifiable via their article name - so an error is
				// thrown if no name is given
				if ( inputs.length > 1 && ( mobyObject.getName() == null || mobyObject.getName().equals( "" ) ) ) {
					throw new FeatureClientException( "All inputs have to have a article name set ! "
							+ "(call setName with the articlename of the input parameter)" );
				}
				// convert the given MobyObject into a MobyDataObject used in the jMoby API
				MobyDataObject mobyDataObject = FeatureClientUtility.convertInput2Moby( mobyObject );
				// add the object to the job with the object name as identifier
				mobyDataJob.put( mobyObject.getName(), mobyDataObject );
			}
			// and set the job to the input structure
			mobyContent.put( mobyDataJob );
		}
		catch ( Exception e ) {
			throw new FeatureClientException( e );
		}
	}

	public void addMultipleCallInput(String id) throws FeatureClientException {
		addMultipleCallInput( "", id );
	}

	public void addMultipleCallInput(JobIdentifier identifier, String id) throws FeatureClientException {
		addMultipleCallInput( identifier, "", id );
	}

	public void addMultipleCallInput(JobIdentifier identifier, String namespace, String id)
			throws FeatureClientException {
		addMultipleCallInput( identifier, namespace, id, "" );
	}

	public void addMultipleCallInput(JobIdentifier identifier, String namespace, String id, String name)
			throws FeatureClientException {
		MobyObject mobyObject = new MobyObject( namespace, id );
		mobyObject.setName( name );
		addMultipleCallInput( identifier, mobyObject );
	}

	/**
	 * Adds a new simple input to the client. This is used if services shall be called multiple times with different
	 * inputs.
	 * 
	 * @param namespace the namespace of the input
	 * @param id the identifier of the input
	 * @throws FeatureClientException
	 */
	public void addMultipleCallInput(String namespace, String id) throws FeatureClientException {
		addMultipleCallInput( namespace, id, "" );
	}

	/**
	 * Adds a new simple input to the client. This is used if services shall be called multiple times with different
	 * inputs. It also sets the article name of the object in case the service require this !
	 * 
	 * @param namespace the namespace of the object
	 * @param id the identifier of the object
	 * @param name the article name of the object
	 * @throws FeatureClientException
	 */
	public void addMultipleCallInput(String namespace, String id, String name) throws FeatureClientException {
		MobyObject mobyObject = new MobyObject( namespace, id );
		mobyObject.setName( name );
		addMultipleCallInput( new JobIdentifier( id ), mobyObject );
	}

	public void addMultipleCallInput(String jobidentifier, MobyObject... inputs) throws FeatureClientException {
		addMultipleCallInput( new JobIdentifier( jobidentifier ), inputs );
	}

	/**
	 * Adds one or more input(s) to the client which are used to call services once or more. If a service requires a
	 * complex data type, this method has to be used. The inputs are not used to call the service more than once, but
	 * only for setting the inputs of one service call. To add more inputs for multiple class this method has to be
	 * called multiple times with different inputs.
	 * 
	 * @param jobIdentifier an identifier to know later which service result was which input
	 * @param inputs the service inputs of a service
	 * @throws FeatureClientException
	 */
	public void addMultipleCallInput(JobIdentifier jobIdentifier, MobyObject... inputs) throws FeatureClientException {
		try {
			// create a new job
			MobyDataJob mobyDataJob = new MobyDataJob();
			for ( MobyObject mobyObject : inputs ) {
				// add all objects to the job
				MobyDataObject mobyDataObject = FeatureClientUtility.convertInput2Moby( mobyObject );
				mobyDataJob.put( mobyObject.getName(), mobyDataObject );
			}
			mobyContent.put( jobIdentifier.getIdentifier(), mobyDataJob );
		}
		catch ( Exception e ) {
			throw new FeatureClientException( e );
		}
	}

	/**
	 * Adds an output definition. The output definition consists of the data type and a list of namespaces (like
	 * 'Object' with namespaces 'PMID', 'PMCID' to get Publications)
	 * 
	 * @param datatype the data type
	 * @param namespaces a list of namespaces
	 */
	public void addOutput(String datatype, String... namespaces) {
		if ( outputMap == null ) {
			outputMap = new HashMap< String, Collection< String > >();
		}
		if ( namespaces == null || namespaces.length == 0 ) {
			outputMap.put( datatype, null );
		}
		else {
			outputMap.put( datatype, Arrays.asList( namespaces ) );
		}
	}

	/**
	 * Adds an output definition. This method is identical to {@link #addOutput(String, String...)} with data type and
	 * null as parameters for namespaces. This is used when the data type does not require specific namespaces.
	 * 
	 * @param datatype the datatype
	 */
	public void addOutput(String datatype) {
		addOutput( datatype, ( String[] ) null );
	}

	/**
	 * Sets the timeout in seconds how long the call shall be kept alive. If a service does not respond in the given
	 * timeout frame the call is terminated.
	 * 
	 * @param timeout the timeout in seconds
	 */
	public void setTimeout(long timeout) {
		this.timeout = timeout;
	}

	/**
	 * Adds a secondary (parameter) to the request.
	 * 
	 * @param name the name of the secondary
	 * @param value the value of the secondary
	 */
	public void addSecondary(String name, String value) {
		if ( serviceSecondaryInputs == null ) {
			serviceSecondaryInputs = new ArrayList< MobyDataSecondaryInstance >();
		}
		serviceSecondaryInputs.add( new MobyDataSecondaryInstance( new MobySecondaryData( name ), value ) );
	}

	/**
	 * Adds a list of service names to the filter. This filter is kind of a blacklist to remove services, one might not
	 * want to call. The services can either be determined by their name or one can provide an authority to remove all
	 * services of that authority.
	 * 
	 * @param services service names to be removed
	 */
	public void add2Filter(String... services) {
		add2Filter( Arrays.asList( services ) );
	}

	/**
	 * Returns the list of services which shall be filtered out
	 * 
	 * @return the services to be filtered
	 */
	Collection< String > getService2Filter() {
		return filterList;
	}

	/**
	 * Adds the services to the filter list. These services will later be filtered out during service finding and not be
	 * called afterwards. The services can either be determined by their name or one can provide an authority to remove
	 * all services of that authority.
	 * 
	 * @param services the services
	 */
	public void add2Filter(Collection< String > services) {
		if ( filterList == null ) {
			filterList = new ArrayList< String >();
		}
		filterList.addAll( services );
	}

	/**
	 * Adds a service to the request. This service will be called during the calling process. The service is determined
	 * by the authority and the service name, which guarantees to exactly find one service during the service find
	 * process.
	 * 
	 * @param auth the authority of the service
	 * @param name the name of the service
	 */
	public void addService(String auth, String name) {
		if ( servicesByName == null ) {
			servicesByName = new HashSet< MobyService >();
		}
		MobyService service = new MobyService();
		service.setCategory( "" );
		service.setAuthority( auth );
		service.setName( name );
		servicesByName.add( service );
	}

	/**
	 * Adds a service name to the list of service which will be called.<br>
	 * WARNING: It is highly adviced to use {@link FeatureClient#addService(String, String)} and to give also the
	 * authority with the service name, as this method is not guaranteed to find and cache the correct service you might
	 * look for !
	 * 
	 * @param name the service name
	 */
	public void addService(String name) {
		addService( "", name );
	}

	/**
	 * Returns all services one wants to call with the mapping service name -> authority
	 * 
	 * @return
	 */
	Set< MobyService > getServicesByName() {
		return servicesByName;
	}

	/**
	 * Returns the secondary parameters as an array
	 * 
	 * @return
	 */
	MobyDataSecondaryInstance[] getServiceSecondaryInputs() {
		if ( serviceSecondaryInputs == null ) {
			return null;
		}
		return serviceSecondaryInputs.toArray( new MobyDataSecondaryInstance[ serviceSecondaryInputs.size() ] );
	}

	/**
	 * Returns the timeout
	 * 
	 * @return
	 */
	long getTimeout() {
		return timeout;
	}

	/**
	 * Returns the output definitions.
	 * 
	 * @return
	 */
	Map< String, Collection< String >> getOutputDefinition() {
		return outputMap;
	}

	/**
	 * Sets the user and the password for a possible authentification of a service.
	 * 
	 * @param user the user
	 * @param password his/her password
	 */
	public void setAuthentication(String user, String password) {
		this.user = user;
		this.password = password;
	}

	String getUser() {
		return user;
	}

	String getPassword() {
		return password;
	}

	public static class JobIdentifier {
		private String identifier;

		public JobIdentifier( String id ) {
			identifier = id;
		}

		String getIdentifier() {
			return identifier;
		}
	}
}