
package de.mpg.mpiz_koeln.featureClient;

import java.lang.reflect.Method;
import java.lang.reflect.Type;

import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.data.MobyDataBoolean;
import org.biomoby.shared.data.MobyDataComposite;
import org.biomoby.shared.data.MobyDataDateTime;
import org.biomoby.shared.data.MobyDataFloat;
import org.biomoby.shared.data.MobyDataInt;
import org.biomoby.shared.data.MobyDataObject;
import org.biomoby.shared.data.MobyDataString;
import org.biomoby.shared.datatypes.MobyObject;

public class FeatureClientUtility {
	private FeatureClientUtility() {
	}

	/**
	 * Converts the more general/abstract type from the jMoby API into a concrete moby object. This can either be a
	 * simple MobyObject or a complex one (like AminoAcidSequence).
	 * 
	 * @param <T> the MobyObject type
	 * @param dataObject the object general term from the jMoby API
	 * @return the MobyObject
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	static <T> T convertServiceOutput(MobyDataObject dataObject) throws Exception {
		// if we have a composite call the right method
		if ( dataObject instanceof MobyDataComposite ) {
			return ( T ) convertServiceOutputComposite( ( ( MobyDataComposite ) dataObject ) );
		}
		// if we have a MobyObject this one is easy.
		String fullClass = "org.biomoby.shared.datatypes.MobyObject";
		// load the class
		Class< ? > class1 = Class.forName( fullClass );
		// and create an instance
		T o = ( T ) class1.newInstance();

		// invoke the corresponding methods
		Method method = class1.getMethod( "setId", String.class );
		method.invoke( o, dataObject.getId() );

		// invoke the corresponding methods
		method = class1.getMethod( "setNamespace", String.class );
		method.invoke( o, dataObject.getNamespaces()[ 0 ].getName() );

		return o;
	}

	/**
	 * Converts the more general/abstract type from the jMoby API into a concrete complex data type such as
	 * AminoAcidSequence.<br>
	 * 
	 * WARNING: This currently works only for complex data types which do not have a complex data type as child !
	 * 
	 * @param <T> the concrete data type such as AminoAcidSequence
	 * @param composite the object returned from the jMoby API from a service call
	 * @return the data type
	 * @throws Exception
	 */
	@SuppressWarnings("unchecked")
	private static <T> T convertServiceOutputComposite(MobyDataComposite composite) throws Exception {
		// get the data type and replace - to _ as - are not allowed in java as qualifier
		String datatype = composite.getDataType().getName().replaceAll( "-", "_" );
		// build the complete class name
		String fullClass = "org.biomoby.shared.datatypes." + datatype;
		// load the class
		Class< ? > class1 = Class.forName( fullClass );
		// create an instance of the class
		T o = ( T ) class1.newInstance();
		// store the basic class as we are iteratively going up the hierarchy up to Object to find all available methods
		Class< ? > class2 = class1;

		// the keys from the composite are the members of the concrete data type, so with their name we can identify all
		// methods we need to set
		for ( String key : composite.keySet() ) {
			// the method name
			String methodName = "set_" + key;

			// as long as we have not gone up to Object fetch all methods from the current class
			// apparently it is not possible via reflection to get all methods from a class (including the methods of
			// the superclasses) !
			while (!class1.getSimpleName().equals( "Object" )) {
				// get all methods from the current class. Ideally we would search via reflection the method, as we know
				// the name but we dont know what MobyObject (currently only primitives) the method needs, so we need to
				// search for it via name. If we would know that we could call this method directly.
				Method[] methods = class1.getDeclaredMethods();
				for ( Method method : methods ) {
					// is the method the one we want ?
					if ( method.getName().equals( methodName ) ) {
						// get the parameter type. all set methods only have 1 parameter, so thats fine here
						Type type = method.getGenericParameterTypes()[ 0 ];
						// get the object from the composite holding the information
						MobyDataObject dataObject = composite.get( key );
						// now convert the general object into a concrete data type
						MobyObject object = convertObjectForParameter( dataObject, type );
						// if we have a type -> call the set_xxx method to set the information
						if ( object != null ) {
							method.invoke( o, object );
						}
					}
				}
				// iteration step - get the superclass to investigate their methods.
				class1 = class1.getSuperclass();
			}
			// and reset the class reference to search for the next method to call
			class1 = class2;
		}
		// invoke the corresponding methods
		Method method = class1.getMethod( "setId", String.class );
		method.invoke( o, composite.getId() );

		// invoke the corresponding methods
		method = class1.getMethod( "setNamespace", String.class );
		method.invoke( o, composite.getNamespaces()[ 0 ].getName() );
		return o;
	}

	/**
	 * Converts the given dataObject into a MobyObject. Its needed to call a member function of a MobyObject (e.g.
	 * set_Length) with the correct parameter so that the method is discovered.
	 * 
	 * @param dataObject the abstract/general object representing the data type (e.g. AminoAcidSequence)
	 * @param type the type the method which is about to be called via reflection requires
	 * @return a MobyObject (e.g. the concrete AminoAcidSequence) which is usable in the method call
	 * @throws Exception
	 */
	private static MobyObject convertObjectForParameter(MobyDataObject dataObject, Type type) throws Exception {
		// to retrieve the actual class out of type is weird. Only (?) way is via toString which returns 'class: xxx'
		String className = type.toString();
		// so the start of the classname is cut off.
		className = className.substring( className.indexOf( " " ) + 1 );
		// now the class is loaded
		Class< ? > class1 = Class.forName( className );
		// and an object is created
		Object object = class1.newInstance();
		// if the class represents a MobyObject
		if ( object instanceof MobyObject ) {
			// get the MobyObject and fill it with the information from the MobyDataObject
			MobyObject mobyObject = ( MobyObject ) class1.newInstance();
			mobyObject.setId( dataObject.getId() );
			mobyObject.setNamespace( dataObject.getNamespaces()[ 0 ].getName() );
			mobyObject.setValue( dataObject.getValue() );
			return mobyObject;
		}
		// this method is only called if the object is a MobyObject - but you know... so if its not a MobyObject return
		// null
		return null;
	}

	/**
	 * Converts a concrete data type such as AminoAcidSequence into the internal moby data structure.
	 * 
	 * @param object the data type
	 * @return the internal representation of the data type.
	 * @throws Exception
	 */
	static MobyDataObject convertInput2Moby(MobyObject object) throws Exception {
		// get the class of the object
		Class< ? > class1 = object.getClass();
		// and get the name of the class (without the package)
		String className = class1.getSimpleName();

		// in case of a single object the world is easy
		if ( className.equals( "MobyObject" ) ) {
			MobyDataObject dataObject = new MobyDataObject( object.getNamespace(), object.getId() );
			dataObject.setName( object.getName() );
			return dataObject;
		}
		else {
			// create a composite for the moby call
			MobyDataComposite composite = new MobyDataComposite( class1.getSimpleName() );

			// now we need to set all relevant attributes of the data type to the composite.
			// All attributes have the method getMoby_XXX where XXX is the name of the attribute
			// so if we find these methods we can call them and set its information to the composite.
			// iterate through all classes and superclasses until Object
			while (!class1.getSimpleName().equals( "Object" )) {
				// get all methods from the current class
				Method[] methods = class1.getDeclaredMethods();
				for ( Method method : methods ) {
					String name = method.getName();
					int index = name.indexOf( "getMoby_" );
					// if the method starts with getMoby_ we need its value
					if ( index > -1 ) {
						// we only need the name after _ which is identical to the key in the composite
						name = name.substring( index + 8 );
						// invoke the method (as get methods they dont have any parameter so we can give null)
						MobyObject o = ( MobyObject ) method.invoke( object, ( Object[] ) null );
						// and convert it into a MobyDataObject
						MobyDataObject dataObject = convertAttribute2Moby( o );
						// If not possible it was a complex type - this MIGHT work here... i doubt it heavily
						if ( dataObject == null ) {
							dataObject = convertAttribute2Moby( object );
						}
						// and set the object in the composite
						composite.put( name, dataObject );
					}
				}
				// iteration step - get the next superclass
				class1 = class1.getSuperclass();
			}
			// finally set the article name
			composite.setName( object.getName() );
			composite.setId( object.getId() );
			if ( object.getNamespace() != null && !object.getNamespace().equals( "" ) ) {
				composite.setNamespaces( new MobyNamespace[]{MobyNamespace.getNamespace( object.getNamespace() )} );
			}

			return composite;
		}
	}

	/**
	 * Converts a given object into the corresponding MobyDataObject. This is called for the children of a moby data
	 * type.
	 * 
	 * WARNING: Currently its only working if the child of the data type is a primitive and not a complex data type by
	 * itself.
	 * 
	 * @param object the child object of a data type
	 * @return the same object as a different view
	 */
	private static MobyDataObject convertAttribute2Moby(MobyObject object) {
		// TODO: what if child is a complex data type
		// get the simple name of the class (without the package)
		String simpleName = object.getClass().getSimpleName();
		MobyDataObject dataObject = null;
		// in case of a string
		if ( simpleName.equals( "MobyString" ) ) {
			dataObject = new MobyDataString( object.getValue() );
		}
		// in case of an int
		if ( simpleName.equals( "MobyInteger" ) ) {
			dataObject = new MobyDataInt( Integer.valueOf( object.getValue() ) );
		}

		// a simple object
		if ( simpleName.equals( "MobyObject" ) ) {
			dataObject = new MobyDataObject( object.getNamespace(), object.getId() );
		}

		// float number
		if ( simpleName.equals( "MobyFloat" ) ) {
			dataObject = new MobyDataFloat( Double.parseDouble( object.getValue() ) );
		}

		// a boolean
		if ( simpleName.equals( "MobyBoolean" ) ) {
			dataObject = new MobyDataBoolean( object.getValue() );
		}

		// a date
		if ( simpleName.equals( "MobyDateTime" ) ) {
			dataObject = new MobyDataDateTime( MobyDataDateTime.parseISO8601( object.getValue() ) );
		}

		// if we have an object set some general information
		if ( dataObject != null ) {
			dataObject.setId( object.getId() );
			dataObject.setName( object.getName() );
			if ( object.getNamespace() != null && !object.getNamespace().equals( "" ) ) {
				dataObject.setNamespaces( new MobyNamespace[]{MobyNamespace.getNamespace( object.getNamespace() )} );
			}
		}

		return dataObject;
	}
}
