package info.ebenoit.ebul.cmp;


import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;

import info.ebenoit.ebul.func.ThrowingBiConsumer;
import info.ebenoit.ebul.func.ThrowingConsumer;
import info.ebenoit.ebul.func.ThrowingSupplier;
import info.ebenoit.ebul.reflection.Annotations;
import info.ebenoit.ebul.reflection.MemberFinder;



/**
 * This class carries information about a component to be added to the registry. Instances can be fed directly to the
 * registry, or generated from a class' annotations.
 *
 * @author <a href="mailto:ebenoit@ebenoit.info">E. BenoƮt</a>
 */
public final class NewComponentInfo< T >
{
	/** Member finder that looks for the default constructor */
	private static final MemberFinder< Constructor< ? > > CONSTRUCTOR_FINDER;
	/** Member finder that looks for lifecycle methods */
	private static final MemberFinder< Method > LC_METHOD_FINDER;
	/** Member finder that looks for dependency injections using fields */
	private static final MemberFinder< Field > DI_FIELD_FINDER;
	/** Member finder that looks for dependency injections using methods */
	private static final MemberFinder< Method > DI_METHOD_FINDER;


	static {
		CONSTRUCTOR_FINDER = new MemberFinder< >( ( final Class< ? > c ) -> c.getDeclaredConstructors( ) , false );
		NewComponentInfo.CONSTRUCTOR_FINDER
				.setMemberFilter( ( final Constructor< ? > c ) -> c.getParameterCount( ) == 0 );

		LC_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true );
		NewComponentInfo.LC_METHOD_FINDER.setClassesFirst( true );
		NewComponentInfo.LC_METHOD_FINDER
				.setMemberFilter( m -> m.getDeclaredAnnotation( LifecycleMethod.class ) != null );

		DI_FIELD_FINDER = new MemberFinder< >( MemberFinder.FIELD_EXTRACTOR , false );
		NewComponentInfo.DI_FIELD_FINDER.setMemberFilter( f -> f.getDeclaredAnnotation( UseComponent.class ) != null );

		DI_METHOD_FINDER = new MemberFinder< >( MemberFinder.METHOD_EXTRACTOR , true );
		NewComponentInfo.DI_METHOD_FINDER.setMemberFilter( m -> m.getDeclaredAnnotation( UseComponent.class ) != null );
	}


	/**
	 * Creates a new component information record based on a component's annotated class.
	 *
	 * @param klass
	 *            the class to extract the information from
	 * @return the generated information record
	 * @throws ComponentDefinitionException
	 *             if the class cannot be used as a component
	 */
	public static < T > NewComponentInfo< T > fromClass( final Class< T > klass )
			throws ComponentDefinitionException
	{
		final Constructor< ? > constructor = NewComponentInfo.getDefaultConstructor( klass );
		constructor.setAccessible( true );
		final NewComponentInfo< T > info = new NewComponentInfo< T >( ( ) -> {
			return klass.cast( constructor.newInstance( ) );
		} );

		// Sets the new component's name
		final Component aComp = klass.getAnnotation( Component.class );
		final boolean isAnon = klass.isAnnotationPresent( Anonymous.class );
		if ( isAnon ) {
			if ( ParametricComponent.class.isAssignableFrom( klass ) ) {
				throw new ComponentDefinitionException( "parametric component can't be anonymous" );
			}
			if ( aComp != null && !"".equals( aComp.value( ) ) ) {
				throw new ComponentDefinitionException( "named component can't be anonymous" );
			}
			info.setName( null );
		} else if ( aComp != null ) {
			if ( !"".equals( aComp.value( ) ) && ParametricComponent.class.isAssignableFrom( klass ) ) {
				throw new ComponentDefinitionException( "parametric component can't be named" );
			}
			info.setName( aComp.value( ) );
		} else {
			info.setName( "" );
		}

		// Autostart flag
		info.setAutostart( klass.isAnnotationPresent( Autostart.class ) );

		// Driver for other component?
		final DriverFor driverFor = klass.getAnnotation( DriverFor.class );
		if ( driverFor != null ) {
			info.setDriverFor( driverFor.value( ) );
		}

		// Explicit dependencies
		for ( final Class< ? super T > c : Annotations.findParentsWith( klass , Dependencies.class ) ) {
			final Dependencies dependencies = c.getDeclaredAnnotation( Dependencies.class );
			for ( final String dependency : dependencies.value( ) ) {
				info.addDependency( dependency );
			}
		}

		// Find lifecycle methods and dependency injections
		NewComponentInfo.findLifecycleActions( info , klass );
		NewComponentInfo.findDependencyInjectorFields( info , klass );
		NewComponentInfo.findDependencyInjectorMethods( info , klass );

		return info;
	}


	/**
	 * Finds the default constructor for the specified class
	 *
	 * @param klass
	 *            the class
	 * @return the default constructor (even if it is private)
	 * @throws ComponentDefinitionException
	 *             if there is no default constructor
	 */
	private static < T > Constructor< ? > getDefaultConstructor( final Class< T > klass )
			throws ComponentDefinitionException
	{
		final ArrayList< Constructor< ? > > constructors = new ArrayList< >( );
		NewComponentInfo.CONSTRUCTOR_FINDER.find( constructors , klass );
		if ( constructors.isEmpty( ) ) {
			throw new ComponentDefinitionException( "could not find default constructor in " + klass );
		}

		final Constructor< ? > constructor = constructors.get( 0 );
		if ( constructor.getDeclaringClass( ) != klass ) {
			throw new ComponentDefinitionException( "could not find default constructor in " + klass );
		}
		return constructor;
	}


	/**
	 * Finds lifecycle methods in all classes and interfaces a component class extends / implements and adds the
	 * corresponding actions to the information record.
	 *
	 * <p>
	 * FIXME: inefficient algorithm
	 *
	 * @param info
	 *            the information record
	 * @param klass
	 *            the component class
	 * @throws ComponentDefinitionException
	 *             if a class or interface in the hierarchy declares more than one method for the same lifecycle stage
	 */
	private static < T > void findLifecycleActions( final NewComponentInfo< T > info , final Class< T > klass )
			throws ComponentDefinitionException
	{
		final ArrayList< Method > output = new ArrayList< >( );
		NewComponentInfo.LC_METHOD_FINDER.find( output , klass );

		final EnumMap< LifecycleStage , Method > methods = new EnumMap< >( LifecycleStage.class );
		final HashMap< Class< ? > , EnumSet< LifecycleStage > > checks = new HashMap< >( );
		final int nFound = output.size( );
		for ( int i = nFound - 1 ; i >= 0 ; i-- ) {
			final Method m = output.get( i );
			final Class< ? > mc = m.getDeclaringClass( );
			EnumSet< LifecycleStage > present = checks.get( mc );
			if ( present == null ) {
				present = EnumSet.noneOf( LifecycleStage.class );
				checks.put( mc , present );
			}

			final LifecycleStage lma = m.getDeclaredAnnotation( LifecycleMethod.class ).value( );
			if ( !present.add( lma ) ) {
				throw new ComponentDefinitionException( "in class " + mc + ": duplicate lifecycle method for " + lma );
			}
			methods.put( lma , m );
		}

		for ( final EnumMap.Entry< LifecycleStage , Method > entry : methods.entrySet( ) ) {
			final Method m = entry.getValue( );
			m.setAccessible( true );
			info.setLifecycleAction( entry.getKey( ) , o -> m.invoke( o ) );
		}
	}


	/**
	 * Finds fields annotated with {@link UseComponent} and add dependency injection actions setting these fields to the
	 * information structure.
	 *
	 * @param info
	 *            the information structure
	 * @param klass
	 *            the component class
	 * @throws ComponentDefinitionException
	 *             if a static or final field is annotated with {@link UseComponent}, or if the field's type is invalid
	 *             (primitive or array type)
	 */
	private static < T > void findDependencyInjectorFields( final NewComponentInfo< T > info , final Class< T > klass )
			throws ComponentDefinitionException
	{
		final ArrayList< Field > fields = new ArrayList< >( );
		NewComponentInfo.DI_FIELD_FINDER.find( fields , klass );
		for ( final Field f : fields ) {
			if ( ( f.getModifiers( ) & Modifier.STATIC ) != 0 ) {
				throw new ComponentDefinitionException( "in class " + f.getDeclaringClass( ) + ": static field "
						+ f.getName( ) + " annotated with UseComponent" );
			}
			if ( ( f.getModifiers( ) & Modifier.FINAL ) != 0 ) {
				throw new ComponentDefinitionException( "in class " + f.getDeclaringClass( ) + ": final field "
						+ f.getName( ) + " annotated with UseComponent" );
			}

			DependencyInfo dep;
			try {
				dep = NewComponentInfo.depFrom( f.getType( ) , f.getDeclaredAnnotation( UseComponent.class ) );
			} catch ( final ComponentDefinitionException e ) {
				throw new ComponentDefinitionException(
						"in class " + f.getDeclaringClass( ) + ", field " + f.getName( ) + ": " + e.getMessage( ) );
			}
			f.setAccessible( true );
			info.addDependencyInjector( dep , ( o , d ) -> f.set( o , d ) );
		}
	}


	/**
	 * Finds methods annotated with {@link UseComponent} and add dependency injection actions calling these methods to
	 * the information structure.
	 *
	 * @param info
	 *            the information structure
	 * @param klass
	 *            the component class
	 * @throws ComponentDefinitionException
	 *             if a static method or a method with more than one parameter is annotated with {@link UseComponent},
	 *             or if the method parameter's type is invalid (primitive or array type)
	 */
	private static < T > void findDependencyInjectorMethods( final NewComponentInfo< T > info , final Class< T > klass )
			throws ComponentDefinitionException
	{
		final ArrayList< Method > methods = new ArrayList< >( );
		NewComponentInfo.DI_METHOD_FINDER.find( methods , klass );
		for ( final Method m : methods ) {
			if ( ( m.getModifiers( ) & Modifier.STATIC ) != 0 ) {
				throw new ComponentDefinitionException( "in class " + m.getDeclaringClass( ) + ": static method "
						+ m.getName( ) + " annotated with UseComponent" );
			}
			if ( m.getParameterCount( ) != 1 ) {
				throw new ComponentDefinitionException( "in class " + m.getDeclaringClass( ) + ": method "
						+ m.getName( ) + " has more than 1 parameter" );
			}

			DependencyInfo dep;
			try {
				dep = NewComponentInfo.depFrom( m.getParameterTypes( )[ 0 ] ,
						m.getDeclaredAnnotation( UseComponent.class ) );
			} catch ( final ComponentDefinitionException e ) {
				throw new ComponentDefinitionException(
						"in class " + m.getDeclaringClass( ) + ", field " + m.getName( ) + ": " + e.getMessage( ) );
			}
			m.setAccessible( true );
			info.addDependencyInjector( dep , ( o , d ) -> m.invoke( o , d ) );
		}
	}


	/**
	 * Create a dependency from an {@link UseComponent}-annotated field or method
	 *
	 * @param type
	 *            the type of the field or of the method's parameter
	 * @param annotation
	 *            the {@link UseComponent} annotation
	 * @return the dependency information record
	 * @throws ComponentDefinitionException
	 *             if the type is invalid (primitive or array type)
	 */
	private static DependencyInfo depFrom( final Class< ? > type , final UseComponent annotation )
			throws ComponentDefinitionException
	{
		if ( "".equals( annotation.value( ) ) ) {
			return new DependencyInfo( type );
		}
		DependencyInfo.checkClassValidity( type );
		return new DependencyInfo( annotation.value( ) );
	}

	/** The component's instance, or <code>null</code> if a supplier is being used */
	private final T component;
	/** The component's supplier, or <code>null</code> if an instance has been supplied */
	private final ThrowingSupplier< T > supplier;

	/** The component's name, or <code>null</code> if it must be guessed */
	private String name;
	/**
	 * The name of a component this new component acts as a driver for, or <code>null</code> if this component is not a
	 * driver.
	 */
	private String driverFor;
	/** The set of dependency names for the new component */
	private final HashSet< DependencyInfo > dependencies;

	/** Lifecylce actions for the new component */
	private final EnumMap< LifecycleStage , ThrowingConsumer< ? super T > > lcActions;
	/** Dependency injectors */
	private final HashMap< DependencyInfo , ArrayList< ThrowingBiConsumer< Object , Object > > > injectors;

	/** Whether the component should be activated automatically */
	private boolean autostart;


	/**
	 * Initialises a component based on a existing instance
	 *
	 * @param component
	 *            the component's instance
	 */
	public NewComponentInfo( final T component )
	{
		this.component = component;
		this.supplier = null;
		this.dependencies = new HashSet< >( );
		this.lcActions = new EnumMap< >( LifecycleStage.class );
		this.injectors = new HashMap< >( );
	}


	/**
	 * Initialises a component based on a component supplier
	 *
	 * @param supplier
	 *            the supplier to obtain the component from
	 */
	public NewComponentInfo( final ThrowingSupplier< T > supplier )
	{
		this.component = null;
		this.supplier = supplier;
		this.dependencies = new HashSet< >( );
		this.lcActions = new EnumMap< >( LifecycleStage.class );
		this.injectors = new HashMap< >( );
	}


	/**
	 * @return the component, or <code>null</code> if a supplier is in use
	 */
	public T getComponent( )
	{
		return this.component;
	}


	/**
	 * @return the component supplier, or <code>null</code> if an instance was provided
	 */
	public ThrowingSupplier< T > getSupplier( )
	{
		return this.supplier;
	}


	/**
	 * Sets the new component's name
	 *
	 * @param name
	 *            the new component's name. May be <code>null</code> for an anonymous component or {@code ""} for a
	 *            component whose class name will serve as its name.
	 * @return the current object
	 */
	public NewComponentInfo< T > setName( final String name )
	{
		this.name = name;
		return this;
	}


	/**
	 * @return the name of the new component. May be <code>null</code> for an anonymous component or {@code ""} for a
	 *         component whose class name will serve as its name.
	 */
	public String getName( )
	{
		return this.name;
	}


	/**
	 * @return the name of the component for which the current component acts as a driver for
	 */
	public String getDriverFor( )
	{
		return this.driverFor;
	}


	/**
	 * Sets the name of the component for which the current component acts as a driver for
	 *
	 * @param driverFor
	 *            the name of the main component
	 * @return the current object
	 */
	public NewComponentInfo< T > setDriverFor( final String driverFor )
	{
		this.driverFor = driverFor;
		this.dependencies.add( new DependencyInfo( driverFor ) );
		return this;
	}


	/**
	 * Sets the action for one of the component's lifecycle stages
	 *
	 * @param stage
	 *            the lifecyle stage for which the action is being set
	 * @param action
	 *            the action to perform
	 * @return the current object
	 */
	public NewComponentInfo< T > setLifecycleAction( final LifecycleStage stage ,
			final ThrowingConsumer< ? super T > action )
	{
		this.lcActions.put( stage , action );
		return this;
	}


	/**
	 * Reads the action for one of the component's lifecycle stages
	 *
	 * @param stage
	 *            the lifecycle stage for which the action must be returned
	 * @return the action corresponding to the specified lifecycle stage
	 */
	public ThrowingConsumer< ? super T > getLifecycleAction( final LifecycleStage stage )
	{
		return this.lcActions.get( stage );
	}


	/**
	 * @return the modifiable set of dependencies
	 */
	public HashSet< DependencyInfo > getDependencies( )
	{
		return this.dependencies;
	}


	/**
	 * Adds a dependency to the component
	 *
	 * @param name
	 *            the dependency's name
	 * @return the current object
	 */
	public NewComponentInfo< T > addDependency( final String name )
	{
		this.dependencies.add( new DependencyInfo( name ) );
		return this;
	}


	/**
	 * Adds a dependency to the component
	 *
	 * @param klass
	 *            the dependency's type
	 * @return the current object
	 */
	public NewComponentInfo< T > addDependency( final Class< ? > klass )
	{
		this.dependencies.add( new DependencyInfo( klass ) );
		return this;
	}


	/**
	 * @return <code>true</code> if the component is supposed to start automatically
	 */
	public boolean getAutostart( )
	{
		return this.autostart;
	}


	/**
	 * Sets the autostart flag
	 *
	 * @param autostart
	 *            a flag indicating whether the component should be started automatically
	 * @return the current object
	 */
	public NewComponentInfo< T > setAutostart( final boolean autostart )
	{
		this.autostart = autostart;
		return this;
	}


	/**
	 * Accesses the map of dependency injectors. Note that any direct change should be reflected on the dependencies
	 * manually.
	 *
	 * @return the modifiable map of dependency injectors, with the dependency information as the key and a list of
	 *         functions that inject the dependency into the target component as the value.
	 */
	public HashMap< DependencyInfo , ArrayList< ThrowingBiConsumer< Object , Object > > > getInjectors( )
	{
		return this.injectors;
	}


	/**
	 * Adds a dependency injector and registers the new dependency.
	 *
	 * @param dependency
	 *            the name of the component being depended upon
	 * @param injector
	 *            a function that accepts two parameters: the instance into which the dependency is being injected, and
	 *            the instance of the component being injected
	 * @return the current object
	 */
	public NewComponentInfo< T > addDependencyInjector( final DependencyInfo dependency ,
			final ThrowingBiConsumer< ? super T , Object > injector )
	{
		ArrayList< ThrowingBiConsumer< Object , Object > > injectors = this.injectors.get( dependency );
		if ( injectors == null ) {
			injectors = new ArrayList< >( );
			this.injectors.put( dependency , injectors );
		}

		@SuppressWarnings( "unchecked" )
		ThrowingBiConsumer< Object , Object > ci = (ThrowingBiConsumer< Object , Object >) injector;
		injectors.add( ci );

		this.dependencies.add( dependency );
		return this;
	}
}